HTML5 ゲームのシンプルなアセット管理

はじめに

HTML5 には、ブラウザで最新のレスポンシブかつパワフルなウェブ アプリケーションを構築するための便利な API が数多く用意されています。素晴らしいですが、実際にゲームを作ってプレイしたいと思っています。幸いなことに、HTML5 はゲーム開発の新時代の先駆けでもあります。Canvas などの API や強力な JavaScript エンジンを使用して、プラグインを必要とせずにブラウザにゲームを直接配信できます。

この記事では、HTML5 ゲーム用にシンプルなアセット管理コンポーネントを作成する手順について説明します。アセット マネージャーがないと、不明なダウンロード時間や非同期の画像読み込みをゲームで埋めるのが難しくなります。ここでは、HTML5 ゲーム向けのシンプルなアセット マネージャーの例を紹介します。

問題

HTML5 ゲームは、HTTP 経由でアセットがダウンロードされ、ウェブブラウザでプレイされることを示唆するため、画像や音声などのアセットがプレーヤーのローカルマシン上にあると想定することはできません。ネットワークが関与しているため、ブラウザではゲームのアセットがダウンロードされて利用可能になるタイミングを把握できません。

プログラムで画像をウェブブラウザに読み込む基本的な方法は、次のコードです。

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

ゲームの開始時に 100 枚の画像を読み込んで表示する必要があるとします。100 個の画像がすべて揃ったことを確認するにはどうすればよいでしょうか。すべて正常に読み込まれたか?実際にゲームを開始するタイミングは?

ソリューション

アセット マネージャーはアセットのキューを処理し、すべての準備が整ったらゲームに報告します。アセット マネージャーは、ネットワーク経由でアセットを読み込むロジックを一般化し、簡単にステータスを確認できるようにします。

シンプルなアセット マネージャーには次の要件があります。

  • ダウンロードをキューに追加
  • ダウンロードを開始
  • 成功と失敗の追跡
  • すべて完了すると通知する
  • アセットを簡単に取得

待機中

1 つ目の要件は、ダウンロードをキューに入れることです。この設計では、実際にダウンロードせずに必要なアセットを宣言できます。これは、たとえば、ゲームレベルのすべてのアセットを構成ファイルで宣言する場合に便利です。

コンストラクタとキューイングのコードは次のようになります。

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

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

ダウンロードを開始する

ダウンロードするすべてのアセットをキューに追加したら、アセット マネージャーにすべてのダウンロードを開始するよう依頼できます。

ウェブブラウザはダウンロードを並列化できます。これは通常、ホストごとに最大 4 つの接続です。アセットのダウンロードを高速化する 1 つの方法は、アセットのホスティングに幅広いドメイン名を使用することです。たとえば、assets.example.com のすべてのコンテンツを配信するのではなく、assets1.example.com、assets2.example.com、assets3.example.com などを使用するようにすることもできます。これらのドメイン名はそれぞれ同じウェブサーバーへの CNAME になっている場合でも、ウェブブラウザはこれらのドメイン名を個別のサーバーとして認識し、アセットのダウンロードに使用する接続数を増やします。この手法について詳しくは、ウェブサイトを高速化するためのベスト プラクティスのドメイン間でコンポーネントを分割するをご覧ください。

ダウンロードの初期化メソッドは downloadAll() と呼ばれます。時間をかけて構築してまいります。ここでは、ダウンロードを開始する最初のロジックを示します。

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

上記のコードからわかるように、downloadAll() は単に downloadQueue を反復処理し、新しい Image オブジェクトを作成します。load イベントのイベント リスナーが追加され、画像の src が設定されます。これにより、実際のダウンロードがトリガーされます。

この方法でダウンロードを開始できます。

成功と失敗の追跡

もう 1 つの要件は、成功と失敗の両方を追跡することです。残念ながら、すべてが必ずしも完全にうまくいくとは限りません。これまでのコードでは、正常にダウンロードされたアセットのみをトラッキングしています。エラーイベント用のイベント リスナーを追加することで、成功と失敗の両方のシナリオをキャプチャできます。

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

アセット マネージャーは、これまで何回の成功と失敗を経験しているかを知る必要があります。そうしないと、ゲームをいつ開始できるかがわかりません。

まず、カウンタをコンストラクタ内のオブジェクトに追加します。次のようになります。

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

次に、イベント リスナーのカウンタをインクリメントします。次のようになります。

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

アセット マネージャーでは、読み込みに成功したアセットと失敗したアセットの両方がトラッキングされるようになりました。

完了時のシグナリング

ゲームがダウンロード用にアセットをキューに入れ、アセット マネージャーにすべてのアセットをダウンロードするように要求した後、すべてのアセットがダウンロードされたことをゲームに通知する必要があります。アセット マネージャーは、アセットがダウンロードされたかどうかをゲームが何度も要求するのではなく、ゲームにシグナルを返すことができます。

アセット マネージャーはまず、すべてのアセットがいつ終了したかを把握する必要があります。ここで isDone メソッドを追加します。

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

アセット マネージャーは、successCount + errorCount を downloadQueue のサイズと比較することで、すべてのアセットが正常に終了したか、なんらかのエラーが発生したかを把握します。

もちろん、完了したかどうかを確認するだけでは不十分です。アセット マネージャーもこの手法をチェックする必要があります。次のコードに示すように、このチェックを両方のイベント ハンドラ内に追加します。

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

カウンタが加算された後、それがキューの最後のアセットだったかどうかを確認します。アセット マネージャーのダウンロードが実際に完了した場合は、具体的に何をする必要がありますか?

アセット マネージャーがすべてのアセットのダウンロードを完了すると、コールバック メソッドが呼び出されます。downloadAll() を変更して、コールバックのパラメータを追加しましょう。

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

イベント リスナー内で downloadCallback メソッドを呼び出します。

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

アセット マネージャーで最後の要件に対応する準備が整いました。

アセットを簡単に取得

ゲームを開始できるシグナルが通知されると、ゲームは画像のレンダリングを開始します。アセット マネージャーは、アセットのダウンロードとトラッキングだけでなく、ゲームへの提供も行います。

最後の要件では、なんらかの getAsset メソッドが必要なため、ここで追加します。

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

このキャッシュ オブジェクトはコンストラクタで初期化され、次のようになります。

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

キャッシュは、次のように downloadAll() の最後に入力されます。

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

ボーナス: バグの修正

バグを見つけましたか?前述のように、isDone メソッドは読み込みイベントまたはエラーイベントがトリガーされた場合にのみ呼び出されます。アセット マネージャーでダウンロード待ちのアセットがない場合はどうなるでしょうか。isDone メソッドはトリガーされず、ゲームは開始されません。

このシナリオに対応するには、次のコードを downloadAll() に追加します。

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

キューにアセットがない場合は、すぐにコールバックが呼び出されます。バグを修正しました。

サンプル使用量

HTML5 ゲームでこのアセット マネージャーを使用するのはとても簡単です。ライブラリを使用する最も基本的な方法は次のとおりです。

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

上記のコードは次のようになります。

  1. 新しいアセット マネージャーを作成する
  2. ダウンロードするアセットをキューに追加
  3. downloadAll() でダウンロードを開始する
  4. コールバック関数を呼び出して、アセットの準備が完了したことを知らせる
  5. getAsset() でアセットを取得する

改善の余地がある領域

ゲームを開発する際に、このシンプルなアセット マネージャーがおそらくは役に立たないでしょう。今後の機能には次のようなものがあります。

  • どのアセットでエラーが発生したかを示す
  • 進行状況を示すコールバック
  • File System API からアセットを取得する

改善点、フォーク、コードへのリンクを以下のコメントに投稿してください。

完全なソース

このアセット マネージャーと、このアセット マネージャーが抽象化しているゲームのソースは、Apache ライセンスに基づくオープンソースであり、Bad Aliens GitHub アカウントにあります。バッド・エイリアンのゲームは HTML5 対応ブラウザでプレイできます。このゲームは、私の Google IO トーク「Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development(スライド動画」)のテーマでした。

まとめ

ほとんどのゲームにはなんらかのアセット マネージャーがありますが、HTML5 ゲームには、ネットワーク経由でアセットを読み込み、エラーを処理するアセット マネージャーが必要です。この記事では、使いやすく、新しい HTML5 ゲームに適応できるシンプルなアセット マネージャーについて概説しました。お楽しみいただけたでしょうか。下のコメント欄でぜひご意見をお聞かせください。よろしくお願いいたします。