BeePool — Documentazione Tecnica
Object pool: prealloca, acquire/release, grow fino a max,
reclaim del più vecchio
📁 Panoramica
BeePool è la pila. Vive in src/core/BeePool.js, il barrel la re-esporta con
BEE_POOL_DEFAULTS. Non è un allocator tipizzato, non è un generational GC, non è un
filter() sull’array delle entity. Tiene tre collezioni private: #free (stack),
#live (Set), #order (FIFO dei vivi, per il reclaim). Serve a non fare
new/destroy a ogni sparo: quello è il hitch GC più comune su Canvas.
Quello che non è: un manager di scena, un sistema di fisica, un despawner automatico.
Non conosce BeeEngine.entities. Chi chiama acquire deve anche
addEntity (o usare gioco.spawn). Chi chiama release (di solito
entity.destroy() se l’oggetto è pooled) deve accettare che transform e body restano montati:
il teardown vero è dispose() / clear().
📁 Acquire → release — il contratto
acquire(...args) esce sempre con un item, oppure null. Non lancia. L’ordine è
fisso: pila libera, poi grow, poi reclaim, poi nulla.
Un ciclo: prendi, resetta, usa, rimetti in pila. Se la pila è vuota e
size ha già toccato max, o rubi il più vecchio vivo o torni
null.
-
1. acquirePop da
#free, altrimenticreatesesize < max, altrimenti reclaim. -
2. reset + live
#wake, poireset(item, ...args). Entra in#livee in#order. - 3. usoL’oggetto è tuo. Il pool non lo aggiorna e non lo toglie dalla scena.
-
4. releaseEsce da live,
#sleep, push su#free. Double-release è no-op.
release(item) ignora null e ignora chi non è in #live. Ritorna
this. releaseAll() rilascia una snapshot di #order (non muta la
coda mentre ci cammina). clear() svuota tutto e chiama dispose: dopo, size è
0.
📁 BEE_POOL_DEFAULTS
Oggetto frozen. Il costruttore non fa spread dei default: legge i campi uno a uno.
Senza create (funzione) lancia BeePool: serve create().
| Parametro | Default | Significato |
|---|---|---|
create |
obbligatorio | Factory. In grow riceve gli stessi ...args di acquire. In
prewarm è chiamata senza argomenti. |
reset |
null |
Se manca, acquire prova item.reset(...args). |
dispose |
null |
Solo clear(). Fallback: item.dispose, poi
item.destroy. |
initial |
0 |
Se > 0, il costruttore chiama prewarm(initial). |
max |
256 |
Tetto di size. Non finito → 256. Zero e negativi sono accettati (pool sterile).
|
reclaim |
true |
Qualsiasi valore diverso da false è true. Pieno + reclaim = ruba il più
vecchio vivo. |
📁 Grow, max, reclaim
size è available + inUse (#free.length + #live.size). Non cresce
oltre max: prewarm esce al tetto, acquire non chiama
create se size >= max.
prewarm(count) alloca N item dormienti: create() senza args,
#bind, #sleep, push su #free. Fluent. Utile a boot per evitare il
primo hitch. Il pool di default gioco.bullets fa initial: 32,
max: 256.
Reclaim (default acceso): se #free è vuoto e sei al max, #order.shift() prende
il vivo più vecchio, lo toglie da #live, lo addormenta, poi lo riwake e lo resetta come un
acquire normale. L’oggetto è lo stesso reference: chi lo teneva in mano vede i campi cambiare sotto.
Con reclaim: false (come BeeParticleSystem.particlePool) acquire
torna null e l’emit salta quella particella.
max è pubblico e mutabile a caldo. Alzarlo sblocca nuovo grow. Abbassarlo non
uccide i vivi in eccesso: il tetto vale solo sulle allocazioni successive.
📁 Factory: create, reset, dispose
Tre hook, tre momenti. create nasce l’istanza. reset la riavvolge a ogni
acquire (anche la prima volta dopo grow: create(...args) e
reset(...args)). dispose la smonta solo in clear().
Se non passi reset ma l’item ha item.reset, quello viene chiamato. Stesso schema
in clear: callback dispose, altrimenti item.dispose, altrimenti
item.destroy. Prima di disporre, clear scrive item.pool = null
così un BeeEntity.destroy di fallback non rientra in release.
📁 Bind, wake, sleep — il pool marca gli oggetti
Non è un pool “puro”: muta i campi sull’item. #bind scrive item.pool = this se
è un object. #wake mette destroyed = false, active = true, e
visible = true solo se 'visible' in item. #sleep inverte:
destroyed = true, active = false, nasconde se c’è visible, poi
chiama detach() e recycle() se esistono.
I primitivi (un create che torna un number) passano dal Set ma skip-ano bind/wake/sleep.
Una factory che torna null / undefined / 0 è un buco: l’item
falsy viene comunque messo in live. Non farlo.
📁 Integrazione BeeEntity e BeeEngine
Ogni BeeEntity nasce con this.pool = null. Il pool, all’acquire, ci scrive sé
stesso. Da quel momento entity.destroy() non chiama
#teardown: fa this.pool.release(this) e basta. Transform, parent (già staccato
da #sleep → detach), collider e body restano. Hook vuoto
recycle() sulla entity; BeeBullet.recycle azzera age/vx/vy
prima del reset dell’acquire successivo.
Per smontare davvero un pooled: entity.dispose() (azzera pool e teardown) oppure
pool.clear() / engine.destroy() che gira clear su ogni pool della
Map. Un secondo destroy() su un item già in pila è no-op (release guarda
#live).
BeeEngine tiene this.pools (Map) e crea in costruttore
this.bullets = createPool('bullet', { create: () => new BeeBullet(), reset: … }).
createPool(name, options) accetta anche un BeePool già fatto.
pool(name) torna l’istanza o null. spawn(name, ...args) è
acquire + addEntity se l’item ha update. addEntity rifiuta i
destroyed e non duplica il reference già in lista: un reclaim su un bullet ancora in scena
lo teletrasporta, non lo pusha due volte.
BeeEnemyShooter spara con engine.bullets.acquire + addEntity.
BeeParticleSystem ha un pool interno di POJO, reclaim: false, e compatta
l’array live senza filter.
📁 Leak — cosa tiene vivo, cosa no
Dimenticare release non è un leak JS classico: l’oggetto resta in
#live / #order, il pool lo ancora. È starvation della pila libera. Con
reclaim: true il più vecchio viene riciclato comunque (i “vivi dimenticati” spariscono di
colpo). Con reclaim: false a tetto acquire torna null.
Il leak vero sta fuori dal pool: un body lasciato in BeePhysicsWorld perché
destroy() pooled non chiama body.destroy(); un listener; un figlio entity non
staccato se hai bypassato #sleep. recycle() è il posto per azzerare lo stato
volatile. clear() è il posto per i teardown.
release fa #order.indexOf + splice: O(n) sui vivi. A 256 è
accettabile. Non è un anello compatto.
📁 Dove è facile sbagliare
Acquire senza addEntity. Il pool non iscrive l’oggetto al loop. O
gioco.addEntity, o gioco.spawn('bullet', …). Un POJO senza
update non viene aggiunto nemmeno da spawn.
Destroy pooled ≠ teardown. colpo.destroy() lo rimette in pila. Transform e
body restano. Per ucciderlo di vero: dispose() o pool.clear(). Non azzerare
item.pool a mano e poi aspettarti il recycle.
Reclaim teletrasporta. Al max, il vivo più vecchio diventa il nuovo sparo. Chi aveva
ancora quel reference (array tuo, tween, collisione in corso) vede posizione e flag cambiare nello
stesso frame. Se il “mai rubare i vivi” ti serve, reclaim: false e gestisci il
null.
create deve tornare istanze distinte. Due acquire con lo stesso object
desincronizzano Set e #order. Factory che torna falsy sporca live con
null.
Reset vs recycle. Sleep chiama recycle (rilascio). Acquire chiama
reset (rinascita). Non mettere in recycle la posa del prossimo sparo: non hai
ancora gli args.
Prewarm senza args. create() a boot deve stare in piedi da sola. I parametri
dello spawn arrivano al reset.
📁 Esempio: pool custom e bullets di stock
📁 Riepilogo API
Fluent: prewarm, release, releaseAll, clear
ritornano this. I default sono in BEE_POOL_DEFAULTS (frozen).
create / reset / dispose / max /
reclaim sull’istanza sono pubblici.
| Membro | Ritorna | Scopo |
|---|---|---|
BEE_POOL_DEFAULTS |
oggetto frozen | initial 0 · max 256 · reclaim true. |
new BeePool(options) |
BeePool |
Richiede create. Prewarm se initial > 0. |
available |
number |
Lunghezza di #free. |
inUse |
number |
#live.size. |
size |
number |
available + inUse. Tetto pratico: max. |
prewarm(count) |
this |
Alloca dormienti con create() senza args, fino a max. |
acquire(...args) |
item o null |
Pop / grow / reclaim. Poi bind, wake, reset. null se pieno e non reclaim. |
release(item) |
this |
No-op se non è live. Sleep + push free. O(n) su #order. |
releaseAll() |
this |
Rilascia tutti i vivi (snapshot di #order). |
clear() |
this |
pool = null + dispose/destroy di ogni item. Size a zero. |
create / reset / dispose |
funzione / null |
Hook pubblici. reset/dispose sono null se non passati.
|
max / reclaim |
number / boolean |
Pubblici, mutabili. Abbassare max non uccide i vivi extra. |