2014 年哈比人體驗

將 WebRTC 遊戲過程加入哈比體驗

丹尼爾.伊薩克森 (Daniel Isaksson)
Daniel Isaksson

準備推出全新《哈比人:五軍之戰》新電影《哈比人:五軍之戰》後,我們為去年的 Chrome 實驗功能「中土大陸冒險之旅」推出更多新內容。本次的主要重心是擴大使用 WebGL,使更多瀏覽器和裝置能夠檢視內容,以及在 Chrome 和 Firefox 中使用 WebRTC 功能。今年的實驗有三個目標:

  • 在 Android 版 Chrome 中使用 WebRTC 和 WebGL 玩 P2P 遊戲
  • 打造採用觸控輸入技術的多人對戰遊戲
  • 在 Google Cloud Platform 上託管

定義遊戲

遊戲邏輯是以格線設定為基礎,軍隊在遊戲板上移動。這樣一來,我們就能在設計規則時,輕鬆試用紙上的遊戲過程。使用格線設定也有助於確保遊戲中的衝突偵測功能維持良好效能,因為您只需檢查相同或鄰近圖塊中的物件是否有衝突。我們從一開始就知道要將新遊戲集中在中土世界、人類、矮人、精靈和半島四大軍隊之間的戰鬥。使用 Chrome 實驗功能時,影片互動方式必須夠輕鬆,且互動性不足才能學習。 我們首先在中土世界地圖上定義了五個戰場,藉此做為遊戲室,讓多位玩家在點對點戰鬥中彼此較量。在行動裝置的螢幕上展示多位玩家,並讓使用者自行選擇挑戰對象。為促進互動與場景,我們決定只使用一個按鈕挑戰,接受挑戰,並且只使用房間來顯示活動,以及誰是山上目前的王者。這個方向也解決了配對方面的一些問題,讓我們得以為最優質的戰鬥機會配對。 在我們先前的 Chrome 實驗 Cube Slam 中,我們發現需要耗費大量心力,才能處理多人遊戲的延遲情況 (如果遊戲的延遲情況與自身需要)。您必須不斷假設對手的狀態,讓對手認為你就是你,並將你與不同裝置上的動畫保持同步。本文會詳細說明這些挑戰。為了方便起見,我們設計這款回合製遊戲。

遊戲邏輯是以格線設定為基礎,軍隊在遊戲板上移動。這樣一來,我們就能在設計規則時,輕鬆試用紙上的遊戲過程。使用格線設定也有助於讓遊戲中的衝突偵測保持良好效能,因為您只需檢查相同或鄰近圖塊中物體的衝突。

遊戲內容

為了製作這款多人對戰遊戲,我們必須建構幾個關鍵部分:

  • 伺服器端玩家管理 API 會處理使用者、隨機對戰、工作階段和遊戲統計資料。
  • 協助建立玩家連線的伺服器。
  • 這個 API 用於處理 AppEngine Channel API 訊號;這個信號的用途是與遊戲室中的所有玩家連線並通訊。
  • 這個 JavaScript 遊戲引擎會處理狀態同步作業,以及兩個玩家/對等點之間的 RTC 訊息同步作業。
  • WebGL 遊戲檢視畫面。

玩家管理

為支援大量玩家,我們在每場戰鬥中分別使用多人並行的遊戲室。限制每間遊戲空間的玩家人數,主要是為了讓新玩家在合理的時間內排名第一的排行榜。此限制也與 JSON 物件的大小相關,說明透過 Channel API 傳送的遊戲房間大小上限為 32 KB。我們必須儲存玩家、房間、得分、工作階段以及遊戲中的玩家關係,為此,我們先針對實體採用 NDB,並透過查詢介面處理關係。NDB 是 Google Cloud Datastore 的介面。一開始使用 NDB 時效果很好,但用不到 NDB 的方式不久後就遇到了問題。查詢會對資料庫的「修訂」版本執行 (NDB 寫入非常長,詳情請參閱這篇深入文章),可能會延遲數秒。但是實體本身沒有延遲,因為它們是直接從快取回應。以下提供一些範例程式碼,可能會更容易說明:

// example code to explain our issue with eventual consistency
def join_room(player_id, room_id):
    room = Room.get_by_id(room_id)
    
    player = Player.get_by_id(player_id)
    player.room = room.key
    player.put()
    
    // the player Entity is updated directly in the cache
    // so calling this will return the room key as expected
    player.room // = Key(Room, room_id)

    // Fetch all the players with room set to 'room.key'
    players_in_room = Player.query(Player.room == room.key).fetch()
    // = [] (an empty list of players)
    // even though the saved player above may be expected to be in the
    // list it may not be there because the query api is being run against the 
    // "committed" version and may still be empty for a few seconds

    return {
        room: room,
        players: players_in_room,
    }

您新增單元測試後,我們就可以清楚看到問題,因此已從查詢中移除,改為在 memcache 中將關係保留在逗號分隔清單中。這感覺到有點複雜,但卻有用,而 AppEngine 記憶快取使用極佳的「比較與設定」功能,為鍵提供了類似於交易的系統,而現在測試再次通過測試。

可惜的是,Memcache 並非全部彩虹和獨角獸,但會有一些限制,其中最值得注意的是,值大小為 1MB 值 (不能有太多與戰場相關的會議室)、重要到期時間,或是說明文件的說明:

我們考慮使用另一個很棒的鍵/值存放區 Redis。但當時設定可擴充的叢集是一項艱鉅的任務,因為我們希望能專心打造體驗,而不是處理未依循此途徑的伺服器維護服務。另一方面,Google Cloud Platform 最近推出了簡易的「點擊部署」功能,其中之一是 Redis 叢集的選項,因此這是相當有趣的選項。

最後我們找到 Google Cloud SQL,並將關係移至 MySQL。這已耗費大量作業,但最後成效非常出色,更新現已完全完整,且測試仍然通過。此外,遊戲配對和評分機制的效率也大幅提升。

隨著時間的推移,越來越多資料逐漸從 NDB 和 Memcache 移轉至 SQL,但一般來說,玩家、戰場和會議室實體仍會儲存在 NDB 中,而所有工作階段和關係實體均儲存在 SQL 中。

我們也必須追蹤玩家是誰,並在將玩家技能等級和經驗納入考量時,透過配對機制來配對玩家。我們以開放原始碼程式庫 Glicko2 為基礎。

由於這是多人對戰遊戲,所以我們希望通知會議室中其他玩家的活動,例如「入選或離開誰」、「贏了或輸的者」,以及是否可接受挑戰。為此,我們內建在 Player Management API 中接收通知的功能。

設定 WebRTC

當兩名玩家配對對戰,訊號服務即可使用信號服務讓兩名配對的同儕互相交流,並協助建立對等的同儕連線。

有多個第三方程式庫可以用於信號服務,也可以簡化 WebRTC 的設定流程。部分選項包括 PeerJSSimpleWebRTCPubNub WebRTC SDK。PubNub 使用託管伺服器解決方案,而我們希望這項專案在 Google Cloud Platform 上託管。其他兩個程式庫所使用的 node.js 伺服器可以安裝在 Google Compute Engine 上,但我們也必須確保該伺服器能處理數千名同時作業使用者,這是我們早已知道 Channel API 具備的功能。

在這個案例中,使用 Google Cloud Platform 的主要優點之一就是擴充。您可以透過 Google Developers Console 輕鬆調整 AppEngine 專案所需的資源配置,使用 Channel API 時不需要額外處理信號服務的資源配置。

關於延遲時間和 Channel API 的穩定性,我們有一些疑慮,但先前我們曾將其用於 CubeSlam 專案,且經證實適用於該專案的數百萬名使用者,因此決定再次使用。

我們當初沒有選擇使用第三方程式庫來協助 WebRTC,因此必須自行建構。幸好我們可以重複使用我們在 CubeSlam 專案上所做的許多工作。當兩位玩家都已加入工作階段後,工作階段就會設為「有效」,隨後兩位玩家都會使用這個運作中的工作階段 ID,透過 Channel API 啟動點對點連線。之後,系統將透過 RTCDataChannel 處理兩位玩家之間的所有通訊。

我們也需要 STUN 和 TURN 伺服器,以透過 NAT 和防火牆建立連線並應付流量。如要進一步瞭解如何設定 WebRTC,請參閱 HTML5 Rocks 文章中的「WebRTC: STUN、TURN 和信號」。

使用的 TURN 伺服器數量也必須能根據流量調度資源。為瞭解決這個問題,我們測試了 Google Deployment Manager。可讓我們在 Google Compute Engine 上動態部署資源,並使用範本安裝 TURN 伺服器。這項工具仍在 Alpha 測試階段,但對於我們的用途而言,這項功能依然沒有問題。針對 TURN 伺服器,我們使用 coturn,這是非常快速、有效且看似穩定的 STUN/TURN 實作。

Channel API

Channel API 可用於在用戶端的遊戲室之間收發所有通訊。我們的 Player Management API 使用 Channel API 傳送遊戲事件相關通知。

使用頻道 API 時會有幾次速度飛快。舉例來說,由於郵件看起來沒有排序的排列順序,因此我們必須將所有訊息納入物件中並進行排序。以下是運作方式的程式碼範例:

var que = [];  // [seq, packet...]
var seq = 0;
var rcv = -1;

function send(message) {
  var packet = JSON.stringify({
    seq: seq++,
    msg: message
  });
  channel.send(packet);
}

function recv(packet) {
  var data = JSON.parse(packet);

  if (data.seq <= rcv) {
    // ignoring message, older or already received
  } else if (data.seq > rcv + 1) {
    // message from the future. queue it up.
    que.push(data.seq, packet);
  } else {
    // message in order! update the rcv index and emit the message
    rcv = data.seq;
    emit('message', data.message);

    // and now that we have updated the `rcv` index we 
    // will check the que for any other we can send
    setTimeout(flush, 10);
  }
}

function flush() {
  for (var i=0; i<que.length; i++) {
    var seq = que[i];
    var packet = que[i+1];
    if (data.seq == rcv + 1) {
      recv(packet);
      return; // wait for next flush
    }
  }
}

此外,我們也希望保留網站模組化的各種 API,並與網站代管分開,並藉由 GAE 內建的模組開始著手。但在全面執行中運作後,我們發現 Channel API 無法在正式環境中使用模組。改用獨立的 GAE 執行個體,並發生 CORS 問題,強迫我們使用 iframe postMessage 橋接器

遊戲引擎

我們透過實體元件系統 (ECS) 方法,打造前端應用程式,盡可能讓遊戲引擎的動態化。我們開始開發時並未設定版面配置框和功能規格,所以可以隨開發進度新增功能和邏輯,非常實用。例如,第一個原型使用簡單的畫布算繪系統,在格線中顯示實體。有些會「之後疊代」,新增了一個衝突系統,並為 AI 控管型玩家提供了一個系統。在專案中,我們可以切換至 3D 轉譯器系統,而不會變更其他程式碼。當網路部分啟動並執行 AI-system 時,可修改網路部分以使用遠端指令。

因此,多人遊戲的基本邏輯是透過 DataChannels 將動作指令的設定傳送至其他對手,然後讓模擬模擬體驗,就像是 AI 玩家一樣。此外,還有邏輯決定轉彎的邏輯,如果玩家按下傳遞/攻擊按鈕,或者在玩家仍在查看上一個動畫時,將指令排入佇列等等。

如果只有兩位使用者輪到換機,雙方的玩家可以共同在工作完成後將這回合給對手,但最後還有第三位玩家。當我們需要加入自動尋檢程式和網路小精靈等敵人,AI 系統就再次派上用場,不僅僅是測試用途。為了讓這些容器能融入回合製流程中,兩邊都必須以相同方式產生和執行。只要讓一個對等點控制轉彎系統,並將目前狀態傳送給遠端對等節點,即可解決這個問題。當自動尋檢程式轉動時,轉彎管理器就會讓 AI 系統建立要發送給遠端使用者的指令。由於遊戲引擎只是對命令和實體 ID 做出回應,所以遊戲的模擬畫面會保持不變。所有單元還可以採用 ai 元件,方便進行自動化測試。

最佳做法是在開發初期使用較簡單的畫布轉譯器,同時專注於遊戲邏輯。不過,當實作 3D 版本,並讓場景和動畫自然活靈活現,真正有趣的事起了。我們使用 three.js 做為 3d-engine,而由於架構的關係,很容易進入可播放的狀態。

傳送滑鼠位置給遠端使用者的頻率較高,並且會以 3D 小提示指出遊標目前所在位置。