簡介
我們在 2010 年 6 月得知,當地出版「雜誌」Boing Boing 舉辦遊戲開發競賽。我們認為這是一個絕佳的理由,可以使用 JavaScript 和 <canvas>
快速製作簡單的遊戲,因此就開始著手製作。比賽結束後,我們仍有許多想法,希望能完成我們開始的計畫。以下是個案研究的結果,這是一款名為「Onslaught」的小遊戲!競技場。
復古像素風格
由於比賽主旨是開發以芯片音樂為主題的遊戲,因此我們的遊戲必須具有復古 Nintendo Entertainment System 遊戲的視覺和操作體驗。大多數遊戲都沒有這項要求,但由於素材資源容易製作,且對懷舊玩家來說自然吸引力十足,因此仍是常見的藝術風格 (尤其是獨立開發人員)。
考量這些圖像精靈的大小,我們決定將像素加倍,也就是說,16x16 圖像精靈現在會是 32x32 像素,以此類推。從一開始,我們就一直在資產建立方面加倍努力,而不是讓瀏覽器負擔繁重的工作。這麼做不但實作起來更容易,也能帶來一些明顯的外觀優勢。
以下是我們考慮的情況:
<style>
canvas {
width: 640px;
height: 320px;
}
</style>
<canvas width="320" height="240">
Sorry, your browser is not supported.
</canvas>
這個方法會包含 1x1 圖像,而不是在素材資源建立端將圖像加倍。接著,CSS 會接手並調整畫布本身的大小。我們的基準測試顯示,這個方法的速度大約是渲染較大 (加倍) 圖片的兩倍,但不幸的是,CSS 大小調整會包含反鋸齒,而我們無法找到避免這種情況的方法。
這對我們的遊戲來說是個致命缺陷,因為個別像素非常重要。不過,如果您需要調整畫布的大小,且您認為抗鋸齒處理方式適合您的專案,則可以考慮採用這種做法來提升效能。
有趣的畫布技巧
我們都知道 <canvas>
是新熱門技術,但有時開發人員仍建議使用 DOM。如果您不確定要使用哪一個,以下範例說明 <canvas>
如何為我們節省大量時間和精力。
在Onslaught!Arena,會閃爍紅色,並短暫顯示「痛苦」動畫。為了限制所需建立的圖形數量,我們只會在向下方向顯示「痛苦」的敵人。這在遊戲中看起來相當不錯,而且省下了大量製作圖像片段的時間。不過,對於 Boss 怪物而言,如果大型精靈 (64 x 64 像素或更大) 在痛苦畫面中從左或上方突然轉向下方,會讓人感到不適。
顯而易見的解決方法,就是為每個 Boss 在八個方向繪製痛苦影格,但這會耗費大量時間。多虧了 <canvas>
,我們才能在程式碼中解決這個問題:
首先,我們將怪物繪製到隱藏的「緩衝區」<canvas>
,並以紅色重疊,然後將結果算繪回螢幕。程式碼如下所示:
// Get the "buffer" canvas (that isn't visible to the user)
var bufferCanvas = document.getElementById("buffer");
var buffer = bufferCanvas.getContext("2d");
// Draw your image on the buffer
buffer.drawImage(image, 0, 0);
// Draw a rectangle over the image using a nice translucent overlay
buffer.save();
buffer.globalCompositeOperation = "source-in";
buffer.fillStyle = "rgba(186, 51, 35, 0.6)"; // red
buffer.fillRect(0, 0, image.width, image.height);
buffer.restore();
// Copy the buffer onto the visible canvas
document.getElementById("stage").getContext("2d").drawImage(bufferCanvas, x, y);
遊戲迴圈
遊戲開發與網頁開發有幾項明顯差異。在網頁堆疊中,通常會透過事件監聽器回應發生的事件。因此,除了監聽輸入事件之外,初始化程式碼可能不會執行任何操作。遊戲邏輯則不同,因為遊戲需要持續更新。舉例來說,如果玩家沒有移動,那麼惡狼人就不會因此停止攻擊!
以下是遊戲迴圈的範例:
function main () {
handleInput();
update();
render();
};
setInterval(main, 1);
第一個重要的差異在於,handleInput
函式實際上並不會立即「執行」任何操作。如果使用者在一般網頁應用程式中按下按鍵,系統應立即執行所需動作。但在遊戲中,事件必須按時間順序發生,才能正確流動。
window.addEventListener("mousedown", function(e) {
// A mouse click means the players wants to attack.
// We don't actually do that yet, but instead tell the rest
// of the program about the request.
buttonStates[e.button] = true;
}, false);
function handleInput() {
// Here is where we respond to the click
if (buttonStates[LEFT_BUTTON]) {
player.attacking = true;
delete buttonStates[LEFT_BUTTON];
}
};
我們現在已瞭解輸入內容,因此可以將其納入 update
函式,並確保其遵循其他遊戲規則。
function update() {
// Check for collisions, states, whatever else is needed
// If after that the player can still attack, do it!
if (player.attacking && player.canAttack()) {
player.attack();
}
};
最後,在所有運算完成後,您就可以重新繪製畫面!在 DOM 世界中,瀏覽器會處理這項重複的動作。不過,使用 <canvas>
時,每當發生某些事件 (通常是每個影格),就必須手動重繪。
function render() {
// First erase everything, something like:
context.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// Draw the player (and whatever else you need)
context.drawImage(
player.getImage(),
player.x, player.y
);
};
時間型模擬
以時間為基礎的模擬是指根據上一個影格更新後經過的時間長度,移動圖像集的概念。這項技巧可讓遊戲盡可能以最快的速度執行,同時確保精靈以一致的速度移動。
為了使用以時間為基礎的模擬,我們需要擷取自上次繪製影格起的經過時間。我們需要擴充遊戲迴圈的 update()
函式,才能追蹤這項資訊。
function update() {
// NOTE: You'll need to initially seed this.lastUpdate
// with the current time when your game loop starts
// this.lastUpdate = Date.now();
// Calculate elapsed time since last frame
var now = Date.now();
var elapsed = (now - this.lastUpdate);
this.lastUpdate = now;
// Do stuff with elapsed
};
有了經過的時間,我們就能計算指定的單格角色應在每個影格移動多遠。首先,我們需要追蹤精靈物件上的幾項資訊:目前位置、速度和方向。
var Sprite = function() {
// The sprite's position relative to the top left of the game world
this.position = {x: 0, y: 0};
// The sprite's direction. A positive x value indicates moving to the right
this.direction = {x: 1, y: 0};
// How many pixels the sprite moves per second
this.speed = 50;
};
考量這些變數後,我們會使用以時間為基礎的模擬方式移動上述圖像文字類別的例項:
// Determine how far this sprite will move this frame
var distance = (sprite.speed / 1000) * elapsed;
// Apply the movement distance to the sprite's current position
// taking into account its direction
sprite.position.x += (distance * sprite.direction.x);
sprite.position.y += (distance * sprite.direction.y);
請注意,direction.x
和 direction.y
值應經過正規化,也就是說,它們一律應介於 -1
和 1
之間。
控制項
在開發 Onslaught!競技場。最早的示範僅支援鍵盤,玩家可透過方向鍵在畫面上移動主角,並透過空格鍵朝向角色面向的方向開火。雖然這項功能相當直覺且容易上手,但在較困難的關卡中,玩家幾乎無法順利進行遊戲。由於隨時都有數十個敵人和飛彈朝玩家飛來,因此您必須能夠在任何方向射擊的同時,穿梭於敵人之間。
為了與同類型遊戲進行比較,我們新增了滑鼠支援功能,用於控制角色用來瞄準攻擊目標的瞄準十字線。角色仍可透過鍵盤移動,但在這個變更之後,角色可以同時在任何 360 度方向發射。雖然核心玩家很喜歡這項功能,但不幸的是,這會讓使用觸控板的使用者感到不便。
為方便使用觸控板的使用者,我們已將箭頭鍵控制項重新加入,這次是為了讓箭頭鍵在按下的方向中發射。雖然我們認為我們可以滿足所有類型的玩家,但我們也無意間為遊戲引入過多複雜性。令人意外的是,我們後來得知,有些玩家並未注意到攻擊的選用滑鼠 (或鍵盤) 控制項,儘管教學課程會話方塊 (大多遭到忽略) 已提供相關資訊。
我們很榮幸擁有一些歐洲粉絲,但我們聽到他們的抱怨,表示他們可能沒有一般 QWERTY 鍵盤,無法使用 WASD 鍵進行方向移動。左撇子玩家也提出類似的抱怨。
我們實作了這個複雜的控制方案,但在行動裝置上播放時也會發生問題。事實上,我們最常收到的要求之一,就是要推出 Onslaught!Arena 適用於 Android、iPad 和其他觸控裝置 (沒有鍵盤的裝置)。HTML5 的核心優勢之一就是可移植性,因此將遊戲移至這些裝置絕對可行,我們只需要解決許多問題 (最明顯的是控制項和效能)。
為瞭解決這些問題,我們開始使用單一輸入方法進行遊戲,只使用滑鼠 (或觸控) 互動。玩家點選或輕觸螢幕後,主角會朝按下的位置走去,並自動攻擊最近的壞人。程式碼如下所示:
// Find the nearest hostile target (if any) to the player
var player = this.getPlayerObject();
var hostile = this.getNearestHostile(player);
if (hostile !== null) {
// Found one! Shoot in its direction
var shoot = hostile.boundingBox().center().subtract(
player.boundingBox().center()
).normalize();
}
// Move towards where the player clicked/touched
var move = this.targetReticle.position.clone().subtract(
player.boundingBox().center()
).normalize();
var distance = this.targetReticle.position.clone().subtract(
player.boundingBox().center()
).magnitude();
// Prevent jittering if the character is close enough
if (distance < 3) {
move.zero();
}
// Move the player
if ((move.x !== 0) || (move.y !== 0)) {
player.setDirection(move);
}
移除必須瞄準敵人的額外因素,在某些情況下可能會讓遊戲變得更簡單,但我們認為為玩家簡化操作有許多好處。其他策略也隨之出現,例如必須將角色設在靠近危險敵人的地點,才能鎖定敵人,而支援觸控裝置的能力也非常重要。
音訊
在控制項和效能方面,Onslaught!Arena 是 HTML5 的 <audio>
標記。最糟糕的情況可能是延遲:在幾乎所有瀏覽器中,呼叫 .play()
和實際播放音訊之間都會有延遲。這可能會破壞玩家的遊戲體驗,尤其是在玩像我們這類節奏快速的遊戲時。
其他問題包括「進度」事件無法觸發,這可能導致遊戲的載入流程無限期掛起。基於這些原因,我們採用了所謂的「備援」方法,如果 Flash 無法載入,我們會改用 HTML5 Audio。程式碼如下所示:
/*
This example uses the SoundManager 2 library by Scott Schiller:
http://www.schillmania.com/projects/soundmanager2/
*/
// Default to sm2 (Flash)
var api = "sm2";
function initAudio (callback) {
switch (api) {
case "sm2":
soundManager.onerror = (function (init) {
return function () {
api = "html5";
init(callback);
};
}(arguments.callee));
break;
case "html5":
var audio = document.createElement("audio");
if (
audio
&& audio.canPlayType
&& audio.canPlayType("audio/mpeg;")
) {
callback();
} else {
// No audio support :(
}
break;
}
};
遊戲也應支援無法播放 MP3 檔案的瀏覽器 (例如 Mozilla Firefox)。在這種情況下,系統可以偵測支援情況,並切換至 Ogg Vorbis 等,程式碼如下所示:
/*
Note: you could instead use "new Audio()" here,
but the client will throw an error if it doesn't support Audio,
which makes using "document.createElement" a safer approach.
*/
var audio = document.createElement("audio");
if (audio && audio.canPlayType) {
if (!audio.canPlayType("audio/mpeg;")) {
// Here you know you CANNOT use .mp3 files
if (audio.canPlayType("audio/ogg; codecs=vorbis")) {
// Here you know you CAN use .ogg files
}
}
}
儲存資料
沒有高分,就無法在街機風格的射擊遊戲中脫穎而出!我們知道需要保留部分遊戲資料,雖然可以使用 Cookie 等舊技術,但我們想深入瞭解新穎的 HTML5 技術。當然,您可以選擇多種選項,包括本機儲存空間、工作階段儲存空間和網路 SQL 資料庫。
我們決定使用 localStorage
,因為它是新功能,且易於使用。它支援儲存基本鍵/值組合,這是簡單遊戲所需的所有內容。以下是使用方式的簡單範例:
if (typeof localStorage == "object") {
localStorage.setItem("foo", "bar");
localStorage.getItem("foo"); // Value is "bar"
localStorage.removeItem("foo");
localStorage.getItem("foo"); // Value is now null
}
但也有一些「陷阱」需要留意。無論您傳入什麼,值都會儲存為字串,這可能會導致一些非預期的結果:
localStorage.setItem("foo", false);
typeof localStorage.getItem("foo"); // Value is "false" (a string literal)
if (localStorage.getItem("foo")) {
// It's true!
}
// Don't pass objects into setItem
localStorage.setItem("bar", {"key": "value"});
localStorage.getItem("bar"); // Value is "[object Object]" (a string literal)
// JSON stringify and parse when dealing with localStorage
localStorage.setItem("json", JSON.stringify({"key": "value"}));
typeof localStorage.getItem("json"); // string
JSON.parse(localStorage.getItem("json")); // {"key": "value"}
摘要
HTML5 是相當優異的程式語言,大多數實作項目都會處理遊戲開發人員需要的所有內容,從圖形到儲存遊戲狀態。雖然 HTML5 仍有待改進之處 (例如 <audio>
標記的問題),但瀏覽器開發人員的腳步相當迅速,且 HTML5 遊戲的表現已相當出色,因此未來前景一片光明。