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

Seth Ladd

簡介

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 張圖片都已準備就緒?是否都已成功載入?遊戲實際開始的時間

解決方法

讓資產管理員處理資產排序,並在所有項目準備就緒時回報給遊戲。資產管理工具可將透過網路載入資產的邏輯進行泛化,並提供簡單的檢查狀態方式。

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

  • 將下載項目加入佇列
  • 開始下載
  • 追蹤成功和失敗
  • 在所有作業完成時發出信號
  • 輕鬆擷取資產

正在排入佇列

第一個必要條件是排入下載佇列。這項設計可讓您宣告所需的素材資源,而不必實際下載。舉例來說,如果您想在設定檔中宣告遊戲關卡的所有素材資源,這項功能就很實用。

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

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

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

開始下載

將所有要下載的素材資源排入佇列後,您可以要求資產管理工具開始下載所有素材資源。

幸運的是,網頁瀏覽器可以並行下載,通常每個主機最多可有 4 個連線。如要加快素材資源下載速度,可以使用一系列網域名稱代管素材資源。舉例來說,請改用 assets1.example.com、assets2.example.com、assets3.example.com 等網址,不要從 assets.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 物件。系統會新增載入事件的事件監聽器,並設定圖片的 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 與 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);

計數器加 1 後,我們會查看該資產是否為佇列中的最後一個資產。如果素材資源管理工具確實已完成下載,那麼我們應該採取哪些行動?

如果素材資源管理工具已完成下載所有素材資源,我們當然會呼叫回呼方法!讓我們變更 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 License 授權下的開放原始碼,可在 Bad Aliens GitHub 帳戶中找到。您可以在支援 HTML5 的瀏覽器中玩Bad Aliens 遊戲。這款遊戲是我在 Google IO 的演講主題,演講名稱為「Super Browser 2 Turbo HD Remix:HTML5 遊戲開發簡介」(投影片面影片)。

摘要

大部分遊戲都有某種資產管理工具,但 HTML5 遊戲需要透過網路載入資產並處理失敗情況的資產管理工具。本文概述了簡單的素材資源管理工具,您應該可以輕鬆使用並調整,以便在下一個 HTML5 遊戲中使用。歡迎試用,並在下方留言分享你的想法。感謝您!