Einfache Asset-Verwaltung für HTML5-Spiele

Einleitung

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

In diesem Artikel erfahren Sie, wie Sie eine einfache Asset-Verwaltungskomponente für Ihr HTML5-Spiel erstellen. Ohne Asset-Manager wird es in deinem Spiel schwierig, unbekannte Downloadzeiten und asynchrones Laden von Bildern zu kompensieren. Unten 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 Assets wie Bilder oder Audio auf dem lokalen Computer des Players verfügbar sind, da bei HTML5-Spielen vorausgesetzt wird, dass sie in einem Webbrowser gespielt werden und die Assets über HTTP heruntergeladen werden. Da das Netzwerk beteiligt ist, weiß der Browser nicht, wann die Assets für das Spiel heruntergeladen und verfügbar sind.

Mit folgendem Code können Sie ein Bild in einem Webbrowser im Wesentlichen programmatisch laden:

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

Stellen Sie sich nun hundert Bilder vor, 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 soll das Spiel tatsächlich beginnen?

Die Lösung

Ein Asset-Manager kann die Assets in die Warteschlange stellen und dem Spiel einen Bericht erstatten, wenn alles bereit ist. Ein Asset-Manager verallgemeinert die Logik für das Laden von Assets über das Netzwerk und bietet eine einfache Möglichkeit, den Status zu überprüfen.

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

  • Downloads zur Wiedergabeliste hinzufügen
  • Downloads starten
  • Erfolg und Misserfolg nachverfolgen
  • signalisieren, wenn alles erledigt ist
  • Assets einfach abrufen

In die Warteschlange gestellt

Die erste Anforderung besteht darin, Downloads in die Warteschlange zu stellen. Mit diesem Design können Sie die benötigten Assets deklarieren, ohne sie tatsächlich herunterzuladen. Das kann beispielsweise nützlich sein, wenn Sie alle Assets für ein Spielelevel 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 du alle zum Herunterladen erforderlichen Assets in die Warteschlange gestellt hast, kannst du den Asset-Manager bitten, mit dem Download aller Assets zu beginnen.

Der Webbrowser kann die Downloads zum Glück parallelisieren – in der Regel bis zu vier Verbindungen pro Host. Eine Möglichkeit, den Download von Assets zu beschleunigen, besteht darin, eine Reihe von Domainnamen für das Hosting von Assets zu verwenden. Anstatt alle Inhalte von „assets.beispiel.de“ auszuliefern, können Sie beispielsweise „Assets1.beispiel.de“, „Assets2.beispiel.de“, „Assets3.beispiel.de“ usw. verwenden. Selbst wenn jeder dieser Domainnamen einfach ein CNAME für denselben Webserver ist, sieht der Webbrowser sie als separate Server und erhöht die Anzahl der Verbindungen, die für den Download von Assets verwendet werden. Weitere Informationen zu dieser Technik finden Sie im Artikel Komponenten auf Domains aufteilen in den Best Practices zur Beschleunigung Ihrer Website.

Unsere Methode zur Initialisierung des Downloads heißt downloadAll(). Wir werden im Laufe der Zeit daran arbeiten. Hier ist vorerst die erste Logik, nur 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 Sie im obigen Code sehen können, durchläuft downloadAll() einfach die DownloadQueue und erstellt ein neues Image-Objekt. Ein Event-Listener für das Ereignis "load" wird hinzugefügt und der "src" des Bildes festgelegt, der den eigentlichen Download auslöst.

Mit dieser Methode können Sie die Downloads starten.

Erfolg und Misserfolg verfolgen

Eine weitere Anforderung besteht darin, sowohl Erfolg als auch Misserfolg zu verfolgen, da leider nicht immer alles perfekt funktioniert. Mit dem Code werden bisher nur erfolgreich heruntergeladene Assets erfasst. Durch Hinzufügen eines Event-Listeners für das Fehlerereignis 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 erzielt haben, sonst weiß er nie, wann das Spiel beginnen kann.

Zuerst fügen wir die Zähler dem Objekt im Konstruktor hinzu, das nun wie folgt aussieht:

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 Event-Listenern, die jetzt wie folgt aussehen:

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

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

Signalisierung nach Abschluss

Nachdem das Spiel die Assets zum Download in die Warteschlange gestellt und den Asset-Manager gebeten hat, alle Assets herunterzuladen, muss dem Spiel mitgeteilt werden, dass alle Assets heruntergeladen wurden. Anstatt dass das Spiel immer wieder fragt, ob die Assets heruntergeladen werden, kann der Asset-Manager dem Spiel ein Signal signalisieren.

Der Asset-Manager muss zuerst wissen, wann ein Asset fertiggestellt ist. Wir fügen jetzt eine isDone-Methode hinzu:

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

Durch den Vergleich von successCount + errorCount mit der Größe der DownloadQueue kann der Asset-Manager ermitteln, ob die einzelnen Assets erfolgreich abgeschlossen wurden oder ein Fehler aufgetreten ist.

Zu wissen, ob es erledigt ist, ist natürlich nur die halbe Miete. Auch der Vermögensmanager muss diese Methode überprüfen. Wir werden diese Prüfung in unsere beiden Event-Handler einfügen, wie der folgende Code 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 inkrementiert wurden, sehen wir, ob dies das letzte Asset in unserer Warteschlange war. Was sollen wir tun, wenn der Asset-Manager den Download tatsächlich abgeschlossen hat?

Wenn der Asset-Manager alle Assets heruntergeladen hat, rufen wir natürlich eine Callback-Methode auf. Ändern wir also downloadAll() und fügen wir einen Parameter für den Callback hinzu:

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

Wir rufen die Methode downloadCallback innerhalb unserer Ereignis-Listener 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 Vermögensmanager kann nun die letzte Anforderung erfüllen.

Einfaches Abrufen von Assets

Sobald das Spiel gestartet wurde, werden Bilder gerendert. Der Asset-Manager ist nicht nur für den Download und das Tracking der Assets verantwortlich, sondern auch für die Bereitstellung dieser Assets im Spiel.

Unsere letzte Anforderung beinhaltet eine Art „getAsset“-Methode, die wir jetzt hinzufügen:

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

Dieses Cache-Objekt wird im Konstruktor initialisiert, was jetzt wie folgt aussieht:

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

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

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

Hast du den Programmfehler gefunden? Wie oben beschrieben, wird die isDone-Methode nur aufgerufen, wenn Lade- oder Fehlerereignisse ausgelöst werden. Aber was ist, wenn der Asset-Manager keine Assets zum Download in der Warteschlange hat? Die isDone-Methode wird nie ausgelöst und das Spiel wird nicht gestartet.

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

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

Befinden sich keine Assets in der Warteschlange, wird der Callback sofort aufgerufen. Fehler behoben

Verwendungsbeispiel

Die Verwendung dieses Asset-Managers in Ihrem HTML5-Spiel ist ganz 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 illustriert:

  1. Erstellt einen neuen Asset-Manager
  2. Assets zum Herunterladen in die Warteschlange stellen
  3. Downloads mit downloadAll() starten
  4. Durch Aufrufen der Callback-Funktion signalisieren, dass die Assets bereit sind
  5. Assets mit getAsset() abrufen

Verbesserungsmöglichkeiten

Du wirst bei der Entwicklung deines Spiels zweifellos aus diesem einfachen Asset-Manager entwachsen. Ich hoffe, dass er dir den Einstieg erleichtert hat. Zu den zukünftigen Funktionen gehören:

  • Sie signalisieren, bei welchem Asset ein Fehler aufgetreten ist.
  • Callbacks zur Anzeige des Fortschritts
  • Assets aus der File System API abrufen

Bitte poste Verbesserungen, Forks und Links zum Code in den Kommentaren unten.

Vollständige Quelle

Die Quelle für diesen Asset-Manager und das Spiel, von dem er abstrahiert ist, ist Open Source unter der Apache-Lizenz und befindet sich im Bad Aliens GitHub-Konto. Sie können das Spiel Bad Aliens in Ihrem HTML5-kompatiblen Browser spielen. Dieses Spiel war das Thema meines Google-I/O-Vortrags mit dem Titel „Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development“ (Folien, Video).

Zusammenfassung

Die meisten Spiele haben eine Art Asset-Manager. Für HTML5-Spiele ist jedoch ein Asset-Manager erforderlich. Dieser lädt Assets über ein Netzwerk und behebt Fehler. In diesem Artikel wird ein einfacher Asset-Manager vorgestellt, der sich leicht für Ihr nächstes HTML5-Spiel einsetzen und anpassen lässt. Viel Spaß! In den Kommentaren unten könnt ihr uns gern Feedback geben. Vielen Dank!