幀同步到底是什么


前段時間看了很多關於同步機制的文章,和同組leader也討論了很多這方面相關的內容,總結了一部分,在此寫下保存自留吧

幀同步

主旨:同步的玩家操作指令 “相同的輸入 + 相同的時機 = 相同的顯示

目的:在於消除網絡波動性帶給玩家的卡頓以及忽快忽慢的不良體驗。

大致流程:

  1. 同步隨機數種子(可以保持同步的一致性)
  2. 客戶端上傳操作指令(游戲操作以及當前幀索引)
  3. 服務端轉發客戶端指令(沒有指令也要廣播空指令以推進游戲幀)

特點:客戶端的邏輯實現和表現實現是需要完全分離的,需要有自己的一套物理引擎,這樣及時unity的渲染不同步,但邏輯跑出來是同步的

設計模式:所有C端強制采用一個邏輯幀率,從而保證輸出一致

同步機制:時間同步+指令同步

時間同步:所有玩家同時間開始游戲

  1. Loading界面來同步所有玩家的資源加載進度
    • 加載完之后也不一定同步,因為還有些初始化的邏輯
  2. 開場動畫消除初始化邏輯差異
    • 三秒的動畫,A用了兩秒初始化,B用了一秒初始化,A就播一秒動畫而B就播兩秒動畫
  3. 多端時間的同步:
    • 請求服務器的時間並同時計算這次請求的ping值,就是包到服務器然后再回來的時間。然后這個ping值除以2再加上服務器的返回的時間,就可以得到一個准確的當前服務器的時間。接着,后面游戲過程中的同步,我們也可以根據更小的ping值來修正時間。

指令同步:服務器每一幀會收集所有玩家的操作,然后廣播給所有的玩家。

核心邏輯的實現

  • 命令隊列:所有的玩家操作不會直接添加到角色身上,而是放在隊列中,然后再從隊列中拿出操作
    • 單機模式的監聽器:監聽到玩家操作的時候,直接把操作加入隊列中
    • 網絡模式的監聽器:監聽到玩家的操作后,把它發送到服務端,同時他也會監聽服務端,把服務器返回的操作插入隊列中
  • 游戲的主循環幀同步對游戲的邏輯執行順序有嚴格的控制,因此一般不使用引擎上的update,得要控制幀率

幀率的控制

  1. 按特定的幀率去執行游戲:比如一秒10幀的一個邏輯幀,60秒一個顯示幀
  2. 要控制追幀:如果說他的游戲進度落后了,那么他應該要跑更多的邏輯幀,原本一秒10幀,追幀的情況下可能一秒需要跑60幀,快進到當前進度。
    • 比如,我們游戲卡了很久,那么update傳進來Delta會很大,那么把這個Delta加進去之后,它會遠遠大於你原本所需要執行的幀數,這時候我們會加速執行,執行到一定的數量退出,然后下一次再加速執行,直到當前進度。
    • 在執行邏輯之前,需要對所有的對象先走一次排序,然后按特定的順序執行每個對象的update的方法去執行邏輯。

如何對抗網絡延遲

  1. 增幀緩沖和前搖動畫去掩蓋延遲
  2. UDP替換TCP
    1. 原因:TCP實時性差(Nagle算法的TCP_NODELAY會使發送時間變長,因為他需要收集小包后在一次性傳);超時重傳發生的時間相比也太長(只要有一幀的包沒有收到,游戲邏輯就無法正常執行,直到接收到包。等超時了,TCP的快速重傳機制會被觸發,才會重新發包)
    2. 可靠的UDP:在UDP上加一層封裝,自己去實現丟包處理,消息序列,重傳等類似TCP的消息處理方式,保證上層邏輯在處理數據包的時候,不需要考慮包的順序,比如KCP。還是會有風險,因為它為了保證包的順序和處理丟包重傳等,在網絡不佳的情況下,Delay很大
    3. 冗余的UDP:兩端的消息里面,帶有確認幀信息,比如客戶端(C)通知服務器(S)第100幀的數據,S收到后通知C,已收到C的第100幀,如果C一直沒收到S的通知(丟包,亂序等原因),就會繼續發送第100幀的數據給S,直到收到S的確認信息。效果會更好,只要和服務器商議好確認幀和如何重傳即可
  3. 鎖幀同步(deterministic lockstep):客戶端的每一幀的推進,都需要得到同一局的所有玩家確認,所以一個玩家掉線游戲就會暫停。卡住等這一幀的數據下來,之后會加速追回到當前的進度
    1. 適合局域網P2P,比如DOTA等。因為是局域網這種情況比較少,所以可以接受
  4. 不鎖幀同步(預測回滾):每個客戶端和服務端以一定的幀率進行幀數據的同步,服務端只管當幀時間抵達時,將數據整理成一個幀數據包廣播給所有的客戶端。客戶端收到幀數據包后對幀號進行比對,判斷是否需要進行追幀,和執行邏輯。如果客戶端沒有操作的話,服務器是不會下發空包的,有操作的時候才進行廣播。
    1. 客戶端沒有收到包也不會卡住,而是繼續執行。所以客戶端的網絡只會影響自己的游戲體驗,其它網絡正常的玩家是可以正常游戲的。
    2. 需要客戶端糾錯。向服務器請求一份最新的狀態,在客戶端反序列化出來。另外,客戶端本身也可以做回滾然后重試”這樣的一個機制,就是回滾到最近的一個正確的狀態,然后再追回。
    3. 適合C/S模式,比如LOL,王者榮耀
    4. 不適合局域網,因為如果作為主機的客戶端離線或者丟包,其他客戶端是沒辦法保持一致的狀態。而中心服務器模式,會將幀數據儲存起來再廣播出去,即使客戶端重連,也是可以獲取到幀數據隊列進行追幀

戰斗框架設計

核心:邏輯層與顯示層分離,使用延遲執行實現模塊解耦

  1. 邏輯層和顯示層分離后,可以按不同的頻率跑。比如邏輯幀10/s,顯示幀60/s,顯示層會根據邏輯幀狀態變化而變化
  2. 低耦合高內聚。利於客戶端的序列化和反序列化
  3. 安全性高一般幀同步都是信任客戶端的,這樣可以在服務器上實時或者離線地校驗戰斗結果,對外掛有很好的防范效果

斷線重連的優化

  1. 服務器跑邏輯,重連的時候讓服務器發送最新的狀態給客戶端。如果頻繁的斷線重連,會對服務器的性能要求比較高
  2. 序列化數據儲存, 對每一幀的數據保存快照
  3. 小重連:只有幾幀的數據,客戶端序列化關鍵幀數據,重連回滾
    1. 每收到一個正確的包會做一次關鍵幀序列化
    2. 每隔10s再做一次定時幀序列化
    3. 最多貯存三個定時幀序列化和一個關鍵幀序列化。每次斷線重連的時候,就從當前時間往前找最近一份數據來做恢復加速(回滾)
    4. 客戶端根據這些快照來更新各自的世界狀態,通常會用插值”方法在兩個相鄰的快照間做平滑
  4. 大重連:序列化數據會做5秒的緩存,如果這段時間一直斷線重連,則會一直復用該數據
    1. 客戶端序列化異步儲存磁盤,從磁盤里加載數據來做恢復

序列化和反序列化

核心:把所有屬性寫入buffer,或從buffer讀取恢復

  1. 優先序列化目錄:我們所有對象的一個表,我們需要先確保所有的對象都已經實例化出來了,后面在恢復對象屬性的時候需要用到
  2. 已經死亡的對象,如果還有其他地方引用到,也需要序列化和反序列化,以保證邏輯正確
  3. 需要注意創建和刪除對象時的副作用。比如在反序列化恢復的時候,添加對象進來的時候,會執行一個技能,這個技能會修改其他對象的一些屬性,那么就污染到了其他人身上的一些變量。或者重連回來了結果戰斗結束了,可以在戰斗結束的時候,通過緩存戰斗結果一段時間,等玩家重連回來后再把最后結果發送給他,直接返回到結算界面

一致性問題

  1. 浮點數計算:在不同機器上有不同的表現,由此,導致了浮點數的精度可能導致計算結果不一致
    1. 使用定點數:包含浮點數計算的常規算法,包含加、減、乘、除、絕對值、負運算等基本運算,另外,還要根據自己的使用實現開平方、指數函數、對數函數,三角函數
      • 在原來浮點數的基礎上乘1000或10000,對應地方除以1000或10000,再輔以三角函數查表,能解決一些問題,減少計算不一致的概率。但有風險,例如一個int和一個float做乘法,如果原數值就要*1000,那最后算出來的數值,可能會非常大,有越界的風險
      • FP替代float,TSVector替代Vector
    2. 一致性數學工具:向量、矩陣、歐拉角、四元數,點、線、面、體,各種幾何元素的關系,相交性檢測等
    3. 一致性物理系統 :動力學和碰撞檢測。Pyhsic.Raycast檢測地面和圍牆算出的浮點數可能會造成不確定性;尾數截斷,按碰撞方向截斷來保持一致性
    4. 一致性動畫系統:需要實現邏輯層上的動畫,時間要改成整數或者定點數,另外,插值數據的類型也得使用一致性得數據類型,比如位置向量等 
  2. 控制隨機:舉例玩家A的暴擊幾率是80%,假設在第200幀中,玩家A進行了一次平A攻擊,客戶端A計算得到這次攻擊產生了暴擊傷害300,而客戶端B計算得到的結果是暴擊傷害280。這就導致了傷害不一致,隨着戰斗的進行兩邊客戶端的差距會越來越大,得到不同的戰斗結果
    • 解決方案:客戶端做偽隨機算法。戰斗開始時,服務器給客戶端下發一個隨機種子,通過自定義隨機算法,保證每次的隨機結果都是可控且一致的
  3. 指針參與計算
  4. 未重置上一局的靜態變量
  5. 執行順序不同
    1. 不用一些不穩定的排序例如Dictionary,或者不確定的算法排序,比如同樣血量同樣距離,客戶端A返回A但客戶端B返回B
    2. 邏輯部分不使用Coroutine
    3. 通過一個統一的邏輯Tick入口,來更新整個戰斗邏輯,而不是每個邏輯自己去Update。保證每次Tick都從上到下,每次執行的順序一致。
      • 比如我們項目中用到了很多第三方的插件(例如UGUI),這些插件的Update是沒法由幀同步去控制的,各個客戶端可能執行某個計算片段的時間並不是在同一邏輯幀內導致出現不同步的情況。unity的物理系統,動畫系統也是如此。解決方案就是不使用第三方插件,使用成熟穩定的開源插件或者自己實現,保證每個update都是自主可控的。避免出現不可控導致的不同步的情況出現
  6. 全局里用了主觀邏輯。比如先執行我的在執行他的,這樣會造成不同的客戶端會有不同的結果。所以一般都是按順序,一號玩家和二號玩家以此類推
  7. 接受網絡順序不一致:UDP在出現網絡波動的情況下,接收端接收到的消息是無序的,客戶端在接收服務器發送的消息時,后發的消息可能會比先發的消息還要先接收到,這樣就會造成客戶端對於操作輸入的順序不一致,也是會造成最終的不同步的。
    • 解決方案:對每一個發出的消息做一個自增的編號,根據編號的連續性確定消息的順序,就算先收到后面的消息,也可以等待前面的消息收到之后進行順序傳入游戲邏輯中

如何debug一致性

  1. 打內存log。每次戰斗結束之后,把這兩個玩家他們的內存日志做一個哈希,上報到服務器那邊。然后去做一個對比,如果對比說不一致的話,那么兩個客戶端就把這份詳細的內存日志壓縮然后上報到后台,尋找bug

防作弊

核心:檢驗關鍵幀數據

  1. 服務端隔一段時間收集每個客戶端指定幀的幀數據,進行對比。少數服從多數,出現異常的客戶端要求重連
  2. 將客戶端的邏輯實現抽象出來,服務端跑一個邏輯服務器,跟游戲進行同步運行或者結算的時候運行,關鍵狀態數據以邏輯服上報的結果為主。

 

幀同步與狀態同步對比

幀同步:

游戲中的操作同步。是一種客戶端與服務器的同步方式,是為了實現高實時性,高同步性的應用而產生的。

  • 實時性
    • 所有玩家的指令一定是要及時地同步到所有玩家的終端上的。
    • 客戶端發出指令到服務器需要時間,服務器發送指令到其他客戶端也需要時間,發送消息的周期一定要短
    • 同步消息頻率越大,對於性能要求越高,成本也就越高。
    • 能夠減小服務器的壓力,也為了能夠更快地轉發信息,游戲的邏輯一般會放到客戶端去執行,這樣更快
  • 同步性
    • 所有玩家收到的信息一定要是一致的
    • 客戶端需要將指令同步后然后在固定的幀間隔內進行邏輯計算,保證每個客戶端收到相同指令都會運行出唯一的結果
    • 為了應對玩家掉線的情況,服務器應該保存一場游戲中的指令,在玩家斷線重連后發送到玩家終端。

狀態同步:

游戲中的各種狀態同步。一般的流程是客戶端上傳操作到服務器,服務器收到后計算游戲行為的結果,然后以廣播的方式下發游戲中各種狀態,客戶端收到狀態后再根據狀態顯示內容。

  • 不同玩家屏幕上的表現的一致性並不是重要指標, 只要每次操作的結果相同即可,狀態同步對網絡延遲的要求並不高。RPG用的比較多。
  • RPG的動畫效果做的很華麗,放技能之前一般也有一個動畫前搖,同時將攻擊請求提交給服務器,等服務器結果返回時,動畫也播放完畢,之后就是統一的傷害效果和結算。

 

Reference:

  1. https://www.youxituoluo.com/528021.html
  2. https://blog.uwa4d.com/archives/USparkle_frame-alignment.html
  3. https://mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg
  4. https://blog.csdn.net/zhang1461376499/article/details/116670361
  5. https://www.youxituoluo.com/528021.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM