Share via



2017 年 10 月

第 32 卷,第 10 期

本文章是由機器翻譯。

遊戲程式開發 - 網頁遊戲程式開發的多人網路物理

Gary Weiss | 2017 年 10 月

Web 遊戲都有 shoddy 評價。編寫完善的遊戲是可在高階的遊戲主控台,以及配備現代圖形處理器的電腦使用。即使行動遊戲有不少自標準 「 Snake。 」 但首先 Dangerfield 類似的 Web 遊戲,取得任何方面。確認前述、 最近使用的進階功能,在新式瀏覽器中可能會將品質遊戲體驗帶到瀏覽器。在這一方面最重要的技術是 WebGL 為硬體加速圖形的直接存取。

我將在本文中,調查 Web 遊戲,從特定的觀點: 簡單多人網路物理遊戲的實作。這個練習中,以調查的技術和遊戲開發工具可供工作和壓力測試網路效能、 計算效率,以及轉譯的新式瀏覽器。本文可被視為屬於多大佈景主題的小型調查:可以專業的遊戲 studio 真的撰寫高階的遊戲在瀏覽器?

遊戲的原始程式碼位於自由bit.ly/2toc4h4,且在遊戲牌桌線上 sprocketleague.lance.gg 在。

遊戲

遊戲我將模型牽涉到汽車周圍場地中。可以跳回至目標場地中沒有大球。這個概念是受到 「 火箭 League,「 熱門電子運動標題所 Psyonix 所啟發。因此,遊戲會組合成從下列物件: 舞 (包括目標),汽車和球,您可以看到在圖 1。播放程式可以轉寄或反向移動汽車,然後開啟滾輪。

在您的瀏覽器中播放足球汽車

圖 1 汽車瀏覽器中播放足球

架構的遊戲

遊戲的實作會使用授權伺服器的模型,讓每個用戶端轉送到中央伺服器的使用者輸入。用戶端將不會等待網路之前套用輸入本機;相反地,用戶端將會執行什麼統稱為用戶端的預測。這項技術可讓用戶端繼續執行遊戲邏輯和遊戲物理。Catch,當然是,當位置和時的更新會廣播來自授權伺服器,每個用戶端必須進行自己累加修正,讓遊戲會保留平滑的視覺效果。

轉譯在轉譯的主要目標存取 GPU。有數個工具,可以藉由簡化 WebGL 介面填入這個位置。Babylon.js 和 Three.js 已經使用了許多進階 Web 遊戲。此遊戲我用 A-Frame,較新的程式庫,可讓您建置的 3D 場景使用 HTML 項目。A-Frame 定義新的 HTML 元素,您可以使用做為建置組塊。此外,A-Frame 會很好的模型使用的實體元件系統設計模式的物件。A-Frame 有趣另一個優點是,設計虛擬實境 (VR),因此可以輕鬆地進行遊戲到 VR 經驗。

物理Cannon.js 是相當方便使用物理引擎。它會提供固定主體機制與碰撞偵測 」。它是以 JavaScript 撰寫從頭,這表示程式庫是遠小於例如 ammo.js transpiled 引擎。

物件序列化和網路廣播物件位置和時的伺服器,遊戲物件需要序列化。針對此用途使用騎士遊戲伺服器 (Lance.gg)、 開放原始碼多人連線遊戲引擎。遊戲衍生騎士 GameObject 基底類別的物件會序列化並轉送至用戶端。進階的遊戲引擎以提升效能、 使用 UDP 通訊端,而騎士仍會使用 TCP 通訊端。這是因為 WebRTC 的複雜度 (而非對等端對端) 的用戶端與伺服器通訊的。這是一個區域,其中 Web 遊戲大幅落後其他平台。不過,在我的經驗產生遊戲提供流暢的體驗,即使在網路延遲的 150ms,一般大陸美國和歐洲內。

用戶端預測此處,也請我依賴騎士執行外的推位置和時。騎士實作伺服器廣播的用戶端集合、 計算針對每個位置和方向,必要的修正,並套用更正以累加方式轉譯迴圈的進度。根據計算的向量差異,並將這項差異為增量單位分成多個會套用更正的位置。相較之下,方向的更正,會套用所計算的四元數的差異。更正四元數伺服器四元數geography_col和用戶端四元數qc由提供qΔ = geography_col o qc-1。可以表示為單一軸的旋轉,再分成該軸遞增此四元數。

遊戲的架構,然後,包含伺服器邏輯、 用戶端邏輯和共用的邏輯中所示圖 2。用戶端邏輯必須處理牽涉到使用者,包括輸入和轉譯工作集合的所有層面。伺服器邏輯相較之下,較簡單,但必須採取一些授權的步驟,像是接受連線,建立遊戲的物件,以及決定誰計分。

架構的多人連線遊戲

圖 2 的架構多人連線遊戲

共用的邏輯的實作,屬於最有趣的一部分,而且個別章節中會討論。它包含主要在授權伺服器上,必須執行,但也必須在每個用戶端上執行伺服器的廣播之間在遊戲進行遊戲邏輯。讓我們開始玩多人遊戲的具體細節: 通訊層。

用戶端伺服器通訊

授權伺服器的多人連線遊戲需要通訊程式碼。此程式碼有許多的責任。  一開始,它必須管理提到 TCP/IP 通訊埠接聽內送資料的伺服器。接下來,交握程序時沒有用戶端連線到伺服器。註冊新的用戶端在伺服器上,此信號交換,並建立專用的通訊端用戶端。驗證或配對程序,以及,可能需要某些遊戲。新的用戶端連線和中斷連接現有的用戶端,會影響遊戲。遊戲需要讓它可以建立新的用戶端的新車,並將播放程式指派給小組的通知。

遊戲當執行時,伺服器會傳送定期廣播到所有用戶端。播送內容描述遊戲的目前狀態。狀態包括位置、 速度、 方向 (四元數) 和角速度的每個遊戲的物件。此外,遊戲物件可能也有其他非實體的屬性,例如強度、 保護盾電源、 拼字逾時等。在這裡,伺服器必須是非常有效率,而且是絕對必要的資料少以傳送。例如,不應該廣播尚未移動或變更的物件。相反地,如果有剛剛所連接的新的播放程式,該播放程式需要啟動遊戲狀態的所有遊戲物件的完整描述。

最後,用戶端與伺服器通訊必須發出,並且擷取的頻外事件,描述遊戲的事件不符合標準的遊戲進度的過程。例如,伺服器可能會想要傳達給特定的播放程式他已達到特殊成就。這個機制也可用來報告一則訊息到所有播放程式。

以下是實作。在伺服器端,我會帶出 express node.js 伺服器和 socket.io 服務。顯示的程式碼圖 3設定 HTTP 伺服器在連接埠 3000、 初始化伺服器引擎和遊戲引擎類別,並將所有的 HTTP 要求路由到 dist 子目錄放置所有 HTML 和資產的位置。這表示 node.js 伺服器有兩個角色。在第一個角色中,它會做為提供 HTML 內容,包括 CSS 檔案、 影像、 音訊和 3D 模型 GLTF 格式的 HTML 伺服器。在第二個角色中,HTTP 伺服器將充當遊戲的伺服器可接受 socket.io 上的連入連線。請注意,最佳作法是將快取下效能良好的內容傳遞網路 (CDN) 背後的所有靜態資產。這項設定不在範例中顯示圖 3,但使用 CDN 大幅增加遊戲的顯示效能,同時從遊戲伺服器減少不必要的負載。最後,在程式碼的最後一行中,會啟動遊戲。

圖 3 伺服器進入點

const express = require('express'); 
const socketIO = require('socket.io'); 
 
// Constants 
const PORT = process.env.PORT || 3000; 
const INDEX = path.join(__dirname, './dist/index.html'); 
 
 
// Network servers 
const server = express(); 
const requestHandler = server.listen(PORT, () => console.log(`Listening on ${PORT}`)); 
const io = socketIO(requestHandler); 
 
 
// Get game classes 
const SLServerEngine = require('./src/server/SLServerEngine.js'); 
const SLGameEngine = require('./src/common/SLGameEngine.js'); 
 
 
// Create instances 
const gameEngine = new SLGameEngine({ traceLevel: 0 }); 
const serverEngine = 
  new SLServerEngine(io, gameEngine, { debug: {}, updateRate: 6, timeoutInterval: 20 }); 
 
 
// HTTP routes 
server.get('/', (req, res) => { res.sendFile(INDEX); }); 
server.use('/', express.static(path.join(__dirname, './dist/'))); 
 
// Start the game 
serverEngine.start();

伺服器已啟動,但尚未處理新的連接。基底 ServerEngine 類別會宣告稱為 onPlayerConnected 和對應的 onPlayerDisconnected 處理常式的新連線的處理常式方法。這是其中的授權伺服器的子類別方法可以指示要建立新的車上或移除現有的汽車的遊戲引擎的位置。中的程式碼片段圖 4從基底類別會顯示如何 socket.io 用來確保新的播放程式已連線時,會呼叫這些處理常式。

圖 4 連接處理常式實作與 socket.io

class ServerEngine {

  // The server engine constructor registers a listener on new connections
  constructor(io, gameEngine, options) {
      this.connectedPlayers = {};
      io.on('connection', this.onPlayerConnected.bind(this));
  }

  // Handle new player connection
  onPlayerConnected(socket) {
    let that = this;

    // Save player socket and state
    this.connectedPlayers[socket.id] = {
      socket: socket,
      state: 'new'
    };
    let playerId = socket.playerId = ++this.gameEngine.world.playerCount;
 
    // Save player join time, and emit a `playerJoined` event
    socket.joinTime = (new Date()).getTime();
    this.resetIdleTimeout(socket);
    let playerEvent = { id: socket.id, playerId, 
      joinTime: socket.joinTime, disconnectTime: 0 };
    this.gameEngine.emit('playerJoined', playerEvent);
    socket.emit('playerJoined', playerEvent);

    // Ensure a handler is called when the player disconnects
    socket.on('disconnect', function() {
      playerEvent.disconnectTime = (new Date()).getTime();
      that.onPlayerDisconnected(socket.id, playerId);
      that.gameEngine.emit('playerDisconnected', playerEvent);
    });


  }

  // Every server step starts here
  step() {

    // Run the game engine step
    this.gameEngine.step();

    // Broadcast game state to all players
    if (this.gameEngine.world.stepCount % this.options.updateRate === 0) {
      for (let socketId of Object.keys(this.connectedPlayers)) {
        this.connectedPlayers[socketId].socket.emit(
          'worldUpdate', this.serializeUpdate());
      }
    }
  }
}

當播放程式連線時,此程式碼就會發出 playerJoined 事件兩次。在遊戲的引擎事件控制站,遊戲程式碼的其他部分可能已註冊的此特定事件的接聽程式,就會發出第一個事件。第二個事件會透過播放程式的通訊端來傳送。在此情況下,事件可以在用戶端通訊端和動作的擷取,做為伺服器有加入遊戲此播放程式的認可。

請注意伺服器引擎的步驟方法: 以下,遊戲的狀態是廣播到所有已連線的播放程式。這個廣播只會在固定間隔發生。在遊戲中選取設定的排程,使每秒 60 的步驟執行,並且廣播傳送每個第六個步驟中,或 10 次每秒鐘。

現在我可以在子類別,實作的方法,因為它們會套用到遊戲特別。中所示圖 5,這段程式碼藉由建立新車,並加入該汽車小組藍色或紅色小組處理新的連接。當播放程式中斷連線時移除 media player 的汽車。

圖 5 伺服器連接處理

// Game-specific logic for player connections
onPlayerConnected(socket) {
  super.onPlayerConnected(socket);

  let makePlayerCar = (team) => {
    this.gameEngine.makeCar(socket.playerId, team);
  };

  // Handle client restart requests
  socket.on('requestRestart', makePlayerCar);
  socket.on('keepAlive', () => { this.resetIdleTimeout(socket); });
}

// Game-specific logic for player dis-connections
onPlayerDisconnected(socketId, playerId) {
  super.onPlayerDisconnected(socketId, playerId);
  this.gameEngine.removeCar(playerId);
}

通訊,也會發生的頻外,表示事件必須有時傳送至一個或多個用戶端以非同步的方式或資料必須傳送的不是遊戲物件狀態的一部分。下列範例會示範如何使用 socket.io,在伺服器端: 

// ServerEngine: send the event monsterAttack! with data 
// monsterData = {...} to all players
this.io.sockets.emit('monsterAttack!', monsterData);

// ServerEngine: send events to specific connected players
for (let socketId of Object.keys(this.connectedPlayers)) {
  let player = this.connectedPlayers[socketId];
  let playerId = player.socket.playerId;

  let message = `hello player ${playerId}`;
  player.socket.emit('secret', message);
}

在用戶端上聽候事件很簡單:

this.socket.on('monsterAttack!', (e) => {
  console.log(`run for your lives! ${e}`);
});

遊戲的邏輯

遊戲的基本邏輯牽涉到的使用者輸入 「 遊戲 」 狀態,應用程式中所示圖 6。例如,如果天然氣上按,應該可以套用轉寄直接的強制汽車。按下 [brakes 應該套用回溯導向的強制,汽車,可能會以反向方向。開啟決策滾輪適用於角速度,依此類推。

圖 6 GameEngine 步驟方法

// A single Game Engine Step
step() {
  super.step();

  // Car physics
  this.world.forEachObject((id, o) => {
    if (o.class === Car) {
      o.adjustCarMovement();
    }
  });

  // Check if we have a goal
  if (this.ball && this.arena) {

    // Check for ball in Goal 1
    if (this.arena.isObjInGoal1(this.ball)) {
      this.ball.showExplosion();
      this.resetBall();
      this.metaData.teams.red.score++;
    }

    // Check for ball in Goal 2
    if (this.arena.isObjInGoal2(this.ball)) {
      this.ball.showExplosion();
      this.resetBall();
      this.metaData.teams.blue.score++;
    }
  }
}

這些作業會實作為呼叫物理引擎,隨著時間過去。有趣的變化,是將正確的實體強制套用到汽車提供不佳的遊戲體驗。如果一樣可將真實世界中,您就會套用正確的實體強制,處理和控制的車輛不"也會覺得很 」 的播放程式。產生的遊戲動作是太慢。要有樂趣,我需要時要套用的人為提高 car 加速很低時從遊戲,否則遊戲的動作而太變慢。

最後,遊戲的邏輯必須檢查球是否傳遞目標的文章,並更新分數。

用戶端預測

每個多人連線遊戲有不同的需求,用戶端預測,而且必須跟著設定。遊戲的物件類型也可以有特定的設定。下列程式碼會顯示一小部分的遊戲的組態。使用者輸入會由三個步驟或 50 毫秒,以更符合在其中輸入將會套用在伺服器的時間延遲。這是設定為 3 delayInputCount。syncOptions.sync 設為"推斷 」 啟用用戶端預測;localObjBending 設為 0.6,指出,本機控制的物件位置應該先修正 60%,預估到達; 下一步] 伺服器廣播因為這些物件是比較可能分歧,而大幅設定 remoteObjBending 更高版本,為 80%。最後,我將 bendingIncrements 設定為 6,指出每個修正必須不套用,但而超過 6 為增量來轉譯迴圈:

const options = {
  delayInputCount: 3,
  syncOptions: {
    sync: 'extrapolate',
    localObjBending: 0.6,
    remoteObjBending: 0.8,
    bendingIncrements: 6
  }
};

球有它自己速度彎曲的參數 (未顯示) 設定為零。這是欄位的因為當一小組分數球取得 teleported 回中央,並嘗試彎曲的產生速度會導致不想要的視覺效果。

用戶端預測邏輯本身是太大,無法在這裡,包括,因此我為一般的虛擬程式碼中呈現它圖 7。它是推手,這可讓多人連線遊戲。第一個步驟掃描最後一個伺服器廣播,並檢查是否已建立的任何新物件。現有的物件之前它們正在同步處理伺服器狀態,請記住其目前狀態。第二個步驟是 reenactment 階段中,重新執行遊戲邏輯廣播中說明的步驟之後所發生的所有步驟。輸入需要重新套用,而且遊戲引擎步驟會在 reenactment 模式中執行。在第三個步驟中,每個物件會記錄必要的修正,並還原到記住的狀態。最後,在最後一個步驟中,可以移除標示為損毀的物件。

圖 7 簡化用戶端預測虛擬程式碼

applySync(lastSync):

  // Step 1: sync to all server objects in the broadcast
  for each obj in lastSync.objects
    localObj = getLocalObj(obj.id)
    if localObj
      localObj.rememberState()
      localObj.syncTo(obj)
    else
      addLocalObj(obj)

  // Step 2: re-enactment using latest state information
  for step = lastSync.serverStep; step < clientStep; step++
    processInputsForStep(step)
    gameEngine.step(reenactment=true)

  // Step 3: record bending and revert
  for each obj in world.obects
     obj.bendToCurrentState()
     obj.revertToRememberedState()

  // Step 4: remove destroyed objects
  for each obj in world.obects
    objSyncEvents = lastSync.getEvents(obj.id)
    for each event in objSyncEvents
      if event.name == ‘objectDestroy’
        gameEngine.removeObjectFromWorld(obj.id)

在詳細資料捲風

沒有在第一眼看起來比此遊戲中的更多工作。我沒觸及相機移動追蹤的車上、 在輸入中的行動裝置與桌面 (行動裝置的汽車移動由並排裝置) 和主題配對像之間的差異和偵錯。所有這些主題已超出本文的範圍。不過,務必提到的一些伴隨顯著的道路碰撞 Web 遊戲中。例如,Web 遊戲可以執行的桌面或平板電腦或行動裝置,因此有時 GPU 隨附,有時也不是,在低網路頻寬增加的可能性時。

還有另一項挑戰是從現有的標準產生相容的 3D 模型所需的工作流程。Web 平台遊戲開發工具組不是為成熟與其在其他平台上的對應項目。最後,Web 遊戲沒有本機儲存體中的其他平台提供的相同意義。所以必須小心 frugally 載入資產。此區域中的令人驚嘆成就是線上遊戲"都市 Galaxy 」 (bit.ly/2uf3iWB)。

這些問題向 Web 平台上的遊戲開發人員顯示新的挑戰。不過,有不同的優點必須提及的 Web 平台。在瀏覽器上的偵錯環境是非常進階。Web 程式設計的動態本質帶來非常快速的原型,並持續傳遞的修補程式。Web 開發人員社群為更大。

可能是 Web 遊戲的最重要的優點是它們正在啟動的方式。Web 遊戲只需按一下 Web 連結可以存取,而不需要預先在啟動之前,先下載完整的用戶端裝置上的遊戲資產。只有在有需求,請下載資產,遊戲體驗可以開始秒,而不是分鐘內,有可能,離線支援也可用來改善後續玩遊戲。

例如,登入 NBA 網站訪客可能會發現自己對從世界各地的玩家遊戲籃球,瀏覽器中以滑鼠右鍵。

開發遊戲的 web 技術

在過去幾年來,有已進展瀏覽器功能。主要前進是 WebGL,帶來 GPU 功能,在瀏覽器直接存取的開發。WebGL 是層級非常低的介面,因此遊戲開發人員通常依賴與文件庫來建置複雜的場景的 Babylon.js、 three.js 和 A-Frame 等較高層級的介面。

其他新的 Web 技術包括 WebAudio,提供音訊的進階的功能。媒體來源延伸模組 (MSE),其中最佳化視訊的存取,而且允許即時視訊的操作。和 WebVR,這是相當新,並提供虛擬實境 (VR) 的硬體,包括 VR 耳機和 VR 控制器的介面。使用搭配 WebAudio WebVR,很可能在網頁導向 VR 場景中實作 ambisonic 3D 音訊體驗。

JavaScript 語言已更新以有用的方式,使用最新的 ecma-262 第 6 版。JavaScript 生態系統很寬,為開發人員提供許多架構。事實上,有很多的架構和眾多有時會造成混淆的方法。

網路前,以及 HTTP 通訊協定上 WebSockets 和 WebRTC 是適用於應用程式的兩個標準。WebSockets 是簡單的雙向資料流,而 WebRTC 提供更複雜的端對端通訊的機制。進行多人連線遊戲開發,Lance.gg 是處理的網路功能和同步處理的遊戲物件的開放原始碼程式庫。

另一個有趣的技術是 WebAssembly,最近到達跨瀏覽器共識,並可能會提供大幅的效能優勢,方法是執行已編譯的瀏覽器中的二進位格式。

您可以輕鬆找到所有這些技術的相關網站上的詳細資訊。

總結

建置的 Web 平台的多人連線遊戲是不同的。Web 自然導向在各種裝置,這會相當大的挑戰,若要開發遊戲。在瀏覽器中的簡單 UDP 網路缺少影響多人連線的回應時間。不過,近來 Web 技術具有可在撰寫多人連線遊戲物理示範相對較短的時間。程式庫和工作流程在此練習中使用會進行快速的進度,而且已經使用今天。與其他資源和時間,很可能在開發高品質遊戲體驗的瀏覽器中。

我想要感謝遊戲 Opher Vishnia 文章中討論的共同作者騎士和多元化 creator 涉及遊戲開發、 程式設計、 設計和音樂組合的共同創辦。


Gary Weiss是軟體架構設計人員和開發的開放原始碼專案 Lance.gg 的群組,騎士的共同創辦。連線到他處garyweiss74@gmail.com

非常感謝下列 Microsoft 技術專家檢閱這篇文章:  Raanan Weber


MSDN Magazine 論壇中的這篇文章的討論