淺談物理引擎的網路同步方案!

Frozen Claw發表於2021-01-21
0x00 前言

本期文章的寫作動機是最近碰到的一個問題:在一款線上對戰FPS遊戲中新增一個多人一起踢球的模式。本以為這個問題解決起來很簡單,稍微研究了一會才發現坑還不小。最終花費了好一段時間才初步做出了還不錯的效果,於是決定寫這篇文章記錄一下,拋磚引玉,供大家參考。

0x01 問題描述

我們要實現的效果可以抽象如下:一個場景中存在一個球形剛體(RigidBody)和多個線上玩家,每個玩家都能觸碰到球體,並對球施加作用力。由於涉及核心玩法,必須保證每個玩家看到的球體位置與朝向(即Unity中的Transform)同步。

0x02 狀態同步初探

說起網路同步,自然會想起經典的各種幀同步與狀態同步。

我們先來說狀態同步,狀態同步的思想是客戶端將輸入上傳到伺服器,由伺服器計算出結果,再將狀態廣播給所有客戶端,客戶端使用獲得的狀態在本地更新渲染資料。原始的狀態同步可以保證每個客戶端上獲得的結果都是相同的,這能夠滿足我們的需求,但也存在一些問題:

  • 伺服器為了節省頻寬,通常狀態同步的頻率不會很高(<每秒30次),造成物體的移動在視覺上不平滑
  • 網路環境不穩定,出現丟包或卡頓時,物體的移動會不連續
  • 輸入延遲較大,玩家的操作需要等待一個來回才會反映到螢幕上

淺談物理引擎的網路同步方案!


左:客戶端 右:服務端 同步頻率:每秒10次

其中1可以通過內插值解決,2適用於外插值,3的經典解決方法則是客戶端預測+狀態矯正。

0x03 內插值(Interpolation)平滑

在收到的兩個資料包間通過線性插值插入過渡資料,可以有效平滑物體移動的視覺效果。具體情況如下:

  • 僅需要同步位置(12位元組)和旋轉(16位元組)資料
  • 不立刻應用收到的狀態資料,而是多等待一個資料包
  • 根據時間差在過去的兩個狀態資料間線性插值
  • 對於四元數表示的旋轉資料,使用球面插值(Slerp)而不是普通插值(Lerp)
  • 客戶端操作 -> 伺服器計算 -> 回傳狀態之後才會開始運動,引入2倍ping值的延遲

淺談物理引擎的網路同步方案!


左:客戶端 右:服務端 同步頻率:每秒10次 線性內插

對於高速反彈等大幅改變運動狀態的情況的模擬會出現一些問題

淺談物理引擎的網路同步方案!
一次快速折返被忽略了

  • 表現效果與通訊頻率強相關,頻率越高效果越高,但也會消耗很多網路頻寬
  • 如果出現網路波動連續丟包或者間隔太久,會停在半空中
  • 可以根據收包間隔時間在內插和外插間切換,太久沒收到新包,則用外插繼續模擬


0x04 外插值(Extrapolation)推測

使用內插值同步時,物體的運動始終落後於伺服器兩個發包延遲,引入外插值推測,可以將延遲降低到一個發包延遲。外插值的基本思想是,每次收到伺服器的狀態包,立即應用狀態,並使用該狀態的速度資料直接預測下一步的軌跡。具體情況如下:

  • 需要在每次同步的資料包中加入線速度(12位元組)
  • 立刻應用收到的狀態資料包
  • 使用線速度隨時間推測位置
  • 可以使用諸如航跡推算(Dead Reckoning)等演算法優化推測結果
  • 在不發生碰撞的情況下,外推效果很好
  • 一旦發生碰撞,由於不知道碰撞另一方的資訊,僅憑自身資料推測結果完全是錯的
  • 給靜止物體施力,啟動延遲依然存在


淺談物理引擎的網路同步方案!
延遲降低了,但在碰撞時出現了穿牆

可以在本地也跑一個物理引擎,讓物理引擎來預測位置,並用狀態資料不斷修正

淺談物理引擎的網路同步方案!


本地執行引擎+不斷同步資料,效果不錯

看到這裡,想必聰明的你又雙叒發現問題了:既然本地也能跑物理引擎,那直接用物理引擎算不就完了嗎,還同步個什麼呢?誒,先別急,這個問題就是我們將要面對的第一個關鍵問題。

0x05 蝴蝶效應

為何需要對物理模擬進行網路同步呢?因為在多人遊戲中,其他玩家的位置是延遲的。本地玩家的位置通常由本機計算,與玩家的操作保持同步;但本地看到的其他玩家皆是由伺服器轉發過來的2個ping值前的位置。考慮這麼一種情況,有一個球在直線前進,本地玩家從下往上嘗試碰球,由於本地位置領先,本地玩家觸碰到了球,而在伺服器上則還沒碰到

淺談物理引擎的網路同步方案!

本地觸碰球后,球發生反彈改變方向,而伺服器上待1P玩家到位時,球已經通過,沒有發生碰撞,於是失去同步。

淺談物理引擎的網路同步方案!

這便是第一個需要同步的理由:由於網路延遲的存在,各端的狀態是不完全相同的,而剛體會碰撞、反彈,這會讓任何細小的差異迅速放大,進而失去同步。

除此之外,物理引擎還存在一個特有的問題,那就是物理模擬具有不確定性。

0x05 物理引擎的不確定性

這裡我用Unity做一個簡單的實驗,在一個碰撞場景中,記錄所有剛體的位置、旋轉、速度、角速度,給一個大球新增一個Impulse去碰撞許多小方塊,隨後重置場景並重復這一過程。

淺談物理引擎的網路同步方案!
每次重放結果都不同

可以看出,雖然狀態相同,但模擬的結果卻差別很大,這是因為Unity中預設並未開啟PhysX的增強確定性模式。在專案設定中開啟Enable Enhanced Determinism選項,PhysX可以保證在同一平臺、同一優化配置(Debug/Release)、同一編譯器、同一時序、同樣步進間隔下的確定性,但若是涉及跨平臺,則需要另請高明瞭。

淺談物理引擎的網路同步方案!
開啟增強確定性的開關後,能夠實現重放

原來,現行的浮點數標準是IEEE754,但該標準只規定了應該怎麼儲存,具體的運算規則(包括舍入、擴充套件、NaN的處理等)並不包括在標準內。因此,不同的指令集(Arm與x86)對浮點數運算的操作存在細微的差異。多次運算後,微小的差異也會不斷累積,導致最後剛體的執行軌跡南轅北轍。

物理引擎的不確定性問題最直接的影響就是沒法用lockstep鎖幀同步了,因為狀態同步可以不斷用伺服器資料對剛體位置進行矯正,鎖幀同步下即使我們能夠在多端同步完全相同的操作,不確定性也會讓各端物理引擎的模擬結果天差地別。

消除恐懼的最好辦法就是面對恐懼,我們可以用下列方法強行解決不確定性問題:

使用基於定點數的確定性物理引擎,例如下面這個開源引擎

https://github.com/sam-vdp/bepuphysics1intgithub.com/sam-vdp/bepuphysics1int

保證模擬的時序和間隔,即保證對剛體的操作順序,以及將步進模擬的操作放在FixedUpdate這樣固定執行間隔的函式中
當然也需要付出一些代價:

  • 定點數引擎的效能消耗與速度要慢於原版
  • 修改引擎的工作量比較大,而且主流的開源引擎(PhysX、Bullet等)都找不到現成的定點數版本,得自己動手
  • 場景資料全部要重新導一遍,轉為定點數
  • 使用定點數表示浮點數,數值範圍有限,上面的BEPU引擎只能處理座標在1000內的物體


0x06 站在G胖的肩膀上

光有理論知識還不夠,參考現有的遊戲能夠幫助我們少走許多彎路。在網上分享這部分技術的遊戲並不多,之前我玩過基於Source引擎製作的CS:Source和CS:GO,這兩款遊戲中都有實現多人踢球遊戲模式,於是我在G胖的開發者社群中搜尋了一下,居然還真找到了關於物理與同步的描述:

Physics Entities on Server & Client
developer.valvesoftware.com/wiki/Physics_Entities_on_Server_%26_Client

A major feature of the Source Engine is the physical simulation of rigid bodies. This simulation implements mostly mechanical and Newtonian physics like gravity, trajectory, friction, collisions, springs and damping. Models have to support this simulation by providing information about their collision model, material type, weight etc. In single player mode all physics entities are controlled and simulated by the server (server-side physics) and networked to the client. In multiplayer mode smaller objects like cans or bottles that don't affect gameplay are completely simulated client-side and are therefore not synchronized between clients. This is necessary because moving physics entities generate significant network traffic since they can change their position and orientation with every frame. Networking these updates would almost stall any connection as soon as lots of physics object start to move at the same time (explosions, etc). Client-side physics objects don't affect player movement, and they should always be significantly smaller than players so that a player can not hide behind the objects. When destroying server-side breakable objects, they break apart into smaller client-side simulated fragments.

大概意思是,Source引擎提供了兩種物理物件,一種是由服務端完全控制,一種則僅本地客戶端執行。服務端物理實體所有的模擬和操作都由伺服器控制,客戶端只做渲染表現,在多人遊戲中會受延遲影響;僅本地物體實體通常用於細小的物體,例如瓶子、花盆等不會影響遊戲的物體,這些物體的物理計算僅由本地模擬,而且不會與其他玩家進行同步。此類物體不會與玩家碰撞,玩家也無法站在它們上面,因此不會影響引擎中基於預測、插值、延遲補償的狀態同步。

這個方案跟前面設想的差不多,看來的確是一種可行的方案。我在CSGO中測試了一下效果,de_dust2地圖中T家後面就有一個足球形狀prop_physics實體,在高延遲下對球進行攻擊,的確存在較高的啟動延遲,與開發者文件描述相符。

此方案的效果比較依賴客戶端與服務端的通訊速率,達10次/秒時就基本可用,30次/秒時效果就很不錯了。雖然在高延遲下,物體狀態改變的延遲肉眼可見,不過這種情況下玩家也有心理準備,加上物理玩法並不是核心玩法,這樣的效果可以接受。

0x07 火箭聯盟的全預測方案

最終我們的專案採用了上面的伺服器模擬+客戶端插值渲染的方案,不過找到了一些別的資料,在此也分享出來。首先是GDC2018上火箭聯盟做的演講:

火箭聯盟物理系統、網路同步講解(含字幕)】It IS Rocket Science!The Physics of Rocket League Detailed_

  • 火箭聯盟是一個以多人載具踢球為核心玩法的遊戲,因此物理同步自然是他們要攻破的關鍵技術問題。
  • 採用了開源的Bullet物理引擎,這樣可以自己修改和定製整個流程,而不像Unity中的PhysX是個黑箱。
  • 同步方案上,採用了類似守望先鋒的關鍵幀同步方案,客戶端和服務端各自執行物理引擎,客戶端不等待伺服器資料而是一路向前執行。
  • 客戶端維護一個輸入緩衝區,儲存過去一段時間的每幀的輸入資料。待收到伺服器資料包後,按照幀號與緩衝區中對應的本地資料進行對比,判斷本地在該幀的計算是否正確
  • 如果對比失敗,說明預測失敗,客戶端狀態回滾到收到資料包的幀號,應用伺服器資料,然後連續執行多次物理引擎模擬,直到追到當前時間,完成一次矯正
  • 物理模擬部分,可以單獨拿一個新引擎專門做需要同步物體的模擬,融入現有專案不會太難


這種方案的效果也是非常不錯的,付出的代價僅僅是在預測錯誤時需要在一幀內回滾並消耗CPU連續執行多次模擬,對於PC/主機遊戲完全可以接受。不過使用該方案也意味著需要重新部署一套支援確定性模擬的物理引擎,工程量上相對會大一些。

0x08 分散式授權方案

除了客戶端預測方案外,筆者還找到了一篇應用在VR多人遊戲上的物理同步資料:

https://gafferongames.com/post/networked_physics_in_virtual_reality/gafferongames.com/post/networked_physics_in_virtual_reality/

這篇文章設計了一個針對VR遊戲特化的物理同步方案,具有以下特點:

  • VR遊戲對幀數要求很高,通常在90Hz左右,並且由玩家手動操作,對延遲敏感,不適用單一服務端主控客戶端表現的方案
  • 使用預測/回滾方案,對CPU壓力較大,因為幀數高,每次回滾時追幀數量多
  • 不是強PVP遊戲,所以可以容忍作弊,也沒什麼人作弊
  • 採用分散式授權方案,哪個玩家拿到球,哪個玩家就獲得球的所有權變成物理主控端
  • 其他玩家像伺服器主控方案一樣渲染球
  • 需要處理好多個玩家競爭球權的情況
  • 可以實現本地幾乎零操作延遲


該方案以容忍作弊+消耗更多的客戶端頻寬為代價換取了超低的本地互動延遲,對VR遊戲這種線上人數少、客戶端配置高、延遲敏感的專用場景來說自然是非常適合的,而且也不依賴於確定性物理引擎。但若是要考慮反作弊,或是手遊等場景,可能該方案就不太合身了。

0x09 總結

由於浮點數精度問題,凡是涉及到跨平臺同步的時候,浮點數就都會變得不可靠,再疊加上物理引擎的時序等問題,看上去很簡單的物理同步就變得異常難處理。Unity似乎也知道有這個問題存在,計劃在未來加入跨平臺確定性物理引擎的官方支援,不過目前仍然是“在做了”的狀態,正式釋出遙遙無期。因此,目前我們還是處在需要自力更生的狀態,希望這篇文章能夠帶給大家一些啟發和幫助吧,也歡迎大家在評論區提出意見和交流,我們下期再見。

作者:Frozen Claw
來源:遊戲蠻牛
地址:https://mp.weixin.qq.com/s/6WisM1FJ62DGVWVSHKvAlQ

相關文章