【搬運】多人遊戲中的幀同步策略


3樓貓 發佈時間:2024-03-20 21:33:16 作者:木晗 Language

多人遊戲的運作方式

遊戲程序當前的狀態隨時間和玩家的輸入會進行變化。也就是說遊戲是有狀態的程序。多人遊戲也不例外,但由於多人玩家之間存在交互,複雜性會更高。
例如貪吃蛇遊戲,我們假設它的操作會發送到服務器,那它的核心遊戲邏輯應該是:
  1. 客戶端讀取用戶輸入改變蛇的方向,也可以沒有輸入,然後發送給服務端
  2. 服務端接收消息,根據消息改變蛇的方向,將蛇的“頭”移動一個單位空間
  3. 服務端檢查蛇是否撞到了牆壁或者自己,如果撞到了遊戲結束,給客戶端發送響應消息,更新客戶端的畫面。如果沒有撞到,則繼續接收客戶端發送的消息,同時也要響應給客戶端消息,告訴客戶端,蛇目前的狀態。
服務端接收該消息做出對應的動作,這個過程會以固定的間隔運行。每一次循環都被稱為 frame 或 tick。
客戶端將解析服務端發送的消息,也就是每一幀的動作,渲染到遊戲華中中。

鎖步狀態更新

為了確保所有客戶端都同步幀,最簡單的方法是讓客戶端以固定的間隔向服務器發送更新。發送的消息包含用戶的輸入,當然也可以發送 no user input。
服務器收集“所有用戶”的輸入後,就可以生成下一次 frame 幀。

上圖演示了客戶端與服務端的交互過程。T0 ~ T1 時間段,客戶端保持等待,或者說空閒狀態,直到服務器響應 frame,等待時間的大小取決於網絡質量,約 50 毫秒到 500 毫秒,人眼能夠注意到任何超過 100 毫秒的延遲,因此這個等待時間對於某些遊戲來說是不可接受的。
鎖步狀態更新,還有一個問題。遊戲的延遲來自最慢的用戶

上圖有兩個客戶端。客戶端 B 的網絡比較差,A 和 B 都在 T0 時間點向服務器發送了用戶輸入,A 的請求在 T1 到達服務端,B 的請求在 T2 到達服務端,前面我們提到,服務器需要收集“所有用戶”的請求後才開始工作,因此需要到 T2 時間點才開始生成 frame。
因為 Client B 比較慢,我們“懲罰”了所有的玩家。
假如我們不等待所有客戶端的用戶輸入,低延遲玩家又會獲得優勢,因為它的輸入到達服務器的時間更短,會更快處理。例如,兩個玩家 A、B 同時互相射擊預期是同時死亡,但是 A 玩家延遲比 B 玩家更低,因此在處理 B 玩家的用戶輸入時,A 玩家已經幹掉 B 玩家了。
小結一下,鎖步狀態更新存在的問題,如下。
  • 遊戲畫面是否卡頓,取決於最慢的玩家
  • 客戶端需要等待來自服務器的響應,否則不會渲染畫面
  • 連接非常活躍,客戶端需要定期發送一些無用的心跳包,以便服務器可以確定它擁有生成 frame 所需的所有信息
回合制類型的遊戲大多數使用這種方法,因為玩家確實需要等待,例如《爐石戰記》。
對於慢節奏的遊戲,少量延遲也是可以接受的,例如《QQ農場》。
但是對於快節奏的遊戲,鎖步狀態更新的這些問題都是致命的,不可能操縱遊戲人物進入某一個建築,500 毫秒後,我才能進入。我們一起來看看下一種方法。

客戶端預測

客戶端預測,在玩家的計算機上,運行遊戲邏輯,來模擬遊戲的行為,而不是等待服務器更新。
例如我們生成 Tn 時間點的遊戲狀態,我們需要 Tn-1 時間點的所有玩家狀態和 Tn-1 時間點所有玩家的輸入。
假設,我們現在的固定頻率為 1 s,每 1s 需要給服務器發送一個請求,獲取玩家狀態並更新玩家的狀態。

在 T0 時間點,客戶端將用戶的輸入發送到服務器,用於獲取 T1 時間點的遊戲狀態。在 T1 時間點,客戶端已經可以渲染畫面了,實際上客戶端的響應是在 T3 時刻,也就是說客戶端沒有等待來自服務器的響應。
使用這個方法,需要滿足一些前置條件:
  • 客戶端擁有遊戲運行邏輯所需的所有條件
  • 玩家狀態的更新邏輯是確定性的,即沒有隨機性,或者可以以某種方式保證確定性,例如客戶端和服務器使用同樣的公式以及隨機種子,可以保證具有隨機性的同時,產生的結果具有確定性。這樣保證了客戶端和服務器在給定相同輸入的情況下產生相同的遊戲狀態
滿足這兩點,客戶端預測的結果也不一定總是對的。就比如剛提到的,使用相同的公式以及相同的隨機種子,進行偽隨機算法,但不同平臺的浮點計算,可能會存在微小的差異。再設想一個場景,如下圖。

客戶端 A 嘗試使用 T0 時間點的信息模擬 T1 時間點上的遊戲狀態,但客戶端 B 也在 T0 時間點提交了用戶輸入,客戶端 A 並不知道這個用戶輸入。
這意味著客戶端 A 對 T1 時間的預測將是錯誤的是,但!由於客戶端 A 仍然從服務器接收 T1 時間點的狀態,因此客戶端有機會在 T3 時間點修正錯誤。
客戶端需要知道,自己的預測是否正確,以及如何修正錯誤。
修正錯誤通常叫做 Reconcilation 和解。
需要根據上下文來實現和解部分,下面我們通過一個簡單的例子來理解這個概念。這個例子只是拋棄我們的預測,並將其遊戲狀態替換為服務器響應的正確狀態。
  • 客戶端需要維護 2 個緩衝區,一個用於預測 PredictionBuffer,一個用於用戶輸入 InputBuffer 。它們是預測這個行為需要的上下文,請記住,預測 Tn 時刻,需要 Tn-1 的狀態和 Tn-1 時刻的用戶輸入。它們一開始都為空

  • 玩家點擊鼠標,移動遊戲角色到下一個位置。此時,玩家輸入的移動信息 Input 0 存儲在 InputBuffer 中,客戶端將生成預測 Prediction 1,存儲在 PredictionBuffer 中,預測將展示在玩家畫面中

  • 客戶端收到服務器響應的 State0 ,發現與客戶端的預測不匹配,我們將Prediction 1 替換為 State 0,並使用 Input 0 和 State 0 重新計算,得到 Prediction 2,這個重新計算的過程,就是 Reconcilation 和解

  • 和解後,我們從緩衝區中刪除 State 0 和 Input 0

這種和解的方式有一個明顯的缺點,如果服務器響應的遊戲狀態和客戶端預測差異太大,則遊戲畫面可能會出現錯誤。例如我們預測敵人在 T0 時間點向南移動,但在 T3 時間點,我們意識到它在向北移動,然後通過使用服務器的響應進行和解,敵人將從北“飛到”正確的位置。
有一些方法可以解決此問題,這裡不展開討論,感興趣可以搜一下實體插值 Entity Interpolation。
小結一下,客戶端預測技術,讓客戶端以自己的更新頻率運行,與服務器的更新頻率無關,所以服務器如果出現阻塞,不會影響客戶端的幀。
但它也帶來複雜性,如下。
  • 需要在客戶端處理更多的狀態和邏輯,比如我們前面提到的緩衝區和預測邏輯
  • 需要和解來自服務器的狀態(正確的遊戲狀態)與預測之前的衝突
還給我們帶來了敵人從南飛到北的問題。
目前為止,我們都在討論客戶端,接下來看看服務端如何解決幀同步。

服務端和解

利用服務端解決幀同步問題,首先需要解決的是網絡延遲帶來的問題。如下圖。

用戶 A 在 T 處進行了操作(比如按下了一個技能鍵),該操作應該在 T+20ms 處理,但由於延遲,服務器在 T+120ms 才接收到輸入。
在遊戲中,用戶做出指定操作後,應該立即有反應。立即有反應,這個立即是多久,取決於遊戲的類型,比如之前我們提到的回合制,它的立即可能是幾十秒。我們可以通過 T + X,表示立即反應的時間,T 代表用戶的輸入時刻,X 代表的是延遲。X 可以為 0,這代表真正的立即 :-)
解決這個問題的思路,與之前客戶端預測中使用的辦法類似,就是通過客戶端的用戶輸入,來和解服務器中的玩家遊戲狀態。
所有的用戶輸入,都需要時間戳進行標記,該時間戳用於告訴服務器,什麼時刻處理此用戶輸入。

為什麼在同一水平線上,Client A 的時間是 Time X,而 Server 的時間是 Time Y?
因為客戶端和服務端獨立運行,通常時間會有所不同,在多人遊戲中,我們可以特殊處理其中的差異。在特殊處理時,我們應該使客戶端的時間大於服務端的時間,因為這樣可以存在更大的靈活性
上圖演示了一個客戶端與服務端之間的交互。
  1. 客戶端發送帶有時間戳的輸入。客戶端告訴服務器在 X 時間點應該發生用戶輸入的效果
  2. 服務端在 Y 時間點收到請求
  3. 在 Y+1 時間點,即紅色框的地方,服務端開始和解,服務端將 X 時間點的用戶輸入應用於最新的遊戲狀態,以保證 X 的 Input 發生在 X 時間點
  4. 服務端發送響應,該響應中包含時間戳
服務端和解部分(上圖紅色底色部分),主要維護 3 個部分,如下。
  • GameStateHistory,在一定時間範圍內玩家在遊戲中的狀態
  • ProcessedUserInput,在一定時間範圍內處理的用戶輸入的歷史記錄
  • UnprocessedUserInput,已收到但未處理的用戶輸入,也是在一定的時間內
服務端和解過程,如下。
  1. 當服務端收到來自用戶的輸入時,首先將其放入 UnprocessedUserInput 中
  2. 等待服務端開始同步幀,檢查 UnprocessedUserInput 中是否存在任何早於當前幀的用戶輸入
  3. 如果沒有,只需要將最新的 GameState 更新為當前用戶的輸入,並執行遊戲邏輯,然後廣播到客戶端
  4. 如果有,則表示之前生成的某些遊戲狀態由於缺少部分用戶輸入而出錯,需要和解,也就是更正。首先需要找到最早的,未處理的用戶輸入,假設它在時間 N 上,我們需要從 GameStateHistory 中獲取時間 N 對應的 GameState 以及從 ProcessedUserInput 獲取時間 N 上用戶的輸入
  5. 使用這 3 條數據,就可以創建一個準確的遊戲狀態,然後將未處理的輸入 N 移動到 ProcessingUserInput,用於之後的和解
  6. 更新 GameStateHistory 中的遊戲狀態
  7. 重複步驟 4 ~ 6,直到從 N 的時間點到最新的遊戲狀態
  8. 服務端將最新幀廣播給所有玩家
我並沒有做過這些工作,分享的知識都是我對它感興趣,在網上看了許多經驗後整理的。

轉自騰訊開發者社區:多人遊戲中的幀同步策略


© 2022 3樓貓 下載APP 站點地圖 廣告合作:asmrly666@gmail.com