簡介
我們在 2010 年 6 月時發現,本地發布《Zine》Boing Boing 舉行遊戲開發競賽。
我們認為這很適合用來在 JavaScript 和 <canvas>
中製作簡易的簡易遊戲,因此決定開始運作。比賽結束後
我們累積了許多構想這是個案研究的個案研究,小遊戲名為 Onslaught!競技場。
復古像素風設計
重要的是,我們的遊戲外觀與風格就像復古的任天堂娛樂系統遊戲,具有競賽前提,以根據晶片開發遊戲。大部分遊戲都沒有這項規定,但由於建立資產十分容易,且深受懷舊玩家的喜愛,因此仍是普遍的藝術風格 (特別是獨立開發人員)。
由於這些 Sprite 的尺寸相當小,因此我們決定將像素加倍,這表示 16 x 16 Sprite 現在的尺寸會是 32x32 像素,以此類推。從一開始,我們就將心力加倍處理素材資源的建立作業,而不是讓瀏覽器代勞。實作方式很簡單,但也有一些明顯的外表優勢。
我們會考慮以下情況:
<style>
canvas {
width: 640px;
height: 320px;
}
</style>
<canvas width="320" height="240">
Sorry, your browser is not supported.
</canvas>
這個方法將包含 1x1 Sprite,而非在素材資源建立方面加倍。接著,CSS 會接管並調整畫布本身大小。我們的基準顯示,相較於轉譯大型 (將增加一倍) 的圖片,這種方法的速度可達約兩倍,還令人驚訝的是,CSS 調整大小功能含有防鋸齒功能,這是我們無法預防的方法。
這對我們的遊戲而言是破壞性的因素,因為個別像素十分重要,但如果您的專案需要調整畫布大小且使用反鋸齒功能,可以基於效能考量,考慮採用這種做法。
趣味畫布小秘訣
我們都知道 <canvas>
是最新的熱度,但有時開發人員仍建議使用 DOM。如果您很熟悉要使用的圍欄,以下是 <canvas>
如何幫助我們節省大量時間和精力的範例。
敵人在 Onslaught!Arena,它會閃爍紅燈,並短暫顯示「派」動畫。為了限制我們要建立的圖形數量,我們只會在一個朝向的方向以「粉筆」呈現敵人。這在遊戲中可接受,並省下大量創作時間。然而,對魔法怪物而言,看到一個大型 Sprite (大小為 64 x 64 像素以上) 從左方或朝向下方貼合,卻突然從疼痛框朝下,實在令人感到不快。
一個顯而易見的解決方案是為 8 個方向的每個 bo 繪製一個問題影格,但這必須耗費大量時間。因為 <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
);
};
以時間為基礎的模擬
時間模型是根據上次影格更新後經過的時間,來移動 Sprite 的概念。這項技術可讓遊戲盡快執行,同時確保 Sprite 以一致的速度移動。
為了使用以時間為基礎的模型,我們必須擷取自上一個影格繪製以來經過的時間。我們需要擴充遊戲迴圈的 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
};
我們現在已取得經過時間,可以計算特定 Sprite 每個影格的移動距離。首先,我們必須追蹤 Sprite 物件的幾個項目:目前位置、速度和方向。
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;
};
瞭解這些變數後,以下是我們如何使用以時間為基礎的模型移動上述 Sprite 類別的執行個體:
// 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 鍵進行方向移動。左手的玩家提出類似的申訴。
透過我們實作的複雜控製配置,在行動裝置上播放時也有問題。事實上,我們最常見的要求之一是對調!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()
和實際播放音訊之間會有延遲。這可能會導致玩家體驗不佳,在玩像 Google 這類快節奏的遊戲時更是如此。
其他問題包括「進度」事件無法觸發,這可能導致遊戲的載入流程無限期停止。基於上述原因,我們採用「備用」方法,如果 Flash 無法載入,就會改用 HTML5 音訊。程式碼應如下所示:
/*
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 是非常強大的功能。多數實作項目可處理遊戲開發人員需要的一切,包括圖像和儲存遊戲狀態。雖然出現越來越難的痛苦 (例如 <audio>
標記),但瀏覽器開發人員也在變化快速,擁有和過去一樣優異的功能,因此使用 HTML5 建立遊戲的未來前景仍顯得十分亮眼。