BeeEngine JAVASCRIPT 2D ENGINE
Documentazione – BeeEngine
v2.5.0

BeeAssetManager — Documentazione Tecnica

Modulo core di BeeEngine per il caricamento centralizzato delle risorse di gioco

BeeEngine Mascot

📁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

this.images = new Map(); this.sounds = new Map(); this.jsons = new Map();

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

[ { type: ‘image’, name: ‘player’, src: ‘assets/player.png’ }, { type: ‘audio’, name: ‘jump’, src: ‘assets/jump.mp3’ } ]

📁loadAssets(assetList) accetta invece un oggetto già suddiviso per categoria:

{ images: [{ name: ‘player’, src: ‘assets/player.png’ }], sounds: [{ name: ‘jump’, src: ‘assets/jump.mp3’ }] }

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

playSound(name, volume = 1) { const sound = this.sounds.get(name); if (!sound) return; const clone = sound.cloneNode(); clone.volume = volume; clone.play(); }

📁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

import { BeeAssetManager } from ‘./core/BeeAssetManager.js’; // 1. Creiamo l’istanza (di solito una sola, condivisa da tutto il gioco) const assetManager = new BeeAssetManager(); // 2. Definiamo il manifest: cosa serve prima che il gioco parta const manifest = [ { type: ‘image’, name: ‘player’, src: ‘assets/img/player.png’ }, { type: ‘image’, name: ‘nemico’, src: ‘assets/img/nemico.png’ }, { type: ‘audio’, name: ‘salto’, src: ‘assets/audio/salto.mp3’ }, { type: ‘audio’, name: ‘moneta’, src: ‘assets/audio/moneta.mp3’ }, { type: ‘json’, name: ‘livello1’, src: ‘assets/data/livello1.json’ } ]; async function avviaGioco() { try { // 3. Mostriamo una schermata di caricamento mentre attendiamo console.log(‘Caricamento risorse in corso…’); await assetManager.loadManifest(manifest); // 4. Da qui in poi tutti gli asset sono garantiti disponibili in memoria const spritePlayer = assetManager.getImage(‘player’); const datiLivello = assetManager.getJSON(‘livello1’); console.log(‘Livello caricato:’, datiLivello); // 5. Disegniamo lo sprite su un canvas, ad esempio const ctx = document.querySelector(‘canvas’).getContext(‘2d’); ctx.drawImage(spritePlayer, 100, 100); // 6. Riproduciamo un suono in risposta a un evento di gioco document.addEventListener(‘keydown’, (e) => { if (e.code === ‘Space’) { assetManager.playSound(‘salto’, 0.8); // volume all’80% } }); } catch (errore) { // Se anche un solo asset fallisce, finiamo qui console.error(‘Impossibile avviare il gioco:’, errore.message); document.body.innerHTML = ‘<p>Errore nel caricamento delle risorse.</p>’; } } avviaGioco();

📁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