BeeEngine JAVASCRIPT 2D ENGINE
Documentazione – BeeEngine
v2.5.0

BeeEntity — Documentazione Tecnica

Nodo di scene graph: trasformata, gerarchia, ciclo di vita e cinematica

BeeEngine Mascot

📁 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.

import { BeeRectCollider } from ‘../physics/BeeRectCollider.js’; import { BeeEntity, BEE_ENTITY_DEFAULTS } from ‘./src/core/BeeEntity.js’;

📁 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.

const mondo = new BeeEntity(100, 50); const arma = new BeeEntity(10, 5); mondo.addChild(arma); arma.x; // 10 → locale al parent arma.worldX; // 110 → 100 (parent) + 10 (locale) // Scrivere worldX converte automaticamente in locale arma.worldX = 130; arma.x; // 30

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.

new BeeEntity(x, y, width, height, physics) // Un nemico che cade, con attrito a terra e caduta limitata const nemico = new BeeEntity(120, 340, 32, 48, { gravity: 1200, friction: 8, maxFallSpeed: 900 }); // I parametri restano modificabili a runtime nemico.gravity = 0; // es. entra in modalità volo

📁 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.

const giocatore = new BeeEntity(0, 0); const scudo = new BeeEntity(4, 8); giocatore.addChild(scudo); // ritorna scudo, per concatenare scudo.detach(); // stacca senza distruggere, ritorna this giocatore.removeChild(scudo); // no-op se non è figlio // Guardia anti-ciclo: l’aggancio viene rifiutato scudo.addChild(giocatore);

📁 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.

nemico.getWorldAABB(); // { x, y, width, height } in spazio mondo if (nemico.collidesWith(giocatore)) { giocatore.destroy(); } // Collisore con offset e dimensioni personalizzate const hitbox = nemico.addRectCollider(4, 2, 24, 44); // Accesso statico alle coordinate mondo di un nodo qualsiasi BeeEntity.worldXOf(piattaforma); BeeEntity.worldYOf(piattaforma);

📁 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.

const overlapX = Math.min(x + this.width px, px + pw x); const overlapY = Math.min(y + this.height py, py + ph y); if (overlapY < overlapX) { const lookahead = this.vy * this.velocityLookahead; const landingBand = py + this.landingTolerance; // atterraggio dall’alto → grounded // vy < 0 → testa contro il blocco, respinta sotto }

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.

if (this.gravity !== 0 && !this.isGrounded) { this.vy += this.gravity * dt; } const drag = this.isGrounded ? this.friction : this.airFriction; if (drag > 0) { this.vx *= Math.max(0, 1 drag * dt); } this.isGrounded = false; // va riconfermato dal solver

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.

// Nel game loop della scena function loop(dt) { radice.update(dt, input, engine); // Subito dopo: riconferma dei contatti solidi for (const p of piattaforme) { giocatore.resolvePlatformCollision(p); } }

📁 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.

class Giocatore extends BeeEntity { draw(ctx) { ctx.drawImage(this.sprite, this.worldX, this.worldY); } }

📁 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

import { BeeEntity } from ‘beeengine’; // Piattaforma statica, senza gravità const suolo = new BeeEntity(0, 560, 800, 40); // Giocatore con fisica da platform const giocatore = new BeeEntity(80, 400, 32, 48, { gravity: 1600, friction: 10, airFriction: 1, maxFallSpeed: 1200 }); // Un’aura agganciata: si muove insieme al giocatore const aura = new BeeEntity(4, 4, 40, 56); giocatore.addChild(aura); // Salto if (input.isDown(‘Space’) && giocatore.isGrounded) { giocatore.vy = 620; } // Tick: prima la cinematica, poi i contatti giocatore.update(dt, input, engine); giocatore.resolvePlatformCollision(suolo); aura.worldX; // segue il giocatore senza codice aggiuntivo

📁 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