您在這裡

基礎 HTML5 遊戲素材管理器

1 篇文章 / 0 新
基礎 HTML5 遊戲素材管理器

=============================================
原文來自: Simple Asset Management for HTML5 Games
作者: Seth Ladd
難度: 入門
=============================================

基礎 HTML5 遊戲素材管理器

簡介
HTML5 帶來相當多實用的 API 讓人配合瀏覽器建立起現代、快速回應、和強大的網站服務。這些特色都很有趣,但我們想要的其實是製作、遊玩一款遊戲。 幸運的是,透過使用像 Canvas 那樣的 API,或著強大的 js 引擎,我們可以用瀏覽器做遊戲而不仰賴許多 Plugin。

本文會帶大家走一遍幫 HTML5 遊戲建立的素材管理器的過程。沒有管理器,我們會到處都得面對未知的非同步下載,以及各種例外狀況的處理。讓我們來看看這個例子吧!

問題
HTML5 遊戲不能假設遊戲素材會存在於玩家的瀏覽器中,例如圖片和音樂,必須從網路上抓,而瀏覽器也不會事先知道哪種資源會先被下載完成。

一個最基本的載圖程式如下:

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

想像一下遊戲啟動時,若有百張圖片需要被下載和顯示,我們該如何判斷圖片都下載完成了?可以正確存取?何時才能啟動遊戲程式?

解法
讓素材管理器以佇列處理素材資訊,並在下載任務全數完成之後回報給遊戲主程式。管理器要歸納好網路載圖的邏輯,並提供簡單的方法得知目前程式狀態。我們條列一下這個簡易素材管理器的功能需求:

  • 用佇列儲存下載目標
  • 開始下載的功能
  • 追蹤成功與失敗
  • 全數完工後發出通知
  • 易於存取素材的介面

佇列儲存
第一個程式要求是把下載任務存起來,在開始下載動作開始之前就儲存所有任務。這樣的設計比較實用,例如我們常常從設定檔一次讀入一大包素材資訊。

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

開始下載
所有素材都被塞進佇列了,準備開始下載啦。

瀏覽器可以平行地處理多個下載任務,每個遠端可能最多同時建立四個連線。所以一種加速下載的方法就是把素材散步在不同的網域,例如除了把資料擺在 assets.example.com 之外,也擺一些到 assets1.example.com 或 assets2.example.com 或 assets3.example.com。即使這些網址只是別名不同 (CNAME),瀏覽器依然視為不同來源而啟用更多的下載連線數。關於這類技巧可參考:Split Components Across DomainsBest Practices for Speeding Up Your Web Site

我們幫下載任務初始化的功能寫在 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。指定到 src 的動作會觸發瀏覽器的下載行為,所以 downloadAll() 執行完畢下載任務就通通啟動了。

追蹤成功或失敗
另一個需求是追蹤下載任務的成功與失敗,因為很不幸的網路傳輸難免會出包。我們的程式至此只能檢查成功的,就繼續加個錯誤事件處理機制吧。

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() {
this.successCount = 0;
this.errorCount = 0;
this.downloadQueue = [];
}

然後於事件發生時遞增它們。

img.addEventListener("load", function() {
that.successCount += 1;
}, false);
img.addEventListener("error", function() {
that.errorCount += 1;
}, 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);

計數器被增加之後,我們就能判斷最後一個素材是否處理完畢。當管理器做完全部任務,我們藉由呼叫 callback 函數做收尾動作。首先改變 downloadAll() 代碼,增加一個匿名函數 downloadCallback 當做參數。

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

cache 物件是在建構子內部初始化的。

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

cache 內的資料則是在 downloadAll() 裡頭存入。

AssetManager.prototype.downloadAll = function(downloadCallback) {
...
img.addEventListener("error", function() {
that.errorCount += 1;
if (that.isDone()) {
downloadCallback();
}
}, false);
img.src = path;
this.cache[path] = img;
}
}

除錯
有發現 bug 嗎? isDone() 只在事件 load 或事件 error 時被觸發,但若管理器沒有任何素材被加入或被下載,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. 下載完成後,用 callback 函式通知任務完成
5. 用 getAsset 函式取得素材

未來改進方向
未來當你開發自己的遊戲時,想必會繼續擴充這個簡易管理器,可能的擴充方向有:

  • 素材本身有錯誤時也發出訊號
  • 用 Callback 回傳下載進度
  • 從檔案系統載入素材

結論
大多數遊戲擁有某種素材管理器,但 HTML5 遊戲所需的管理器還要能從網路載入資料,並處理下載錯誤的情形。本文提出一個易於使用、適合HTML5遊戲作品的管理器,祝你玩得愉快!