BeeEngine logo BeeEngine JAVASCRIPT 2D ENGINE

Object Pooling JavaScript: Guida a BeePool | BeeEngine Docs

Documentazione – BeeEngine
v2.7.0

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

import { BeePool, BEE_POOL_DEFAULTS } from ‘beeengine’; const sparks = new BeePool({ create() { return { life: 0 }; }, reset(item, life) { item.life = life; }, initial: 16, max: 64, reclaim: false }); const spark = sparks.acquire(0.4); sparks.release(spark);

📁 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. 1. acquirePop da #free, altrimenti create se size < max, altrimenti reclaim.
  2. 2. reset + live#wake, poi reset(item, ...args). Entra in #live e in #order.
  3. 3. usoL’oggetto è tuo. Il pool non lo aggiorna e non lo toglie dalla scena.
  4. 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.

// gioco.bullets — factory + reset sul tipo gioco.bullets; // BeePool, initial 32, max 256, reclaim true const colpo = gioco.bullets.acquire(x, y, 0, 280, 10, 10); gioco.addEntity(colpo); colpo.destroy(); // release, non #teardown gioco.spawn(‘bullet’, x, y, 0, 280); // acquire + addEntity se ha update

📁 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

import { BeeEngine } from ‘beeengine’; const gioco = new BeeEngine(‘testCanvas’, 800, 600); const colpo = gioco.bullets.acquire(40, 80, 0, 280, 10, 10); gioco.addEntity(colpo); gioco.createPool(‘spark’, { create() { return { life: 0 }; }, reset(item, life) { item.life = life; }, initial: 16, max: 64, reclaim: false }); gioco.bullets.inUse; // vivi gioco.bullets.available; // dormienti in pila gioco.bullets.size; // somma, tetto 256

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