瞭解多人遊戲下的使用者端與伺服器體系結構

2023-08-22 09:00:51

直連

image-20230821204552758

直連模式下,選擇一個玩家充當伺服器(房主)。如果遊戲出現不同步,那麼均按房主的世界來,玩家1可以作弊修改其遊戲來影響其他玩家的世界

針對兩個玩家來說,直連連線質量更好,延遲小

如果玩家數量很多,不同玩家間的通訊則需要靠房主為中介,那通訊質量與房主主機設定、網路情況有很大關係


專用伺服器

image-20230821205140980

所有玩家與專用伺服器通訊,專用伺服器通常執行沒有遊戲介面的遊戲程式碼的修改版本,因此可以在設定較低的計算機上執行,還可以檢測作弊,伺服器維護的遊戲世界是最權威的


① 使用者端和伺服器

使用者端全聽伺服器的

遊戲狀態由伺服器單獨管理,使用者端只把操作(按鍵,指令)傳送給伺服器端,伺服器定期更新遊戲狀態,將新的狀態傳送給使用者端,使用者端只需要在螢幕上渲染即可

image-20230821232557753

可以預防大範圍作弊

  • 玩家本地修改生命值為99999,伺服器那依舊是10,玩家依舊會收到死亡事件訊息
  • 玩家本地修改位置,伺服器處理玩家向右移動一個單位,玩家位置被同步修復

應用舉例:慢節奏遊戲,如策略或卡牌


延遲問題

網路資料傳輸要經過大量路由器,還可能遇到網路擁塞情況,遠距離傳輸延遲可能會很高(100~500ms)

玩家傳送一個向右移動一格指令,花費了100ms傳到伺服器,伺服器處理狀態,再花費100ms將狀態傳給玩家

在玩家看來,按下了右鍵後有0.2秒的時間遊戲沒有任何反應,然後角色才向右移動一格,這是能明顯感知到的卡頓,嚴重時無法玩家無法進行遊戲


② 使用者端預測和伺服器對賬

使用者端預測

大部分玩家的輸入都是按預期效果執行的。 輸入 = 操作 = 行為 = 動作 = 滑鼠點選移動,鍵盤按下釋放等

如果給定遊戲狀態和輸入操作,使用者端能完全預測遊戲世界的變化(跟伺服器一樣的處理),且只有唯一一種結果(絕對性)

我們可以將輸入操作傳送給伺服器端,先不等伺服器端返回,在使用者端立即生效預測的結果,這種方式消除了輸入操作和狀態變化之間的延遲,而且使用者端預測的結果大多是正確的,能跟伺服器返回的結果相匹配。

假設有100ms的網路延遲(往返),移動位置的動畫需要100ms,整個操作需要200ms,這是方案①導致的結果

image-20230822000158999

使用方案②後,使用者端檢測到輸入預測玩家位置並更新,動畫播放和網路請求同步進行,原來的200ms現在只需要100ms。伺服器返回的結果與使用者端預測的結果一致。注:伺服器依舊是權威的,維護著所有玩家真實的狀態

image-20230822000419011

同步問題

假設有250ms的網路延遲,玩家按了兩次右鍵,向右移動兩次,使用者端立即模擬,間隔地向伺服器傳送兩次"向右移動"操作

兩次移動均完成後過50ms才收到伺服器傳來的第一次「向右移動」操作的結果,此時出現狀態衝突

由於伺服器權威性,使用者端根據「真實的狀態」重新設定了角色位置(角色居然跳回去了),然後又跳回來

image-20230822000815170

伺服器對賬

對賬:會計先做個手工表,每算一會把系統算的表拿出來核對,如果不對就糾正手工表,然後接著算

要修復同步問題需要知道伺服器可能沒有處理完使用者端的所有輸入,使用者端的狀態是現在時,而伺服器傳來的狀態是過去某一時刻的,兩者有一定的時間差

可以為使用者端的每個輸入新增一個數位標記,按照輸入的順序遞增這個標記。使用者端傳送的兩個輸入分別被標記了#1、#2,且儲存了各自的副本。下面的例子演示了一個使用者端和一個伺服器端的網路互動

伺服器每發來一個真實狀態,使用者端就重新模擬一次:當t=250ms,使用者端接收到伺服器對#1輸入的結果時,使用者端丟棄#1輸入的副本,並在#1的結果上根據#2副本重新計算當前遊戲狀態。計算後與當前狀態對比,如果有差異則重新設定

image-20230822002124542

當t=350ms,使用者端接收到伺服器對#2輸入的結果時,丟棄#2輸入的副本,此時因為輸入副本佇列為空,不需要重新模擬。只需要將結果與當前狀態對比,如果有差異則重新設定

應用:在回合制戰鬥中,玩家A攻擊另一個角色B時,可以優先顯示血液效果和造成傷害的數位,但不應該在伺服器返回結果前更新角色健康狀態(不是絕對的,可能有多種結果,如這一擊是否把角色B打斷了腿,或者是角色B使用醫療包的事件比攻擊早)

不容易逆轉:如果A可以預測並更新健康狀態、角色B打醫療包的事件又在A攻擊之前,此時A使用者端還沒有受到角色B健康狀態的更新,A使用者端預測B生命值降至0,觸發角色死亡事件(可能銷燬了這個實體),而實際上角色還在伺服器那活得好好的,那還原前一步就十分複雜了

使用者端預測的結果與伺服器預測的結果不匹配問題,在多個使用者端情況下經常發生


ANTI WEB SPIDER BOT www.cnblogs.com/linxiaoxu

③ 實體插值(平滑插值)

伺服器時間步長

考慮方案②,當大量使用者端接入一個伺服器時,伺服器將收到大量的操作(按鍵、滑鼠)輸入,每接收一個輸入就要更新當前世界並廣播遊戲狀態,需要消耗大量的CPU和頻寬

最好的辦法是整個遊戲世界以低頻率週期性地更新,例如每秒10次,每次更新延遲為100ms,稱為時間步長

把接收到的輸入全部放入佇列中,不做處理(個人認為接收到輸入直接處理也行)。每100ms,處理佇列,將更新後的狀態廣播給每個使用者端

在這種情況下,遊戲世界以可預測的速率獨立於使用者端輸入的存在、輸入數量進行更新


航位推算

航位推算是通過使用先前確定的位置或定位,並結合對速度、航向(或方向或航向)和經過時間的估計,來計算移動物體的當前位置的過程。

多人模式下,在實體方向和速度都會立即改變的情況下,航位推算的結果是不準確的。比如多人賽車,伺服器每100ms將其他車輛的位置、速度、方向狀態傳遞給使用者端,使用者端僅能根據這些資訊來模擬賽車運動100ms,最後模擬得到的位置跟伺服器下一次發來的位置有較大的差別,位置糾正,汽車瞬移。

詳細的說(省略了對賬過程):

  • 有玩家A(0,0)跟玩家B(0,0),伺服器C
  • C告訴A:B正在往左移動,速度100m/s,位置是(0,0)
  • C告訴B:A現在沒移動,位置是(0,0)
  • 50ms後中B開始(往右移動,速度瞬間變成1000m/s);這兩個事件被加入了C的佇列
  • 又過了50ms,到現在 t = 100ms
  • B在A上渲染的位置(-10,0)
  • B自己渲染的位置(-5+50,0)
  • C處理佇列,將狀態傳送給A跟B
  • C告訴A:B正在往右移動,速度1000m/s,位置是(45,0)
  • C告訴B:A現在沒移動,位置是(0,0)
  • A發現伺服器給的B位置與自己預測的位置有出路
  • A修復B的位置,B被瞬移了

玩家自身跟伺服器通訊是不會出現這種問題的,因為玩家操作的實體是實時的,沒有延遲;其他玩家不是實時的,同步資料都是伺服器給的,即其他實體相關資訊的一種稀疏性


實體插值

由於玩家的方向和速度都會立即改變,航位推算無法應用。如FPS,玩家通常以非常高的速度奔跑、蹲比和轉彎,這使得航位推算毫無用處,因為無法再根據之前的狀態準確地預測位置和速度。

為了給玩家帶來連續性和流暢移動的錯覺,採用一種巧妙的做法

每個玩家本身是現在時,而其他玩家都是過去式:玩家自己是實時的,而看到的其他玩家都是他們過去某一時刻的狀態。

將伺服器最新發來的狀態記P1,前一個時間步長的狀態記P2(舊狀態)使用者端在本地,那接下來一個時間步長內整個世界狀態將線性從P2變為P1

也就是說,你比其他所有人都快了一個時間步長,其他人比你都慢一拍

下圖很好解釋了線性插值,v=(11.75,10)意味著使用者端2在收到P1後已經過了75ms(時間步長100ms,線性插值的步長也是100ms),當時間過了100ms,使用者端2的世界狀態將完全變為P1,此時可能已經有了新的P1或者還沒收到P1,我們可以根據網路延遲動態修改線性插值的步長

image-20230822020146078

目前的功能

  • 使用者端在本地傳送輸入並模擬效果
  • 伺服器從所有使用者端獲取帶有時間戳的輸入
  • 伺服器處理輸入並更新世界狀態
  • 伺服器向所有使用者端傳送伺服器世界狀態的快照
  • 使用者端接收伺服器發來的世界狀態更新
    • 根據這個狀態與沒被伺服器處理的輸入 重新模擬
    • 對其他實體狀態進行線性插值

④ 延遲補償

對時間和空間敏感的事件來說,比如射擊事件,當玩家向另一名玩家射擊時,由於其他玩家都是過去的玩家,所以你的瞄準延遲為100毫秒,你在對100毫秒延遲之前的敵人射擊。

通用解決方法 伺服器根據射擊事件的時間戳,重建該時間戳時的世界狀態,可以準確地知道你開槍的那一刻準星瞄準的實體

但由於是過去式,在敵人看來,100ms之後可能已經移動到掩體之後,卻依舊被爆頭了,不過這個解決方案已經很不錯了


⑤ 影格同步化

影格同步化

該部分還沒講全,未來某天補上程式碼

狀態同步講完,接下來講主流同步方式的另外一種:影格同步化。通常用於實時戰略和FPS

影格同步化通過同步玩家的動作,確保每個人都能獲得相同的輸入,並在每一幀上執行相同的邏輯,最終獲得一致的效能和結果

相同的輸入 + 相同的時序 = 相同的輸出

如何確保同一時間點

等待所有玩家載入完成,由於載入完成後還會有一系列初始化操作,可以播個開場動畫,做到所有玩家都在同一時間點開始遊戲

同步裝置時間

使用者端存取伺服器,伺服器返回一個ping值,乘以2加上伺服器返回的時間就是準確的當前伺服器時間。遊戲期間後續同步中根據較小的ping值修改時間

同步種子

遊戲裡經常會使用亂數,同步亂數種子可以保證各個使用者端模擬的一致性

命令同步

伺服器每幀收集所有玩家操作,然後將其廣播給所有玩家,沒有玩家操作就廣播一個空指令,向前推動遊戲幀

核心邏輯-命令佇列

命令佇列的設計可以輕鬆實現戰鬥回放。建立兩種偵聽器,分別是本地模式和網路模式

  • 本地模式下偵聽玩家的操作並將操作填充到佇列中
  • 網路模式下偵聽玩家的操作並行送給伺服器,同時監視伺服器發來的資料並將操作填充到佇列中

核心邏輯-遊戲主迴圈識別

影格同步化需要我們嚴格控制整個遊戲的執行順序,通常情況下,不能直接使用引擎更新,需要把一切掌握在自己手中。首先需要控制的是幀速率

  • 以特定的影格率來執行遊戲,如每秒60幀
  • 跟蹤幀進度並控制,如果當前裝置幀索引落後過多,加快它的影格率

物件的更新應當是按特定順序執行的,需要進行排序

網路延遲

新增幀緩衝區和前捲動畫,用UDP取代底層TCP如KCP

由於TCP超時重傳機制。沒有收到一幀的封包時,遊戲的邏輯無法正常執行,直到封包被重新傳送

或者直接幀鎖定,直到有資料來,以超快的影格率同步

不用幀鎖定,使用者端請求伺服器狀態副本,實現回滾跟重試然後恢復

重新連線

如果是一個小的重新連線,只丟失了幾幀資料,會用這幾幀的資料進行補充。如果是一個大型重新連線,伺服器序列化的資料此時將快取5秒。如果在這段時間內重複斷開連線並重新連線,伺服器將重用這些快取的資料。

優點

使用影格同步化可以節省訊息量,狀態同步需要伺服器對每個使用者端傳送大量狀態資訊(大量實體,每個實體各自維護大量欄位),影格同步化只需要傳送操作指令和幀索引

由於訊息量得到了節省,在網路情況不佳的情況下,也能實現實時戰鬥遊戲的同步問題

我們可以輕鬆實現回放,伺服器記錄所有操作,使用者端請求回放檔案執行每一幀


參考資料

Client-Server Game Architecture - Gabriel Gambetta

Networking (part 2) · GitBook (rvagamejams.com)

Game server synchronization of large amounts of data in a battle (monstar-lab.com)

Tutorial: Technical Implementation Details of Frame Synchronization in Games


ENet

ENet是LOVE使用的一個第三方網路庫,採用UDP協定,在運輸層幫我們完成了各種事情,包括訊息確認

帶心跳檢測功能,當有一方不回覆超過5~30秒時則認為其disconnect