個案研究 - Onslaught!運動場

Geoff Blair
Geoff Blair
Matt Hackett
Matt Hackett

簡介

我們在 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 調整大小功能含有防鋸齒功能,這是我們無法預防的方法。

畫布大小調整選項
左圖:在 Photoshop 中,完美像素的素材資源會加倍。右圖:調整 CSS 大小功能會使相片模糊不清。

這對我們的遊戲而言是破壞性的因素,因為個別像素十分重要,但如果您的專案需要調整畫布大小且使用反鋸齒功能,可以基於效能考量,考慮採用這種做法。

趣味畫布小秘訣

我們都知道 <canvas> 是最新的熱度,但有時開發人員仍建議使用 DOM。如果您很熟悉要使用的圍欄,以下是 <canvas> 如何幫助我們節省大量時間和精力的範例。

敵人在 Onslaught!Arena,它會閃爍紅燈,並短暫顯示「派」動畫。為了限制我們要建立的圖形數量,我們只會在一個朝向的方向以「粉筆」呈現敵人。這在遊戲中可接受,並省下大量創作時間。然而,對魔法怪物而言,看到一個大型 Sprite (大小為 64 x 64 像素以上) 從左方或朝向下方貼合,卻突然從疼痛框朝下,實在令人感到不快。

一個顯而易見的解決方案是為 8 個方向的每個 bo 繪製一個問題影格,但這必須耗費大量時間。因為 <canvas>,我們可以在程式碼中解決這個問題:

走進了不幸的隨身者!運動場
您可以使用 context.globalCompositeOperation 來製造有趣的效果。

首先,我們要將怪物繪製至隱藏的「緩衝區」<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.xdirection.y 值應進行正規化,這表示兩者應一律介於 -11 之間。

控管機制

控制方式可能是在開發「Onslaught!」競技場。第一個示範模式僅支援鍵盤;玩家使用方向鍵移動畫面中的主要角色,並依照與空格鍵的面對方向觸發。雖然有些直覺易用,但這使遊戲在較困難的關卡中幾乎無法玩。玩家隨時都有數十個敵人和投影機飛到玩家,有必要在不肖分子之間穿梭「同時」向任何方向發射。

為了與同類型的遊戲比較,我們新增了滑鼠支援來控制目標動作,讓這個角色能用來發動攻擊。這個字元仍可使用鍵盤移動,但完成變更後,他就能同時以任何 360 度方向觸發。硬派玩家感謝這項功能,但這款遊戲對於觸控板使用者相當感到不悅的副作用。

已抓緊!競技場控制項互動視窗 (已淘汰)
Onslaught 的舊控制式或「如何玩」模式!競技場。

為了配合觸控板使用者,我們推出返回箭頭鍵控制項,這次允許在按下的方向觸發。雖然我們瞭解應用程式適合所有類型的玩家,但在不知情的情況下,也使得遊戲內容變得過於複雜。出乎意料,我們之後會聽到有些玩家不知道使用滑鼠 (或鍵盤!) 的控制項進行攻擊,但大部分玩家都忽略了教學課程互動視窗。

已抓緊!競技場控制功能教學課程
玩家大多會忽略教學課程中的重疊元素,他們會想要享受遊戲樂趣!

此外,我們也幸好有部分歐洲粉絲,但他們也瞭解到他們可能不使用一般 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 資料庫等選項並不多。

ALT_TEXT_HERE
這會儲存高分,以及擊敗每位老闆後,你在遊戲中的排名。

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 建立遊戲的未來前景仍顯得十分亮眼。

已抓緊!有隱藏 HTML5 標誌的小運動場
播放「Onslaught!」時輸入「html5」,即可取得 HTML5 盾牌!競技場。