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

はじめに

HTML5 には、ブラウザで最新のレスポンシブで強力なウェブ アプリケーションを構築するための便利な API が多数用意されています。ここまではよかったのですが、ゲームを作成してプレイしたいという気持ちが抑えられません。幸い、HTML5 はゲーム開発の新しい時代を切り開きました。Canvas などの API と強力な JavaScript エンジンを使用して、プラグインを必要とせずにブラウザに直接ゲームを配信できるようになりました。

この記事では、HTML5 ゲーム用のシンプルなアセット管理コンポーネントの作成手順について説明します。アセット マネージャーがないと、不明なダウンロード時間や非同期画像読み込みを補正するのが難しくなります。ここでは、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 枚の画像がすべて準備できたことを確認するにはどうすればよいですか?すべて正常に読み込まれましたか?実際に試合を開始する日時

ソリューション

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

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

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

待機中

最初の要件は、ダウンロードをキューに追加することです。この設計では、必要なアセットを実際にダウンロードしなくても宣言できます。これは、ゲームレベルのすべてのアセットを構成ファイルで宣言する場合などに便利です。

コンストラクタとキューイングのコードの例を次に示します。

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 アカウントで入手できます。Bad Aliens ゲームは、HTML5 対応のブラウザでプレイできます。このゲームは、Google IO での講演「Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development」(スライド動画)のテーマでした。

概要

ほとんどのゲームにはなんらかのアセット マネージャーがありますが、HTML5 ゲームには、ネットワーク経由でアセットを読み込み、障害を処理するアセット マネージャーが必要です。この記事では、次回の HTML5 ゲームで簡単に使用、調整できるシンプルなアセット マネージャーについて説明しました。ぜひお試しください。ご感想がございましたら、以下のコメント欄よりお知らせください。よろしくお願いいたします。