簡易的 HTML5 遊戲素材資源管理

Seth Ladd

引言

HTML5 提供許多實用的 API,可讓您在瀏覽器中建立先進、回應且功能強大的網頁應用程式。這麼做固然很好,但你真的很想建構並玩遊戲!幸好,HTML5 也已進入新遊戲開發時代,透過 Canvas 和強大的 JavaScript 引擎等 API,直接在瀏覽器中提供遊戲,不需另外安裝外掛程式。

本文將逐步說明如何為 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 張圖片全都準備就緒?是否全部成功載入?遊戲何時能實際開始?

解決方法

讓資產管理員處理資產排入佇列,並在一切準備就緒時回報給遊戲。資產管理工具會將透過網路載入資產的邏輯統整為一句,方便您查看狀態。

我們的簡易資產管理工具有下列規定:

  • 將下載排入佇列
  • 開始下載。
  • 追蹤成功和失敗
  • 發出訊號
  • 輕鬆擷取資產

正在排入佇列

第一項規定是將下載排入佇列。這種設計可讓您宣告需要的資產,而不必實際下載。舉例來說,如果想在設定檔中宣告遊戲關卡的所有資產,這項功能就能派上用場。

建構函式和佇列的程式碼如下所示:

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

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

開始下載

將所有要下載的資產加入佇列後,即可要求資產管理員開始下載所有資料。

不過,網路瀏覽器可以平行執行下載作業,通常每個主機最多 4 個連線。如要加快資產下載速度,其中一種方法是使用一系列的網域名稱代管資產。例如,請嘗試使用 asset1.example.com、asset2.example.com、asset3.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() 只會疊代下載佇列,並建立新的 Image 物件。新增載入事件的事件監聽器,並設定圖片的 src ,藉此觸發實際下載作業。

這樣您就可以開始下載。

追蹤成功和失敗

另一個要求是追蹤成功與失敗情況,因為並非萬無一失。目前,程式碼只追蹤成功下載的素材資源。只要為錯誤事件新增事件監聽器,您就能擷取成功和失敗情況。

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 與下載佇列的大小,藉此知道每項資產是否順利完成或發生錯誤。

當然,想要知道是否確實完成工作只是其中一半,資產管理員也必須檢查這種方法。我們會在事件處理常式中同時新增這項檢查,如下列程式碼所示:

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 相容的瀏覽器播放。這個遊戲是我名為「Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development」(HTML5 遊戲開發簡介) 的 Google IO 講座的主題 (投影片影片)。

摘要

大多數遊戲都有一種資產管理工具,但 HTML5 遊戲需要使用資產管理工具,才能透過網路載入素材資源並處理錯誤。本文將概述簡單的素材資源管理工具,可供您輕鬆使用,並配合您接下來的 HTML5 遊戲進行調整。希望您可以玩,歡迎在下方留言分享心得。Thanks!