BeePathfinder — Documentazione Tecnica
A* e flow field su griglia:
find /
track /
follow,
catalogo gioco.pathfinder
📁 Panoramica
BeePathfinder è il navigatore. Vive in src/core/BeePathfinder.js, il barrel
lo re-esporta con BeePath e BEE_PATH_DEFAULTS. Non è steering con raggio,
non è un navmesh, non è un chase in linea retta. Tiene una griglia (celle + ostacoli), cerca un
cammino A* da cella a cella e, se vuoi, un flow field verso un goal. Senza questo l’AI 2D resta
pattuglia o inseguimento dritto.
Quello che non è: un collider, un body, un smoother LOS. Il path è una lista di
centri-cella. follow scrive worldX/worldY (o
x/y) verso il waypoint corrente. La fisica e le collisioni restano
fuori.
BeeEngine crea un navigatore: this.pathfinder = new
BeePathfinder(). gioco.setGrid(grid) chiama
pathfinder.useGrid(grid). Una tilemap o una matrice cruda si agganciano a mano
con useTilemap / useCells.
📁 Tre fonti: grid, tilemap, cells
Prima di find serve una mappa. Ogni use* azzera
#blocked, fa #bump (sale version) e rialloca i buffer
A* / flow. Una sola fonte alla volta: useGrid stacca la tilemap,
useTilemap stacca la grid.
| Ingresso | Dimensioni | Camminabile |
|---|---|---|
useGrid(grid, originX, originY) |
grid.cols / rows / cellSize |
grid.getCell(col, row) === walkable (default 0).
null (fuori) è solido. |
useTilemap(tilemap) |
tilemap.cols / rows / tileSize |
tilemap.isSolidTile(col, row) !== true. Origine =
tilemap.worldX/Y. |
useCells(data, cols, rows, cellSize, ox, oy) |
argomenti espliciti | data[row][col] === walkable, o la funzione
walkable(value, col, row). |
walkable può essere un numero (confronto stretto) o una funzione. Su
tilemap la funzione non entra: vince isSolidTile.
configure({ walkable }) cambia il predica ma non alza
version — i path già in cache restano.
📁 Integrazione BeeGrid
BeeGrid è la geometria: matrice data[riga][colonna], API
setCell(c, r, val) / getCell(c, r). Il pathfinder legge
getCell a ogni ricerca, quindi un muro scritto sulla grid è visibile al
prossimo find.
setCell non avvisa il navigatore.
track riusa il path se goal e path.version sono
uguali. Un muro nuovo sulla grid, senza setBlocked e senza un altro
useGrid, lascia track sul vecchio corridoio. Per invalidare:
setBlocked, clearBlocked, o riagganciare la fonte.
worldToCell / cellToWorld stanno sul pathfinder, non sulla
grid. I punti mondo sono il centro della cella
((col + 0.5) * cellSize). L’origine di default è (0, 0); su tilemap è
worldX/Y della mappa.
📁 A* — find e findCells
find(start, goal, out) converte gli argomenti in celle e chiama
findCells. Accetta { col, row } oppure un oggetto con
x/y (e opzionali worldX/worldY,
width/height): usa il centro, come
BeeEntity.
Una ricerca: bound e goal camminabile, heap octile o
Manhattan, ricostruzione sui parent. Se il budget scade, found resta
false — niente path parziale.
- 1. boundStart/goal in mappa. Goal non camminabile → path vuoto. Start su un muro è legale.
- 2. A*Heap binario, closed set, costi 1 / √2. Heuristic ammissibile e consistente.
-
3. reconstructCatena
parentdal goal. Celle + punti centro,lengthin pixel. -
4. BeePath
found,version,goalCol/Row. Pronto perfollow.
Buffer typed (Uint32 / Float64 / Int32) e uno
stamp per non azzerare ad ogni search. maxIterations
default 8000: oltre, la ricerca esce e found è false. Passa
out un BeePath già tuo per non allocare.
📁 Diagonali, cornerCut, heuristic
Default diagonal: true, cornerCut: false. Ortho costa 1,
diagonale costa Math.SQRT2. Con cornerCut: false una
diagonale è legale solo se entrambe le celle ortogonali sono camminabili:
non si taglia l’angolo tra due muri. I test coprono il caso 2×2
([[0,1],[1,0]] → niente path; cornerCut: true → sì).
Heuristic: Manhattan (dx + dy) se le diagonali sono spente; octile
(max + (√2 − 1) * min) se sono accese. Allineata ai costi, quindi
ammissibile e consistente: il closed set non riapre i nodi e il path è ottimo sulla
griglia.
📁 Celle bloccate
setBlocked(col, row, true) mette un ostacolo runtime (porta, corpo,
spawn) sopra la grid/tilemap. false lo toglie.
clearBlocked() svuota il Set. Tutti e tre fanno #bump:
version sale, il flow field si invalida, track ricalcola.
La chiave è (row << 16) ^ (col & 0xffff). Fine per mappe da
gioco; oltre 65536 colonne le chiavi collidono. Fuori mappa è sempre solido
(#inBounds).
📁 BeePath — celle, punti, follow
Il risultato non è un array nudo. BeePath tiene
cells[{col,row}], points[{x,y}] (centri mondo),
found, index (waypoint corrente),
goalCol/Row, version, length (somma
ipotenuse). current è il punto sotto index.
finished è true se non found o se l’indice è in fondo.
rewind / clear sono fluent.
follow(entity, path, dt, speed) avanza di
speed * dt verso il waypoint. Raggio di arrivo:
cellSize * arrive (default 0.35). Entro quel raggio salta al
prossimo punto — è l’unico “smoothing”: niente string-pull, niente LOS. Scrive
il centro dell’entity su worldX/Y se è un number (BeeEntity),
altrimenti su x/y. Non tocca vx/vy né il
body. Ritorna true finché c’è un waypoint, poi
false.
track(path, start, goal, out) riusa path se
found, stesso goal in celle e path.version === this.version.
Altrimenti rifà find. chase(entity, target, dt, options)
è track + follow: salva il path su entity[options.key || 'path'],
speed da options.speed o entity.speed (80).
📁 Flow field
flow(goal) è un Dijkstra dal goal (budget
maxIterations * 4), poi su ogni cella prende il vicino a costo
minore e normalizza (dx, dy). Un goal, tanti agenti:
sampleFlow(x, y) o sampleFlow(entity) torna
{ x, y } o null (fuori, irraggiungibile, o flow
stantio: #flowVersion !== version).
Dopo un setBlocked / use* il campo è morto finché non
richiami flow. Non è A* al contrario e non scrive le entity: sei
tu a fare x += dir.x * speed * dt.
📁 Entity, motore, demo
BeeEntity espone worldX/worldY dalla
transform: find / follow / chase usano il
centro (world + size/2). Un POJO con solo
x/y funziona uguale. La demo di stock non chiama il
pathfinder; il contratto è coperto da
scripts/test-beepathfinder.mjs (corridoio, muro pieno, cornerCut,
tilemap solidTiles, track dopo setBlocked, follow su
entity, sampleFlow).
📁 Dove è facile sbagliare
Niente path smoothing LOS. Il cammino è una spezzata sui centri
cella. arrive salta il waypoint quando sei vicino, non tira una
corda attraverso le celle libere. Un agent “liscio” lo fai tu (o inflati le
celle se è più grosso di una tile).
setCell non invalida track. Cambia i
muri e poi setBlocked / useGrid di nuovo, oppure
chiama find ogni volta. configure (diagonali,
walkable, arrive) non alza version.
Goal su muro → found: false. Start su muro è
accettato (l’agente è già lì). Goal fuori mappa o non camminabile: path vuoto,
niente fallback al vicino libero.
follow ignora la fisica. Scrive la posa. Un
BeeRigidBody o un collider non vengono spinti: o navighi senza
body, o copi tu la velocità.
maxIterations è un fail silenzioso. Mappa grande +
goal irraggiungibile: found === false, stesso segnale di “muro
pieno”. Alza il budget o controlla prima isWalkable sul goal.
Flow stantio. Dopo un bump, sampleFlow torna
null finché non rifai flow. Non è un errore: è il
contratto.
📁 Esempio: grid, chase, porte
📁 Riepilogo API
Fluent: configure, useGrid, useTilemap,
useCells, setBlocked, clearBlocked,
flow ritornano this. Default frozen:
BEE_PATH_DEFAULTS (diagonal true, cornerCut false, walkable 0,
maxIterations 8000, arrive 0.35).
| Membro | Ritorna | Scopo |
|---|---|---|
BEE_PATH_DEFAULTS |
oggetto frozen | diagonal true · cornerCut false · walkable 0 · maxIterations 8000 · arrive 0.35. |
new BeePathfinder(options?) |
BeePathfinder |
Navigatore vuoto. Senza use*, find torna
path non found. |
cols / rows / cellSize /
version |
number |
Mappa corrente e generazione ostacoli. version parte da
1 e sale ad ogni bump. |
configure(options) |
this |
diagonal, cornerCut, walkable, maxIterations, arrive. Non bumpa
version. |
useGrid / useTilemap / useCells |
this |
Fonte unica. Argomento mancante lancia. Clear blocked + bump + alloc. |
setBlocked(col, row, blocked?) /
clearBlocked() |
this |
Ostacoli runtime. Bump. clearBlocked è no-op se il Set
è vuoto. |
isWalkable(col, row) |
boolean |
Bound + blocked + tile solida o valore vs walkable. |
worldToCell(x, y) / cellToWorld(col, row) |
{col,row} / {x,y} |
Pixel ↔ cella. World = centro cella + origine. |
find(start, goal, out?) /
findCells(…) |
BeePath |
A*. Goal non walkable o fuori → found: false. |
track(path, start, goal, out?) |
BeePath |
Riusa se stesso goal e stessa version; altrimenti find. |
follow(entity, path, dt, speed) |
boolean |
Avanza i waypoint, scrive il centro. false a fine path. |
chase(entity, target, dt, options?) |
boolean |
track + follow. Path su entity.path (o
options.key). |
flow(goal) / sampleFlow(x, y) |
this / {x,y}|null |
Campo verso un goal. Sample null se stantio o irraggiungibile. |
new BeePath() |
BeePath |
cells, points, found, index, goalCol/Row, version, length.
current / finished /
rewind / clear. |
gioco.pathfinder / gioco.setGrid |
istanza / this |
Un navigatore per motore. setGrid chiama
useGrid. |