BeeAssetManager — Documentazione Tecnica
Modulo core di BeeEngine per il caricamento centralizzato delle risorse di gioco
📁Panoramica
BeeAssetManager è la classe di BeeEngine responsabile del preloading di tutte le risorse multimediali di un gioco HTML5: immagini/sprite, tracce audio e file JSON (mappe, configurazioni, dati di livello). L’idea alla base è semplice ma fondamentale per qualsiasi motore di gioco: non si inizia mai il game loop finché tutte le risorse necessarie non sono effettivamente in memoria. Caricare un’immagine “al volo” nel bel mezzo del rendering causa flicker, sprite mancanti per qualche frame o crash silenziosi.
📁BeeAssetManager
Elimina questo problema centralizzando il caricamento in una fase dedicata, prima dell’avvio vero e proprio. La classe non ha dipendenze esterne: usa solo le API native del browser (Image, Audio, fetch) racchiuse dentro Promise, il che la rende coerente con la filosofia zero-dependency di BeeEngine.
📁Le tre “mappe” di storage
📁Architettura interna
Ogni asset caricato viene salvato in una Map dedicata al proprio tipo, indicizzata per nome logico (una stringa scelta dallo sviluppatore, non il percorso del file). Questa separazione in tre contenitori distinti — invece di un’unica mappa generica — ha un vantaggio pratico: puoi avere un’immagine e un suono con lo stesso nome logico (es. “esplosione”) senza collisioni, e i metodi getter specifici (getImage, getSound, getJSON) restano tipizzati concettualmente, anche se il linguaggio è JS puro.
📁L’uso di Map
L’uso di Map invece di un oggetto {} è una scelta corretta qui: evita i problemi di chiavi che collidono con proprietà ereditate da Object.prototype (es. un asset chiamato “toString”), ed è più esplicita semanticamente quando si itera o si controlla la dimensione.
📁Il pattern Promise-per-singolo-asset
Ogni metodo loadX restituisce una Promise che si risolve solo quando il browser conferma che la risorsa è pronta all’uso:
📁loadImage
loadImage: si affida all’evento nativo onload dell’oggetto Image. Solo quando il browser ha decodificato completamente i byte dell’immagine, la Promise si risolve.
📁loadSound
loadSound: usa oncanplaythrough, un evento più conservativo di onloadeddata — garantisce che l’audio possa essere riprodotto interamente senza buffering, il che è importante per effetti sonori che devono partire istantaneamente (es. il suono di un power-up).
📁loadJSON
loadJSON: usa fetch con gestione esplicita degli errori HTTP (controllo di response.ok), cosa che fetch non fa automaticamente — un 404 non lancia un’eccezione di default, quindi la classe lo intercetta manualmente e costruisce un messaggio di errore informativo includendo status code e corpo della risposta. In tutti e tre i casi, l’errore non viene mai ignorato: ogni Promise ha il suo reject con un messaggio che identifica chiaramente quale asset ha fallito e perché. Questo è prezioso in fase di debug quando il manifest contiene decine di file.
📁Caricamento aggregato: loadManifest vs loadAssets
La classe offre due strategie di caricamento in blocco, pensate per stili di organizzazione diversi:
📁loadManifest(manifest) accetta un array “piatto” dove ogni elemento dichiara il proprio tipo:
📁loadAssets(assetList) accetta invece un oggetto già suddiviso per categoria:
📁Promise.all()
Entrambi i metodi usano internamente Promise.all(), quindi tutti gli asset vengono caricati in parallelo, non in sequenza — questo è cruciale per le performance: con 50 sprite da caricare, aspettare che finiscano uno alla volta sarebbe enormemente più lento che lasciare che il browser scarichi tutto contemporaneamente (entro i limiti di connessioni parallele del browser stesso). Va notato un comportamento implicito di Promise.all(): se anche un solo asset fallisce, l’intera operazione di caricamento viene rigettata immediatamente (fail-fast), anche se gli altri 49 asset sono andati a buon fine. È una scelta ragionevole per un motore di gioco — è meglio bloccare l’avvio con un errore chiaro che partire con uno sprite mancante che causerà un crash più avanti nel gameplay.
📁Accesso agli asset
I metodi getter (getImage, getSound, getJSON) sono semplici wrapper su Map.get(). Il metodo getAsset(name) è un fallback comodo che cerca il nome in tutte e tre le mappe in sequenza — utile quando il chiamante non sa (o non gli importa) di che tipo sia l’asset richiesto, ma attenzione: se esistono un’immagine e un suono con lo stesso nome logico, getAsset restituirà sempre l’immagine per prima, silenziosamente. È un dettaglio da tenere a mente per evitare bug difficili da individuare.
📁Riproduzione audio: il trucco del cloneNode()
📁Audio
Questo è il dettaglio più importante da capire per chi integra la classe. Un singolo oggetto Audio non può riprodurre sé stesso più volte in sovrapposizione: se chiami .play() due volte di seguito sullo stesso oggetto, la seconda chiamata interrompe (o riavvia) la prima. Per un effetto come “colpo” o “moneta raccolta” che può scattare più volte in rapida successione, questo produrrebbe un audio spezzato o mancante.
📁cloneNode()
La soluzione è clonare l’elemento Audio ad ogni riproduzione con cloneNode(): il clone eredita il src già decodificato dal browser, quindi non ricarica nulla da rete, ma è un’istanza indipendente che può suonare in overlap con le altre. Il prezzo pagato è la gestione della memoria: ogni clone non viene mai esplicitamente rimosso, ma viene lasciato al garbage collector del browser una volta terminata la riproduzione e non più referenziato — comportamento accettabile per effetti brevi, ma da tenere d’occhio se playSound viene chiamato con frequenza molto alta (es. decine di volte al secondo).
📁Esempio pratico
📁Perché è scritto così — note per chi integra la classe
Un’unica istanza condivisa. BeeAssetManager va istanziato una sola volta all’avvio e passato (o reso accessibile) a tutte le entità del gioco che devono leggere asset — scene, sprite, sistema audio. Istanziarlo più volte spreca memoria e ricarica gli stessi file inutilmente.
📁try/catch
try/catch obbligatorio attorno al caricamento. Dato che sia loadManifest che loadAssets possono rigettare, un caricamento fallito senza gestione dell’errore produce una Promise non gestita (unhandled rejection) e il gioco resta bloccato su una schermata bianca senza spiegazioni per l’utente.
📁I nomi logici
I nomi logici vanno scelti con cura. Convenzioni tipo prefissi (sfx_salto, img_player) aiutano a evitare le collisioni silenziose descritte sopra per getAsset, soprattutto quando il numero di asset cresce.
📁Il manifest:
Il manifest può essere generato dinamicamente. Non è vincolato a un array scritto a mano: può essere costruito da un JSON esterno di configurazione livello, permettendo di caricare solo gli asset del livello corrente invece di tutto il gioco in un colpo solo — utile per portali come Poki o GamePix dove il peso iniziale del bundle è un fattore di valutazione editoriale.
📁Riepilogo API
| Metodo | Ritorna | Scopo |
|---|---|---|
loadManifest(manifest) |
Promise<void> |
Carica un array misto di asset tipizzati |
loadAssets(assetList) |
Promise<void> |
Carica un oggetto asset suddiviso per categoria |
loadImage(name, src) |
Promise<Image> |
Carica una singola immagine |
loadSound(name, src) |
Promise<Audio> |
Carica una singola traccia audio |
loadJSON(name, src) |
Promise<Object> |
Carica e fa il parsing di un file JSON |
getImage(name) |
Image | undefined |
Recupera un’immagine già caricata |
getSound(name) |
Audio | undefined |
Recupera un audio già caricato |
getJSON(name) |
Object | undefined |
Recupera un JSON già caricato |
getAsset(name) |
any | undefined |
Cerca il nome in immagini → suoni → JSON, in ordine |
playSound(name, volume) |
void |
Riproduce un suono clonando il nodo per permettere overlap |
