BeeEngine JAVASCRIPT 2D ENGINE
Documentazione – BeeEngine
v2.5.0

BeeSceneManager — Documentazione Tecnica

Registro di scene, una sola corrente, replace (non stack). Il manager è l’unico owner del loop entity.

BeeEngine Mascot

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

export class BeeSceneManager { constructor(engine) { if (!engine) { throw new Error(‘BeeSceneManager: serve un engine’); } this.engine = engine; this.ctx = engine.ctx; this.scenes = new Map(); this.currentScene = null; this.currentSceneName = null; this.#inUpdate = false; this.#changedDuringUpdate = false; } }

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

game.scenes.add(‘menu’, new BeeMenuScene()) .add(‘game’, livello); // currentScene è ancora null: niente tick, niente draw game.scenes.has(‘game’); // true

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

game.scenes.change(‘game’, { level: 3 }); // leave precedente → enter({ level: 3 }) game.scenes.change(‘game’); // restart: exit + sweep + enter; data = null

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

// equivalente, se currentScene esiste: game.addEntity(player); game.scenes.addEntity(player);

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

update(dt, input) { if (!this.currentScene) return; this.#inUpdate = true; this.#changedDuringUpdate = false; const scene = this.currentScene; if (typeof scene.update === ‘function’) { scene.update(dt, input ?? this.engine.input, this.engine); } this.#inUpdate = false; if (this.#changedDuringUpdate) return; if (dt <= 0) return; this.#tickEntities(dt, input ?? this.engine.input); }

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

import { BeeEngine, BeeMenuScene, BeePlayer } from ‘beeengine’; const game = new BeeEngine(‘game’, 800, 600); const livello = { entities: [], onEnter(data) { game.addEntity(new BeePlayer(80, 400)); // data?.level — payload di change(), o null // niente this.entities = []: lo sweep in leave ha già svuotato }, onExit() { // opzionale: musica, listener di scena. Le entity le distrugge #sweep }, update(dt, input, engine) { // collisioni / win: NON iterare this.entities, lo fa il manager if (input.wasPressed(‘Escape’)) engine.scenes.change(‘menu’); }, draw(ctx) { ctx.fillStyle = ‘#0d0f1a’; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); // NON ridisegnare le entity: ci pensa engine.drawEntity } }; game.scenes.add(‘menu’, new BeeMenuScene()) .add(‘game’, livello) .change(‘menu’); game.start();

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