Gestione semplificata degli asset per i giochi HTML5

HTML5 ha fornito molte API utili per creare applicazioni web moderne, adattabili e potenti nel browser. Ottimo, ma vuoi davvero creare e giocare. Fortunatamente, HTML5 ha anche inaugurato una nuova era di sviluppo di giochi che utilizza API come Canvas e potenti motori JavaScript per offrire giochi direttamente nel browser senza bisogno di plug-in.

Questo articolo illustra la procedura per creare un semplice componente di gestione degli asset per il tuo gioco HTML5. Senza un gestore degli asset, il tuo gioco avrà difficoltà a compensare i tempi di download sconosciuti e il caricamento asincrono delle immagini. Continua a leggere per vedere un esempio di un semplice gestore di asset per i tuoi giochi HTML5.

Il problema

I giochi HTML5 non possono presupporre che i relativi asset, come immagini o audio, siano sul computer locale del giocatore, in quanto presuppongono di essere riprodotti in un browser web con asset scaricati tramite HTTP. Poiché è coinvolta la rete, il browser non sa con certezza quando gli asset per il gioco verranno scaricati e saranno disponibili.

Il modo di base per caricare un'immagine in modo programmatico in un browser web è il seguente codice:

var image = new Image();
image.addEventListener("success", function(e) {
  // do stuff with the image
});
image.src = "/some/image.png";

Ora immagina di avere cento immagini che devono essere caricate e visualizzate all'avvio del gioco. Come faccio a sapere quando tutte le 100 immagini sono pronte? Sono stati caricati tutti correttamente? Quando dovrebbe iniziare la partita?

La soluzione

Lascia che sia un asset manager a gestire la coda delle risorse e a comunicare al gioco che è tutto pronto. Un asset manager generalizza la logica di caricamento delle risorse sulla rete e offre un modo semplice per verificarne lo stato.

Il nostro semplice gestore delle risorse presenta i seguenti requisiti:

  • mettere in coda i download
  • avviare i download
  • Monitora i risultati positivi e negativi
  • segnala quando è tutto pronto
  • facile recupero delle risorse

Inserimento in coda

Il primo requisito è mettere in coda i download. Questo design ti consente di dichiarare gli asset di cui hai bisogno senza scaricarli effettivamente. Questo può essere utile se, ad esempio, vuoi dichiarare tutti gli asset per un livello di gioco in un file di configurazione.

Il codice per il costruttore e la coda ha il seguente aspetto:

function AssetManager() {
  this.downloadQueue = [];
}

AssetManager.prototype.queueDownload = function(path) {
    this.downloadQueue.push(path);
}

Avvia download

Dopo aver messo in coda tutti gli asset da scaricare, puoi chiedere all'asset manager di avviare il download di tutti gli asset.

Fortunatamente, il browser web può eseguire in parallelo i download, in genere fino a 4 connessioni per host. Un modo per velocizzare il download degli asset è utilizzare una serie di nomi di dominio per l'hosting degli asset. Ad esempio, anziché pubblicare tutto da assets.example.com, prova a utilizzare assets1.example.com, assets2.example.com, assets3.example.com e così via. Anche se ciascuno di questi nomi di dominio è semplicemente un CNAME per lo stesso server web, il browser web li vede come server separati e aumenta il numero di connessioni utilizzate per il download delle risorse. Scopri di più su questa tecnica nella sezione Suddividi i componenti in più domini delle Best practice per velocizzare il sito web.

Il nostro metodo di inizializzazione del download si chiama downloadAll(). Lo amplieremo nel tempo. Per il momento, ecco la prima logica per avviare i download.

AssetManager.prototype.downloadAll = function() {
    for (var i = 0; i < this.downloadQueue.length; i++) {
        var path = this.downloadQueue[i];
        var img = new Image();
        var that = this;
        img.addEventListener("load", function() {
            // coming soon
        }, false);
        img.src = path;
    }
}

Come puoi vedere nel codice riportato sopra, downloadAll() esegue semplicemente un'iterazione della coda di download e crea un nuovo oggetto Image. Viene aggiunto un listener di eventi per l'evento load e viene impostato l'attributo src dell'immagine, che attiva il download effettivo.

Con questo metodo puoi avviare i download.

Monitoraggio di successo e fallimento

Un altro requisito è monitorare sia i risultati positivi che quelli negativi, perché purtroppo non tutto funziona sempre perfettamente. Finora il codice monitora solo gli asset scaricati correttamente. Aggiungendo un gestore di eventi per l'evento di errore, potrai acquisire gli scenari di successo e di errore.

AssetManager.prototype.downloadAll = function(downloadCallback) {
  for (var i = 0; i < this.downloadQueue.length; i++) {
    var path = this.downloadQueue[i];
    var img = new Image();
    var that = this;
    img.addEventListener("load", function() {
        // coming soon
    }, false);
    img.addEventListener("error", function() {
        // coming soon
    }, false);
    img.src = path;
  }
}

Il nostro asset manager deve sapere quanti successi e fallimenti abbiamo riscontrato, altrimenti non saprà mai quando può iniziare il gioco.

Innanzitutto, aggiungeremo i contatori all'oggetto nel costruttore, che ora ha il seguente aspetto:

function AssetManager() {
<span class="highlight">    this.successCount = 0;
    this.errorCount = 0;</span>
    this.downloadQueue = [];
}

A questo punto, incrementa i contatori nei listener di eventi, che ora hanno il seguente aspetto:

img.addEventListener("load", function() {
    <span class="highlight">that.successCount += 1;</span>
}, false);
img.addEventListener("error", function() {
    <span class="highlight">that.errorCount += 1;</span>
}, false);

Il gestore degli asset ora monitora sia gli asset caricati correttamente sia quelli con errori.

Segnale quando è tutto pronto

Dopo che il gioco ha messo in coda le risorse per il download e ha chiesto all'asset manager di scaricarle tutte, è necessario comunicare al gioco quando tutte le risorse sono state scaricate. Invece che chiedere più volte se le risorse sono state scaricate, il gestore delle risorse può inviare un segnale al gioco.

L'asset manager deve prima sapere quando ogni risorsa è stata completata. Aggiungeremo un metodo isDone:

AssetManager.prototype.isDone = function() {
    return (this.downloadQueue.length == this.successCount + this.errorCount);
}

Confrontando successCount + errorCount con le dimensioni della coda di download, il gestore delle risorse sa se ogni risorsa è stata completata correttamente o se si è verificato un qualche tipo di errore.

Ovviamente, sapere se è stato completato è solo la metà del lavoro; anche il gestore degli asset deve controllare questo metodo. Aggiungeremo questo controllo in entrambi i gestori eventi, come mostrato nel codice seguente:

img.addEventListener("load", function() {
    console.log(this.src + ' is loaded');
    that.successCount += 1;
    if (that.isDone()) {
        // ???
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
if (that.isDone()) {
        // ???
    }
}, false);

Dopo l'incremento dei contatori, vedremo se si tratta dell'ultima risorsa nella coda. Se il gestore delle risorse ha effettivamente completato il download, cosa dobbiamo fare esattamente?

Se il gestore delle risorse ha completato il download di tutti gli asset, chiameremo un metodo di callback, ovviamente. Modifichiamo downloadAll() e aggiungiamo un parametro per il callback:

AssetManager.prototype.downloadAll = function(downloadCallback) {
    ...

Chiameremo il metodo downloadCallback all'interno dei nostri listener di eventi:

img.addEventListener("load", function() {
    that.successCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);

L'asset manager è finalmente pronto per l'ultimo requisito.

Recupero facile degli asset

Una volta che il gioco ha ricevuto l'indicazione che può iniziare, inizierà a eseguire il rendering delle immagini. L'asset manager non è responsabile solo del download e del monitoraggio delle risorse, ma anche del loro inserimento nel gioco.

Il nostro ultimo requisito implica una sorta di metodo getAsset, quindi lo aggiungeremo ora:

AssetManager.prototype.getAsset = function(path) {
    return this.cache[path];
}

Questo oggetto cache viene inizializzato nel costruttore, che ora ha il seguente aspetto:

function AssetManager() {
    this.successCount = 0;
    this.errorCount = 0;
    this.cache = {};
    this.downloadQueue = [];
}

La cache viene compilata alla fine di downloadAll(), come mostrato di seguito:

AssetManager.prototype.downloadAll = function(downloadCallback) {
  ...
      img.addEventListener("error", function() {
          that.errorCount += 1;
          if (that.isDone()) {
              downloadCallback();
          }
      }, false);
      img.src = path;
      <span class="highlight">this.cache[path] = img;</span>
  }
}

Bonus: correzione di bug

Hai notato il bug? Come scritto sopra, il metodo isDone viene chiamato solo quando vengono attivati eventi di caricamento o errore. Ma cosa succede se l'asset manager non ha risorse in coda per il download? Il metodo isDone non viene mai attivato e il gioco non si avvia mai.

Puoi gestire questo scenario aggiungendo il seguente codice a downloadAll():

AssetManager.prototype.downloadAll = function(downloadCallback) {
    if (this.downloadQueue.length === 0) {
      downloadCallback();
  }
 ...

Se non sono presenti asset in coda, il callback viene chiamato immediatamente. Bug risolto

Esempio di utilizzo

L'utilizzo di questo gestore di asset nel tuo gioco HTML5 è abbastanza semplice. Ecco il modo più semplice per utilizzare la libreria:

var ASSET_MANAGER = new AssetManager();

ASSET_MANAGER.queueDownload('img/earth.png');

ASSET_MANAGER.downloadAll(function() {
    var sprite = ASSET_MANAGER.getAsset('img/earth.png');
    ctx.drawImage(sprite, x - sprite.width/2, y - sprite.height/2);
});

Il codice riportato sopra illustra:

  1. Crea un nuovo gestore di asset
  2. Mettere in coda gli asset da scaricare
  3. Avvia i download con downloadAll()
  4. Indica quando gli asset sono pronti chiamando la funzione di callback
  5. Recupera gli asset con getAsset()

Aree di miglioramento

Senza dubbio, questo semplice asset manager non sarà più sufficiente man mano che sviluppi il tuo gioco, ma spero che ti abbia fornito un punto di partenza di base. Le funzionalità future potrebbero includere:

  • segnalando quale asset ha generato un errore
  • Callback per indicare l'avanzamento
  • recupero di asset dall'API File System

Pubblica miglioramenti, forking e link al codice nei commenti di seguito.

Full Source

Il codice sorgente di questo gestore di asset e del gioco da cui è stato estratto è open source ai sensi della Licenza Apache e può essere trovato nell'account GitHub di Bad Aliens. Puoi giocare al gioco Bad Aliens nel tuo browser compatibile con HTML5. Questo gioco è stato l'argomento del mio talk di Google IO intitolato Super Browser 2 Turbo HD Remix: Introduzione allo sviluppo di giochi HTML5 (slide, video).

Riepilogo

La maggior parte dei giochi ha una sorta di gestore di asset, ma i giochi HTML5 richiedono un gestore di asset che carichi gli asset su una rete e gestisca gli errori. Questo articolo ha descritto un semplice gestore degli asset che dovrebbe essere facile da utilizzare e adattare per il tuo prossimo gioco HTML5. Buon divertimento e facci sapere cosa ne pensi nei commenti qui sotto. Grazie.