Einfache Asset-Verwaltung für HTML5-Spiele

Einführung

HTML5 bietet viele nützliche APIs zum Erstellen moderner, responsiver und leistungsstarker Webanwendungen im Browser. Das ist toll, aber Sie möchten wirklich Spiele entwickeln und spielen. Glücklicherweise hat HTML5 auch eine neue Ära der Spieleentwicklung eingeläutet, bei der APIs wie Canvas und leistungsstarke JavaScript-Engines verwendet werden, um Spiele direkt in Ihrem Browser ohne Plug-ins bereitzustellen.

In diesem Artikel wird beschrieben, wie Sie eine einfache Asset-Management-Komponente für Ihr HTML5-Spiel erstellen. Ohne einen Asset-Manager ist es schwierig, in Ihrem Spiel unbekannte Downloadzeiten und asynchrones Bildladen auszugleichen. Im Folgenden sehen Sie ein Beispiel für einen einfachen Asset-Manager für Ihre HTML5-Spiele.

Das Problem

Bei HTML5-Spielen kann nicht davon ausgegangen werden, dass sich ihre Assets wie Bilder oder Audioinhalte auf dem lokalen Computer des Spielers befinden, da HTML5-Spiele in einem Webbrowser gespielt werden, wobei Assets über HTTP heruntergeladen werden. Da das Netzwerk beteiligt ist, kann der Browser nicht genau sagen, wann die Assets für das Spiel heruntergeladen und verfügbar sein werden.

Die grundlegende Methode zum programmatischen Laden eines Bildes in einem Webbrowser ist der folgende Code:

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

Stellen Sie sich nun vor, Sie haben hunderte von Bildern, die beim Start des Spiels geladen und angezeigt werden müssen. Woher wissen Sie, wann alle 100 Bilder bereit sind? Wurden alle erfolgreich geladen? Wann sollte das Spiel eigentlich beginnen?

Die Lösung

Überlassen Sie die Asset-Warteschlange einem Asset-Manager und melden Sie sich im Spiel, wenn alles bereit ist. Ein Asset-Manager generalisiert die Logik zum Laden von Assets über das Netzwerk und bietet eine einfache Möglichkeit, den Status zu prüfen.

Für unseren einfachen Asset-Manager gelten die folgenden Anforderungen:

  • Downloads in die Warteschlange stellen
  • Downloads starten
  • Erfolg und Misserfolg verfolgen
  • Signalisiert, wenn alles erledigt ist
  • einfacher Abruf von Assets

In die Warteschlange gestellt

Als Erstes müssen Sie Downloads in die Warteschlange stellen. Mit diesem Design können Sie die benötigten Assets deklarieren, ohne sie tatsächlich herunterzuladen. Das kann nützlich sein, wenn Sie beispielsweise alle Assets für ein Spiellevel in einer Konfigurationsdatei deklarieren möchten.

Der Code für den Konstruktor und die Warteschlange sieht so aus:

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

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

Downloads starten

Nachdem Sie alle Assets zur Download-Warteschlange hinzugefügt haben, können Sie den Asset-Manager bitten, mit dem Download zu beginnen.

Glücklicherweise kann der Webbrowser die Downloads parallelisieren – in der Regel bis zu vier Verbindungen pro Host. Eine Möglichkeit, das Herunterladen von Assets zu beschleunigen, besteht darin, mehrere Domainnamen für das Asset-Hosting zu verwenden. Anstatt alle Assets von assets.beispiel.de bereitzustellen, können Sie beispielsweise assets1.beispiel.de, assets2.beispiel.de, assets3.beispiel.de usw. verwenden. Auch wenn jeder dieser Domainnamen nur ein CNAME für denselben Webserver ist, werden sie vom Webbrowser als separate Server erkannt und die Anzahl der Verbindungen für das Asset-Herunterladen erhöht. Weitere Informationen zu dieser Technik finden Sie unter Komponenten auf mehrere Domains verteilen in den Best Practices für die Beschleunigung Ihrer Website.

Unsere Methode zur Downloadinitialisierung heißt downloadAll(). Wir werden das Angebot im Laufe der Zeit ausbauen. Hier ist die erste Logik, um die Downloads zu starten.

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;
    }
}

Wie im Code oben zu sehen, iteriert downloadAll() einfach durch die downloadQueue und erstellt ein neues Image-Objekt. Es wird ein Ereignis-Listener für das Ladeereignis hinzugefügt und die „src“ des Bildes wird festgelegt, wodurch der eigentliche Download ausgelöst wird.

Mit dieser Methode können Sie die Downloads starten.

Erfolg und Fehler erfassen

Außerdem sollten Sie sowohl Erfolge als auch Misserfolge im Blick behalten, da leider nicht immer alles reibungslos funktioniert. Der Code erfasst bisher nur erfolgreich heruntergeladene Assets. Wenn Sie einen Event-Listener für das Fehlerereignis hinzufügen, können Sie sowohl Erfolgs- als auch Fehlerszenarien erfassen.

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;
  }
}

Unser Asset-Manager muss wissen, wie viele Erfolge und Misserfolge wir hatten, sonst weiß er nie, wann das Spiel beginnen kann.

Zuerst fügen wir dem Objekt im Konstruktor die Zähler hinzu. Es sieht jetzt so aus:

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

Erhöhen Sie als Nächstes die Zähler in den Ereignis-Listenern. Sie sehen jetzt so aus:

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

Der Asset-Manager erfasst jetzt sowohl erfolgreich geladene als auch fehlgeschlagene Assets.

Signalisierung bei Fertigstellung

Nachdem das Spiel seine Assets zum Download in die Warteschlange gestellt und den Asset-Manager gebeten hat, alle Assets herunterzuladen, muss das Spiel darüber informiert werden, wenn alle Assets heruntergeladen wurden. Anstatt dass das Spiel immer wieder fragt, ob die Assets heruntergeladen wurden, kann der Asset-Manager dem Spiel eine entsprechende Rückmeldung geben.

Der Asset-Manager muss zuerst wissen, wann jedes Asset fertig ist. Fügen wir jetzt die Methode „isDone“ hinzu:

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

Durch den Vergleich von „successCount“ und „errorCount“ mit der Größe der DownloadQueue weiß der Asset-Manager, ob alle Assets erfolgreich abgeschlossen wurden oder es zu einem Fehler gekommen ist.

Natürlich ist es nur die halbe Miete, zu wissen, ob die Arbeit erledigt ist. Der Asset Manager muss diese Methode auch prüfen. Wir fügen diese Prüfung in beide Ereignis-Handler ein, wie der Code unten zeigt:

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);

Nachdem die Zähler erhöht wurden, prüfen wir, ob das das letzte Asset in der Warteschlange war. Was sollten wir tun, wenn der Asset-Manager tatsächlich fertig mit dem Herunterladen ist?

Wenn der Asset-Manager alle Assets heruntergeladen hat, wird natürlich eine Rückrufmethode aufgerufen. Ändern wir downloadAll() und fügen einen Parameter für den Rückruf hinzu:

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

Wir rufen die Methode „downloadCallback“ in unseren Ereignis-Listenern auf:

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

Der Asset Manager ist endlich bereit für die letzte Anforderung.

Einfaches Abrufen von Assets

Sobald das Spiel gestartet werden kann, beginnt es mit dem Rendern von Bildern. Der Asset-Manager ist nicht nur für das Herunterladen und Nachverfolgen der Assets, sondern auch für die Bereitstellung der Assets für das Spiel verantwortlich.

Unsere letzte Anforderung impliziert eine Art getAsset-Methode. Fügen wir sie also jetzt hinzu:

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

Dieses Cache-Objekt wird im Konstruktor initialisiert, der jetzt so aussieht:

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

Der Cache wird am Ende von downloadAll() gefüllt, wie unten dargestellt:

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

Haben Sie den Fehler gefunden? Wie oben erwähnt, wird die Methode „isDone“ nur aufgerufen, wenn entweder Lade- oder Fehlerereignisse ausgelöst werden. Was ist aber, wenn der Asset-Manager keine Assets zum Download in der Warteschlange hat? Die Methode „isDone“ wird nie ausgelöst und das Spiel wird nie gestartet.

Sie können dieses Szenario berücksichtigen, indem Sie downloadAll() den folgenden Code hinzufügen:

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

Wenn keine Assets in der Warteschlange sind, wird der Callback sofort aufgerufen. Fehler behoben

Verwendungsbeispiel

Die Verwendung dieses Asset-Managers in Ihrem HTML5-Spiel ist recht einfach. So verwenden Sie die Bibliothek am einfachsten:

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);
});

Der Code oben veranschaulicht:

  1. Erstellt einen neuen Asset-Manager
  2. Assets für den Download in die Warteschlange stellen
  3. Downloads mit downloadAll() starten
  4. Signalisieren, dass die Assets bereit sind, indem die Callback-Funktion aufgerufen wird
  5. Assets mit getAsset() abrufen

Verbesserungswürdige Bereiche

Dieser einfache Asset-Manager wird Ihnen beim Entwickeln Ihres Spiels mit Sicherheit bald nicht mehr ausreichen. Ich hoffe aber, dass er Ihnen einen guten Einstieg ermöglicht hat. Zu den zukünftigen Funktionen gehören:

  • angibt, bei welchem Asset ein Fehler aufgetreten ist
  • Callbacks, die den Fortschritt anzeigen
  • Assets aus der File System API abrufen

Verbesserungen, Forks und Links zum Code können Sie in den Kommentaren unten posten.

Vollständige Quelle

Die Quelle für diesen Asset-Manager und das Spiel, aus dem er abstrahiert wurde, ist Open Source unter der Apache-Lizenz und kann im GitHub-Konto von Bad Aliens gefunden werden. Das Spiel „Bad Aliens“ kann in einem HTML5-kompatiblen Browser gespielt werden. Dieses Spiel war das Thema meines Google IO-Vortrags mit dem Titel „Super Browser 2 Turbo HD Remix: Einführung in die HTML5-Spielentwicklung“ (Folien, Video).

Zusammenfassung

Die meisten Spiele haben eine Art Asset-Manager, aber HTML5-Spiele erfordern einen Asset-Manager, der Assets über ein Netzwerk lädt und Fehler behebt. In diesem Artikel wurde ein einfacher Asset-Manager beschrieben, der sich leicht verwenden und an Ihr nächstes HTML5-Spiel anpassen lässt. Viel Spaß damit und lasst uns gern in den Kommentaren unten wissen, was ihr davon haltet. Vielen Dank!