Gestione semplificata degli asset per i giochi HTML5

Introduzione

HTML5 ha fornito molte API utili per la creazione di applicazioni web moderne, reattive e potenti nel browser. È fantastico, ma vuoi davvero costruire e giocare! Fortunatamente, HTML5 ha anche inaugurato una nuova era dello sviluppo di giochi che utilizza API come Canvas e potenti motori JavaScript per distribuire i giochi direttamente sul tuo browser senza bisogno di plug-in.

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

Il problema

I giochi HTML5 non possono presupporre che i propri asset, come immagini o audio, siano sul computer locale del player, in quanto i giochi HTML5 implicano di essere riprodotti in un browser web con asset scaricati tramite HTTP. Poiché la rete è coinvolta, il browser non è sicuro quando le risorse per il gioco verranno scaricate e disponibili.

Il modo base per caricare in modo programmatico un'immagine 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 un centinaio di immagini che devono essere caricate e visualizzate all'avvio del gioco. Come fai a sapere quando sono pronte tutte le 100 immagini? Sono stati caricati tutti correttamente? Quando dovrebbe iniziare effettivamente il gioco?

La soluzione

Consenti a un gestore di risorse di gestire la coda delle risorse e di riferire al gioco quando è tutto pronto. Un asset manager generalizza la logica di caricamento degli asset sulla rete e fornisce un modo semplice per controllare lo stato.

Il nostro semplice strumento di gestione delle risorse ha i seguenti requisiti:

  • mettere in coda i download
  • avvia download
  • monitorare successi e insuccessi
  • un segnale quando tutto è pronto
  • facile recupero delle risorse

Inserimento in coda

Il primo requisito è mettere in coda i download. Questa struttura ti consente di dichiarare gli asset che ti servono senza scaricarli. Questo può essere utile se, ad esempio, vuoi dichiarare tutte le risorse per un livello di gioco in un file di configurazione.

Il codice per il costruttore e l'operazione di 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 al gestore degli asset di avviare il download di tutto.

Il browser web è in grado di caricare in contemporanea i download, fortunatamente, di solito 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, invece di pubblicare tutto da assets.example.com, prova a utilizzare assets1.example.com, assets2.example.com, asset3.example.com e così via. Anche se ognuno di questi nomi di dominio è semplicemente un CNAME per lo stesso server web, il browser web li considera server separati e aumenta il numero di connessioni utilizzate per il download degli asset. Per saperne di più su questa tecnica, consulta la sezione Dividi i componenti tra domini nella pagina Best practice per velocizzare il tuo sito web.

Il nostro metodo per l'inizializzazione dei download è chiamato downloadAll(). Creeremo nel tempo. Per il momento, questa è la prima logica per avviare solo 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 precedente, downloadAll() esegue semplicemente l'iterazione della downloadQueue e crea un nuovo oggetto Image. Viene aggiunto un listener di eventi per l'evento di caricamento e viene impostato il valore src dell'immagine, che attiva il download effettivo.

Con questo metodo puoi avviare i download.

Monitoraggio delle operazioni riuscite e non riuscite

Un altro requisito è monitorare sia il successo che gli errori, perché purtroppo non sempre tutto funziona alla perfezione. Finora il codice monitora solo gli asset scaricati correttamente. Aggiungendo un listener di eventi per l'evento di errore, sarai in grado di acquisire sia gli scenari di successo che quelli 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 insuccessi abbiamo incontrato, altrimenti non saprà mai quando il gioco può iniziare.

Per prima cosa, 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 = [];
}

Successivamente, 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 sta monitorando sia gli asset caricati correttamente sia quelli non riusciti.

Segnalazione al termine

Dopo che il gioco ha messo in coda le sue risorse per il download e ha chiesto al gestore delle risorse di scaricarle, il gioco deve ricevere l'avviso quando tutte le risorse sono state scaricate. Invece di chiedere più e più volte se le risorse vengono scaricate, il gestore delle risorse può segnalare di nuovo al gioco.

Il gestore degli asset deve sapere innanzitutto quando è stato completato ogni asset. Aggiungeremo ora un metodo isDone:

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

Confrontando successCount + errorCount con le dimensioni di downloadQueue, il gestore degli asset sa se ogni asset è stato completato correttamente o ha generato un errore.

Naturalmente, sapere se è possibile farlo è solo a metà della battaglia; anche il gestore delle risorse deve verificare questo metodo. Aggiungeremo questo controllo in entrambi i gestori di eventi, come mostra il codice riportato di seguito:

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 aver incrementato i contatori, vedremo se questo è stato l'ultimo asset nella coda. Se il download del gestore degli asset è stato effettivamente completato, che cosa dobbiamo fare esattamente?

Se il gestore degli asset ha finito di scaricare tutti gli asset, chiameremo ovviamente un metodo di callback. Modifichiamo downloadAll() e aggiungiamo un parametro per il callback:

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

Chiameremo il metodo downloadCallback nei 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);

Il gestore delle risorse è finalmente pronto per l'ultimo requisito.

Recupero facile dei beni

Una volta che il gioco può iniziare, inizierà a visualizzare le immagini. Il gestore delle risorse non è solo responsabile del download e del monitoraggio delle risorse, ma anche della loro fornitura al gioco.

Il nostro ultimo requisito implica un 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 individuato il bug? Come spiegato in precedenza, il metodo isDone viene chiamato solo quando vengono attivati eventi di caricamento o di errore. Che cosa succede se il gestore delle risorse non ha nessun asset in coda per il download? Il metodo isDone non viene mai attivato e il gioco non inizia mai.

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

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

Se non ci sono asset in coda, il callback viene chiamato immediatamente. Bug corretto.

Esempio di utilizzo

L'uso di questo strumento di gestione degli asset nel tuo gioco HTML5 è piuttosto 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 asset manager
  2. Mettere in coda gli asset da scaricare
  3. Avvia i download con downloadAll()
  4. Segnala quando gli asset sono pronti richiamando la funzione di callback
  5. Recupera gli asset con getAsset()

Aree da migliorare

Sicuramente supererai le dimensioni di questo semplice asset manager durante la creazione del gioco, anche se spero che ti abbia fornito una base di partenza. Le funzionalità future potrebbero includere:

  • segnalare quale asset presentava un errore
  • callback per indicare l'avanzamento
  • recupero di asset dall'API file system

Pubblica miglioramenti, fork e link al codice nei commenti qui sotto.

Fonte completa

La fonte di questo asset manager e del gioco da cui è stato astratti sono open source secondo la licenza Apache e sono disponibili nell'account GitHub di Bad Aliens. Puoi giocare al gioco Bad Aliens nel tuo browser compatibile con HTML5. Questo gioco è stato l'oggetto del mio discorso su Google IO intitolato Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development (diapositive, video).

Riepilogo

La maggior parte dei giochi ha una sorta di gestione delle risorse, ma i giochi HTML5 richiedono un gestore delle risorse che carichi gli asset su una rete e gestisca gli errori. Questo articolo illustra un semplice asset manager che dovrebbe essere facile da usare e adattare al tuo prossimo gioco HTML5. Divertiti e facci sapere cosa ne pensi nei commenti qui sotto. Grazie.