BeeEntity — Documentazione Tecnica
Nodo di scene graph: trasformata, gerarchia, ciclo di vita e cinematica
📁 Panoramica
BeeEntity è il nodo base dello scene graph di BeeEngine: da lei derivano il giocatore, i
nemici, le piattaforme, i proiettili e i collezionabili. Ha una responsabilità unica e dichiarata: dati
di trasformata, gerarchia, ciclo di vita e cinematica (velocità → posizione).
Quello che non fa è altrettanto importante: non disegna nulla. Il rendering è responsabilità delle sottoclassi, la traversata del grafo per il disegno è del motore. Le collisioni leggono l’AABB in coordinate mondo, e il solver riscrive il risultato in coordinate locali.
📁 Il concetto chiave: coordinate locali e coordinate mondo
Questa è la differenza più importante rispetto a un’entità “piatta”. x e y sono
sempre locali al parent. Le coordinate assolute si leggono da worldX e
worldY, che sono una cache lazy: non vengono ricalcolate quando muovi l’entità, ma
solo quando qualcuno le legge dopo che la catena è stata marcata come “sporca”.
Il meccanismo è a due tempi. Quando scrivi x o y, un flag di invalidazione si
propaga a tutto il sottoalbero (con uscita anticipata se era già sporco, così non si ripercorre lo
stesso
ramo due volte). Alla lettura di worldX, la catena viene risincronizzata dal parent verso
il
basso e il valore viene messo in cache.
Il vantaggio pratico: muovere un parent aggiorna implicitamente tutti i figli, senza cicli espliciti e senza ricalcoli inutili. Il costo si paga solo alla lettura, e una volta sola per frame per nodo.
📁 BEE_ENTITY_DEFAULTS — parametri cinematici
Nessun magic number sparso nel solver: tutti i parametri fisici vivono in un oggetto congelato ed esportato, sovrascrivibile in costruzione o a runtime sulla singola istanza.
| Parametro | Default | Significato |
|---|---|---|
width / height |
32 |
Dimensioni dell’AABB dell’entità |
gravity |
0 |
Accelerazione verticale in px/s². A 0 l’entità non cade |
friction |
0 |
Attrito orizzontale applicato quando l’entità è a terra |
airFriction |
0 |
Attrito orizzontale applicato in aria |
landingTolerance |
10 |
Banda in px sopra la piattaforma entro cui l’atterraggio è valido |
velocityLookahead |
0.1 |
Frazione di vy usata per anticipare il contatto |
maxFallSpeed |
Infinity |
Clamp della velocità di caduta. Applicato solo se finito |
📁 Costruttore
La firma resta posizionale per compatibilità, con un quinto argomento opzionale per la fisica. Se
physics è omesso, i default vengono usati direttamente senza creare una copia.
📁 Proprietà di stato
| Proprietà | Tipo | Ruolo |
|---|---|---|
x / y |
number |
Posizione locale al parent. In scrittura invalidano la cache mondo |
worldX / worldY |
number |
Posizione assoluta. In scrittura vengono convertite in locale |
vx / vy |
number |
Velocità in px/s, integrate da integrate() |
parent |
BeeEntity|null |
Sola lettura. Si modifica con addChild / detach |
children |
BeeEntity[] |
Sola lettura. Vedi la nota sull’array vivo più sotto |
isGrounded |
boolean |
Vero solo se l’atterraggio è stato riconfermato in questo frame |
active |
boolean |
Se falso, l’entità non viene aggiornata |
visible |
boolean |
Se falso, il motore non la disegna |
destroyed |
boolean |
Marcatore di fine vita. Il parent la scollega al tick successivo |
collider |
BeeRectCollider|null |
Collisore opzionale creato con addRectCollider() |
📁 Gerarchia: addChild, removeChild, detach
addChild() aggancia un nodo mantenendo le sue x/y come locali al
nuovo parent. È difensivo per costruzione: ignora valori falsy, sé stesso e le entità già distrutte,
rifiuta l’aggancio se creerebbe un ciclo nel grafo, ed è idempotente se il nodo è già figlio. Se
l’entità aveva un altro parent viene staccata prima, così non esistono mai doppi riferimenti.
📁 Collisioni: getWorldAABB, collidesWith, addRectCollider
getWorldAABB() restituisce il rettangolo dell’entità in coordinate assolute, pronto da
passare a un sistema di collisioni esterno. collidesWith() esegue il classico test
Axis-Aligned Bounding Box in spazio mondo: due rettangoli si sovrappongono se e solo se si sovrappongono
su entrambi gli assi. È il test più economico possibile, solo confronti numerici.
Un dettaglio di interoperabilità: la controparte non deve essere necessariamente una
BeeEntity. Se l’oggetto passato espone worldX/worldY numerici
vengono usati quelli, altrimenti si ricade su x/y. Questo permette di
collidere anche con piattaforme rappresentate da oggetti semplici.
📁 resolvePlatformCollision — il solver dei contatti solidi
Il metodo lavora in spazio mondo e riscrive il risultato in coordinate locali. Calcola quanto i due rettangoli si sovrappongono su ciascun asse e risolve lungo l’asse con la sovrapposizione minore: se lo scavalcamento verticale è più piccolo di quello orizzontale, l’entità sta toccando la piattaforma da sopra o da sotto, quindi ha senso spingerla fuori verticalmente.
Sul ramo verticale entrano in gioco i due parametri configurabili. velocityLookahead
anticipa
il contatto di una frazione della velocità corrente, e landingTolerance definisce una banda
di qualche pixel sopra la piattaforma entro cui l’atterraggio è considerato valido. Insieme rendono
l’atterraggio morbido e insensibile alle piccole discrepanze di frame-timing, evitando che l’entità
venga respinta di lato per errore quando appoggia sul bordo.
Il valore di ritorno ha un significato preciso e vale la pena sottolinearlo: true significa
“sono atterrato sopra”, non “c’è stata una collisione”. Le spinte laterali e la
respinta dal basso avvengono comunque, ma restituiscono false.
📁 integrate — la cinematica pura
Integra velocità e gravità sulla posizione locale. Non risolve collisioni e non tocca il
canvas: è puro aggiornamento di dati, quindi si può chiamare in isolamento (utile nei test). Esce subito
se l’entità è inattiva, distrutta o se dt non è positivo.
L’ordine delle operazioni è significativo. La gravità si applica solo se non sei a terra, così non
accumula velocità verso il basso mentre appoggi. L’attrito usa friction a terra e
airFriction in volo. La posizione si aggiorna solo se c’è movimento effettivo, evitando
invalidazioni inutili della cache mondo.
L’ultima riga è il punto su cui si sbaglia più spesso: isGrounded viene azzerato
alla fine di ogni integrazione. Il flag va riconfermato ad ogni frame chiamando
resolvePlatformCollision() per ciascuna piattaforma rilevante. Se la scena dimentica di
farlo, l’entità risulterà “in aria” anche se è ferma su un terreno solido.
📁 update — il tick del grafo
Applica la cinematica al nodo, poi ricorre sui figli attivi. La rimozione dei figli distrutti avviene qui, controllata due volte (prima e dopo l’update del figlio, perché un’entità può distruggersi durante il proprio update) e senza allocare un nuovo array: l’indice non avanza quando si rimuove, perché lo splice ha già spostato gli elementi.
📁 draw — hook intenzionalmente vuoto
BeeEntity non renderizza: draw() è un hook vuoto per rispettare la singola
responsabilità. Le sottoclassi disegnano usando worldX/worldY, mentre la
traversata dell’albero per il disegno è compito di BeeEngine.
📁 destroy — teardown ricorsivo
È idempotente: la seconda chiamata esce subito. Marca l’entità come distrutta, inattiva e invisibile, la stacca dal parent per rompere il ciclo di riferimenti, poi svuota la lista figli e distrugge ricorsivamente ciascuno. Infine azzera il riferimento inverso del collisore, se puntava a questa entità.
Il flag destroyed resta comunque un marcatore logico: la rimozione effettiva dall’albero
avviene al tick successivo del parent, dentro update().
📁 Note per chi integra la classe
L’ordine update → resolvePlatformCollision non è intercambiabile. È l’interazione più
importante da conoscere: isGrounded viene resettato in coda a integrate(), e
il solver è l’unico che lo riporta a vero.
Le collisioni non sono automatiche. collidesWith() e
resolvePlatformCollision() vanno invocati esplicitamente dalla scena: BeeEntity non tiene
un registro globale di cosa può collidere con cosa.
Il getter children restituisce l’array interno vivo, non una copia. È una
scelta voluta per non allocare ad ogni frame, ma significa che modificarlo direttamente aggira la
contabilità di parent e cache mondo. Per aggiungere e togliere nodi usa sempre addChild(),
removeChild() e detach().
collidesWith usa l’AABB grezzo dell’entità
(width/height), non l’eventuale collider con offset creato da
addRectCollider(). Il collisore serve ai sistemi esterni che lo leggono esplicitamente.
I setter di x e y normalizzano l’input. Un valore non numerico
diventa 0 invece di propagare NaN in tutta la catena delle trasformate: la
posizione resta sempre in uno stato valido.
Estendere, non modificare. Sovrascrivi update e draw nelle
sottoclassi richiamando super.update() per non perdere cinematica e traversata dei figli.
Le collisioni discrete hanno il limite noto del tunneling: a velocità molto alte
un’entità può attraversare un ostacolo sottile tra un frame e l’altro, perché il test avviene sulla
posizione già aggiornata. Se serve precisione, riduci il passo temporale o limita
maxFallSpeed.
📁 Esempio completo
📁 Riepilogo API
| Membro | Ritorna | Scopo |
|---|---|---|
BeeEntity.worldXOf(node) |
number |
Statico. Coordinata X mondo di un nodo, con fallback su x |
BeeEntity.worldYOf(node) |
number |
Statico. Coordinata Y mondo di un nodo, con fallback su y |
getWorldAABB() |
object |
Rettangolo {x, y, width, height} in coordinate assolute |
addRectCollider(ox, oy, w, h) |
BeeRectCollider |
Crea e assegna un collisore rettangolare con offset opzionale |
addChild(entity) |
BeeEntity |
Aggancia un figlio, con reparenting e guardia anti-ciclo |
removeChild(entity) |
void |
Scollega un figlio senza distruggerlo |
detach() |
this |
Si stacca dal proprio parent, concatenabile |
collidesWith(other) |
boolean |
Test AABB in spazio mondo contro un altro nodo o oggetto |
resolvePlatformCollision(platform) |
boolean |
Risolve un contatto solido. true solo se è un atterraggio |
integrate(dt) |
void |
Integra gravità, attrito e velocità sulla posizione locale |
update(dt, input, engine) |
void |
Tick del nodo e dei figli attivi, con pulizia dei distrutti |
draw(ctx, engine) |
void |
Hook vuoto. Il disegno spetta alle sottoclassi |
destroy() |
void |
Teardown idempotente e ricorsivo sui figli |
