BeeSceneManager — Documentazione Tecnica
Registro di scene, una sola corrente, replace (non stack). Il manager è l’unico owner del loop entity.
📁 Panoramica
BeeSceneManager è il registro delle schermate e il puntatore alla scena attiva. Il motore la crea nel costruttore e la espone come engine.scenes. Tu registri oggetti-scena con add, passi da una all’altra con change, e il game loop chiama update / draw sulla corrente.
Quello che non è: uno stack (niente push/pop, niente overlay di pausa), un tween di transizione. change è un replace. Stesso nome = restart: exit, sweep delle entity, enter di nuovo. Il teardown ora esiste: #leave chiama onExit/exit e poi distrugge le entity; destroy() svuota la Map; engine.destroy() chiama il manager.
Contratto del loop: il manager è l’unico owner del tick e del draw delle entity. scene.update / scene.draw sono logica di scena e HUD — non un secondo for su this.entities.
📁 Contratto di una scena (duck typing)
Non esiste una classe BeeScene obbligatoria. Qualsiasi oggetto va bene. Gli hook sono opzionali; se mancano, quel passo è un no-op. Se sulla stessa scena esistono sia onEnter sia enter, parte solo onEnter (stessa regola per onExit vs exit).
| Hook / campo | Quando | Note reali |
|---|---|---|
onEnter(data) / enter(data) |
Fine di change (#enter) |
data è il secondo argomento di change, default null. |
onExit() / exit() |
Inizio di #leave (change della corrente, remove della corrente, destroy) |
Nessun argomento. Poi parte lo sweep delle entity. |
update(dt, input, engine) |
Ogni tick del manager, prima delle entity | Parte anche se dt <= 0. Logica e input di scena, non il for sulle entity. |
draw(ctx, engine) |
Ogni frame, prima delle entity | HUD / fondo. Poi il manager chiama engine.drawEntity. |
entities |
Lista viva della scena | Se non è un array, add / #enter / addEntity la creano come []. |
persistEntities |
Opt-out dello sweep | Se true, #sweep non chiama destroy e non svuota la lista (vale anche in manager.destroy()). |
📁 Owner del loop: la scena non ritocca le entity
Il manager ticka e disegna currentScene.entities. Se in scene.update / scene.draw iteri già this.entities, nello stesso frame le ritocchi due volte. Non c’è un lock runtime: è un contratto. HUD, collisioni di livello, win/lose, input di menu: sì. for (const e of this.entities) e.update(...): no.
Il motore, se non è in pausa, chiama scenes.update(dt, this.input) e poi updateEntities sulla lista engine.entities. Con una scena corrente, engine.addEntity delega a scenes.addEntity: gli spawn finiscono sulla scena, non sulla lista del motore. engine.entities resta per oggetti aggiunti prima del primo change (o senza scena).
📁 add / has — registra, non avvia
add(name, scene) mette l’oggetto nella Map, assegna scene.engine = this.engine e, se scene.entities non è un array, lo inizializza a []. Non chiama enter. Ritorna this (fluent). Un secondo add con lo stesso nome sovrascrive la voce nella mappa; se quella era la scena corrente, currentScene resta l’oggetto vecchio finché non fai di nuovo change.
Guardie: nome undefined / null / '' → throw new Error('BeeSceneManager.add: name required'). Scena falsy o non-oggetto → throw new Error('BeeSceneManager.add: scene must be an object'). Quindi add(null) e add('menu', null) non arrivano più a un TypeError su .entities.
has(name) è this.scenes.has(String(name ?? '')). Non lancia: has(null) è false a meno che tu abbia (impossibile via add) una chiave vuota.
📁 change — replace, e stesso nome = restart
Ordine reale, se il nome esiste:
1. #leave sulla corrente (se c’è): onExit / exit, poi #sweep (destroy + svuota, salvo persistEntities).
2. currentScene / currentSceneName puntano alla nuova. Copia anche su engine.currentScene.
3. #enter: re-inietta engine, assicura entities, poi onEnter(data) / enter(data).
4. Se siamo dentro scene.update, alza #changedDuringUpdate: le entity della nuova scena partono al frame dopo.
Nome assente → throw new Error('BeeSceneManager: scena non trovata: …'). Nome vuoto → name required. Stesso nome della corrente: non è un no-op. È un restart dello stesso oggetto: esce, sweep, rientra. Utile per un retry se in onEnter riallochii le entity (dopo lo sweep la lista è vuota, a meno di persistEntities).
Non esiste push / pop. Un menu di pausa “sopra” il livello non è supportato: o sostituisci, o gestisci la pausa fuori da questa classe (engine.time.pause()).
📁 change durante update
Il flag #inUpdate è true solo mentre gira scene.update, non durante il tick entity.
Se scene.update chiama change (come BeeMenuScene su Invio), in quello stesso tick onExit / sweep / onEnter sono già corsi. Poi il manager vede #changedDuringUpdate e non chiama #tickEntities. Le entity della scena appena uscita non ricevono un ultimo tick; quelle della nuova partono al frame successivo. È il percorso coperto.
Se invece change parte da un entity.update, il flag è già spento. #leave fa sweep della lista che il compact loop sta scorrendo (list.length = 0). Le entity della nuova scena stanno su un altro array: in pratica non tickano in questo frame, ma non è lo stesso contratto documentato per scene.update. Su un restart dello stesso nome, onEnter può re-pushare sulla stessa lista e gli indici nuovi oltre read potrebbero ricevere un tick nello stesso frame. Preferisci change dall’hook della scena.
📁 remove — toglie dalla Map
remove(name) è nuovo. Se la scena non c’è, no-op e ritorna this. Se è la corrente: #leave (exit + sweep), poi azzera currentScene / currentSceneName / engine.currentScene. Se non è la corrente: solo #sweep (niente onExit). In entrambi i casi scenes.delete(id).
Nome vuoto lancia come gli altri metodi. persistEntities salva le entity dallo sweep, ma la scena esce comunque dalla mappa: restano riferimenti orfani se non le distruggi tu.
📁 addEntity — lista della scena, con guardie
scenes.addEntity(entity) pusha su currentScene.entities, inietta entity.engine e entity.scene, evita i duplicati (indexOf). Ritorna sempre entity.
Guardie silenziose: entity nullo, già destroyed, o nessuna scena corrente → ritorna entity senza throw. Se entities non è un array, lo crea.
engine.addEntity non è più una seconda lista parallela quando c’è una scena: delega a scenes.addEntity. Solo senza corrente fa push su engine.entities. BeeEngine.setScene svuota engine.entities e poi chiama scenes.change (che a sua volta fa leave/sweep della precedente).
📁 Ciclo di vita nel loop: update poi draw
Il motore, se non è in pausa, chiama scenes.update(dt, this.input). Il secondo argomento non è più ignorato: se lo ometti, il manager cade su this.engine.input.
Dentro update, se non c’è scena corrente, esce. Altrimenti:
1. Hook della scena — scene.update(dt, input, engine) parte sempre, anche con dt <= 0. Tiene vivo un menu mentre il mondo è fermo, se qualcuno chiama il manager con dt zero.
2. Cambio a metà tick — se change è partito da quell’hook, return: niente tick entity.
3. Guardia dt — se dt <= 0 return: niente tick entity, niente compact.
4. Entity — #tickEntities: compact in-place (niente filter che alloca). Salta buchi e destroyed. Se active !== false e ha update, la chiama con (dt, input, engine). Poi accorcia list.length.
Pausa del motore. BeeEngine.loop non chiama affatto scenes.update quando time.paused è true. Il ramo “scena sì, entity no” lo vedi solo se chiami scenes.update(0) tu, o se dt è zero senza passare da quella guardia del loop.
draw(ctx = this.ctx): no-op senza scena. Poi scene.draw(ctx, engine) se è una funzione, poi per ogni voce di entities (salta destroyed) chiama engine.drawEntity (lì il motore salta anche visible === false e attraversa i children).
📁 Sweep, persistEntities, destroy
#sweep(scene) è il teardown delle entity: per ciascuna, se non è già destroyed e ha destroy, la chiama; poi list.length = 0. Non dealloca listener del motore né i collision group: quello lo fa (o no) entity.destroy.
scene.persistEntities === true fa return immediato: le entity sopravvivono a change, remove e anche a manager.destroy(). Usalo per un hub che deve restare vivo sotto un menu; altrimenti le ritrovi duplicate al re-enter se onEnter le rialloca.
destroy() sul manager: #leave della corrente, poi sweep di tutte le scene ancora in mappa, scenes.clear(), puntatori a null (anche engine.currentScene). Ritorna this. BeeEngine.destroy() lo chiama, poi svuota engine.entities.
Senza scena corrente, update, draw e addEntity sono no-op. Non esiste start() sul manager: il “doppio start” è di BeeEngine.start() (se isRunning è già true, ritorna).
📁 Esempio: menu → livello, owner unico
📁 Note per chi integra
Un solo owner del loop entity. Il manager ticka sempre se dt > 0 e non c’è stato un change in scene.update. Non iterare this.entities negli hook della scena.
Restart = change sullo stesso nome. Lo sweep ha già distrutto le entity: in onEnter rialloca, non “pulire a mano” a meno che tu abbia messo persistEntities.
Spawn via engine.addEntity dopo il primo change finiscono sulla scena. Prima del change restano su engine.entities e il loop del motore le ticka a parte.
Nessuna transizione. Fade, wipe, load async: tu, prima di change o dentro onEnter. Il manager è sincrono e immediato.
📁 Riepilogo API
| Membro | Ritorna | Scopo |
|---|---|---|
constructor(engine) |
BeeSceneManager |
Salva engine e engine.ctx. Map vuota, corrente null. Throw se engine è falsy. |
scenes |
Map |
Registro vivo nome → oggetto scena. |
currentScene |
object | null |
Oggetto attivo, o null prima del primo change / dopo remove della corrente. |
currentSceneName |
string | null |
Chiave nella Map della corrente. |
add(name, scene) |
this |
Registra. Inietta engine, crea entities se manca. Non entra. Throw su nome vuoto o scena non-oggetto. |
has(name) |
boolean |
Presenza in mappa. Non lancia. Coerce con String(name ?? ''). |
change(name, data = null) |
this |
Replace: leave (exit + sweep) → swap → enter. Stesso nome = restart. Throw se assente. Da scene.update, skip tick entity in questo frame. |
remove(name) |
this |
Se corrente: leave e azzera i puntatori. Altrimenti solo sweep. Poi delete dalla Map. |
addEntity(entity) |
entity |
Push su currentScene.entities (dedup). No-op se nullo, destroyed o senza scena. Non tocca engine.entities. |
update(dt, input) |
void |
Hook scena, poi (se niente change in-hook e dt > 0) compact-tick delle entity. input default engine.input. |
draw(ctx = this.ctx) |
void |
Hook scena, poi engine.drawEntity su ogni entity non destroyed. |
getCurrentScene() |
object | null |
Alias di currentScene. |
getCurrentSceneName() |
string | null |
Alias di currentSceneName. |
destroy() |
this |
Leave della corrente, sweep di tutte le scene, Map vuota, puntatori null. Chiamato da engine.destroy(). |
persistEntities (sullo scene object) |
boolean |
Se true, #sweep è un no-op. Non è un metodo del manager. |
