聯網戰鬥實現

前言

最近在做聯網戰鬥同步這塊的東西,讀了不少文章、書籍,於是整理了一下。

之前也有在 團隊內部技術分享 中分享過這塊內容,但是有些東西受限於時間,只是大概的略過,重點放在了實現與遇到的難題解決上。

後來,在做優化調整的時候,又有不少新的收穫,改進了之前的分享稿。

歡迎各位小夥伴來一起討論,通過分享討論來不斷進步。





1. 簡介

現狀

網絡遊戲的同步方案,大概由以下三部分搭配組成

  • 網絡傳輸協議
  • 網絡同步模型
  • 網絡拓撲結構


網絡傳輸協議(Network Transport Protocol)

  • 分類

    • UDP協議
    • TCP協議
  • 共同點

    • 在 TCP/IP 協議族中[物理層,數據鏈路層,網絡層,傳輸層,應用層],位於 傳輸層的協議,均依賴底層 網絡層中的 IP協議。
  • 區別

    UDP TCP
    傳輸可靠性 不可靠 可靠
    傳輸速度
    帶寬 包頭小,省 包頭大,費
    連接速度
    • 此外,TCP還提供了 流量控制、擁塞控制等
  • 其他

    • 關於 KCP協議
      • KCP協議是一個快速可靠協議,它以浪費10%-20%帶寬的代價(相較於TCP協議),換取平均延遲降低 30%-40%,且最大延遲降低三倍的傳輸效果。純算法實現,並不負責底層協議的收發。
      • 更詳細信息可跳轉最下方 參考資料2


網絡同步模型(Network Model)

  • 分類

    • 狀態同步(State Synchronization )
      • 本質:上傳包含 遊戲外部變化原因集合(玩家操作等)及 中間狀態的子集(客戶端計算的部分);下發包含 遊戲狀態的集合。
    • 幀同步(Lockstep)
      • 本質:上傳下發只包含遊戲外部變化原因集合(玩家操作等)。
        • 對於A客戶端: [輸入A] -> [過程A + 運算A] -> 輸出A
        • 對於B客戶端: [輸入B] -> [過程B + 運算B] -> 輸出B
        • 確保 [輸入] 相同,再保證 [過程] 與 [運算] 一致,那麼 [輸出] 一定是一致的
      • 邏輯幀,遊戲在邏輯層面是離散的過程,即可認爲是一個邏輯幀一個邏輯幀進行邏輯運算。
      • 渲染幀,遊戲在渲染層面是離散的過程,即可認爲是一個渲染幀一個渲染幀進行畫面呈現。
      • 遊戲邏輯幀 與 渲染幀 需要互相獨立。
  • 共同

    • 兩種同步方案都分爲 上傳 和 下發 過程。
      • 上傳 指客戶端將信息傳輸給 服務器/客戶端
      • 下發 指客戶端從 服務器/客戶端 中獲取信息
  • 對比

    幀同步 狀態同步
    流量 通常較低,取決於玩家數量 通常較高,取決於該客戶端可觀察到的網絡實體數量
    預表現 難,客戶端本地計算,進行回滾等 較易,客戶端進行預表現,服務器進行權威演算,客戶端最終和服務器下發的狀態進行調節或回滾
    確定性 嚴格確定性 不嚴格確定性
    弱網影響 大,較難做到預表現 小,較易做到預表現
    斷線重連 難,需要獲取所有相關幀且快播追上進度 易,根據快照迅速恢復
    實時回放 難,客戶端需要消耗非常大性能去從頭播放到對應序列,回放完後需要快播追趕 易,根據快照進行回放,回放完再根據快照恢復
    邏輯性能優化 難,客戶端需要運算所有邏輯,跟客戶端性能強相關 易,大部分邏輯可在服務器進行,分擔客戶端運算壓力
    外掛影響 大,客戶端擁有所有信息,透視類外掛影響嚴重 小,服務器可做視野剔除等處理
    開發特徵 平時開發高效,不需要前後端聯調;但是開發時要保證各模塊確定性,不同步BUG出現,難以排查 平時開發效率一般,需要前後端聯調,無不同步BUG
    第三方庫影響 大,第三方庫需要確保確定性 小,第三方庫不需要確保確定性


網絡拓撲結構(Network Topology)

  • 分類

    • 對等結構
      • P2P結構(Peer to Peer)
    • 主從結構
      • CS結構(Client-Server)
  • 共同

    • 網絡時延的評估標準
      • Ping
        • 概念:網絡連接的兩個端之間的信號在網絡傳輸所花費的時間。
        • 例:從A端發出信號開始計時,到B端響應並立刻返回響應信號,A端收到響應後停止計時,該時長爲Ping。
      • RTT(Round Trip Time)
        • 概念:一般可認爲等於Ping,但此處 RTT = Ping + 兩個端的處理信號前等待時間 + 兩個端處理信號的時間,即 實際體驗到的遊戲時延。
        • 例:A端邏輯發出信號開始計時,在A端等待一段時間、處理一段時間;發出到B端,在B端等待一段時間、處理一段時間;處理髮出響應信號;再次在B端等待一段時間、處理一段時間;發出到A端,再次等待一段時間、處理一段時間;A端邏輯收到響應信號,停止計時;該時長爲RTT。
      • 標準(單位 ms)
        • 極好:<= 20
        • 優秀:21 ~ 50
        • 正常:51 ~ 100
        • 差:101 ~ 200
        • 極差:>= 201
    • 丟包率
      • 原因: 直接原因是由於 無線網絡 和 擁塞控制,根本原因比較複雜。
      • 標準:
        • 優秀:<= 2%
        • 一般:2% ~ 10%
        • 差:>= 10%
  • 對比

    P2P結構 CS結構
    樣式 全連接的網狀結構 星狀結構
    連接數 O(n^2) O(n)
    流量 各客戶端相等,均爲 O(n^2) 服務器爲 O(n),客戶端爲 O(1)
    客戶端間的時延 較小,爲RTT/2 較大,爲RTT



2. 實現

廣義

廣義上來說,遊戲採用的技術是:

  • 網絡傳輸協議: KCP & TCP
  • 網絡同步模型: 幀同步
  • 網絡拓撲結構: CS結構

圖例
幀同步流程



狹義

關聯類

  • 同步管理類(SyncManager)
  • 聯網戰鬥數據緩存類(CacheNetworkedFight)
  • 聯網戰鬥場景類(NetworkFightScene)

同步管理類(SyncManager)

功能

  • 同步幀的操作的處理
    • 添加
    • 處理
    • 執行
    • 修正
  • 對玩家操作的處理
    • 收集
    • 上傳
    • 吞噬

聯網戰鬥數據緩存類(CacheNetworkedFight)

功能

  • 提供聯網戰鬥通用數據模型
    • 解析玩家數據
  • 客戶端與戰鬥服交互的中樞
    • 發往戰鬥服消息,均由此統一發送
    • 收到戰鬥服的消息,處理後轉發給其他業務類
  • 斷線重連相關處理
    • 追幀相關

聯網戰鬥場景類(NetworkFightScene)

功能

  • 處理聯網戰鬥基礎場景流程、方法
    • 房間狀態切換流程
    • 創建角色數據及實體
    • 傳送邏輯
  • 實現本地戰鬥與聯網戰鬥切換

如何支持聯網戰鬥

  1. Cache,定義自己的Cache
    • 通過 聯網戰鬥數據緩存類 解析數據
    • 在 聯網戰鬥數據緩存類中 綁定戰鬥類型及Cache
    • 重寫需要處理的協議收發
  2. Scene
    • 繼承 聯網戰鬥場景類
    • 處理數據 及 處理相應配置
      • 是否自動傳送
      • 是否本地戰鬥


爲什麼採用幀同步

  1. 遊戲的核心邏輯在客戶端實現,服務器只負責轉發驗證等
  2. 遊戲類型及形式,動作類、房間爲單位;更適合用幀同步
  3. 開發速度快,週期短



3. 重點處理

如同幀同步的簡介中介紹,要保證 輸出的一致性,先要確保輸入、過程、運算的一致性。

浮點數與定點數 [運算一致性]

浮點數的運算在不同的操作系統,甚至不同的機器上算出來的結果都是有精度差異的。

一般解決該類問題方法:

  • 使用定點數
  • 使用分數

這裏主要麻煩點在於lua支持定點數,lua中的小數是double,需要把lua源碼中的基礎小數全部替換爲定點數。

然後,物理引擎的計算,第三方庫的引用(比如隨機數),都需要使用定點數。



確定的 隨機數機制 [運算一致性]

確定的隨機數機制就是保證各個客戶端一旦用到隨機數,隨機出來的值必須是一樣的。

得益於計算機的僞隨機,通過設定同樣的隨機種子即可實現。

但是,在客戶端內,需要明確區分隨機數的類型

  • 戰鬥類
    • 設計戰鬥的實體、BUFF、技能 等等
  • 非戰鬥類
    • 主要是顯示項的隨機,比如 loading期間的 tip選擇

這裏,爲了更明確區分,在客戶端做了一層封裝:

  • AEUtil:GRandom,戰鬥類的隨機數方法
  • AEUtil:UIRandom,非戰鬥類的隨機數方法

做好區分,也便於相關日誌的打印。

使用戰鬥類隨機數模塊:

  • AI
  • 行爲樹
  • 相機
  • 技能、BUFF、特殊能力
  • 實體相關
  • 地圖相關

使用 非戰鬥類 隨機數模塊:

  • UI界面
  • 外掛檢測
  • 數據收集
  • 音樂音效

當然,也不是絕對的,比如實體相關的有些可以不用戰鬥類隨機數,比如NPC彈出個對話,也是純顯示性的。這裏是爲了好區分,方便開發,一刀切了。



確定的 容器及算法 [過程一致性]

  • 對於lua語言,不要用 pairs 方式遍歷,要用 ipairs,也相應就要求容器必須是數組
  • 所有用到的算法,必須是 穩定 的算法


隔離與封裝 邏輯層 [過程一致性]

所有模塊都可以分爲 draw 與 update 兩部分

  • draw 進行繪製,走本地繪製幀更新
  • update 進行邏輯計算,走邏輯幀更新,可被幀同步接管

實現幀同步尤其需要對 邏輯層的數據進行封裝與隔離

以位移組件爲例:

  • 位移組件有兩套座標
    • 邏輯座標
    • 渲染座標
  • 人物的行走都是通過邏輯座標計算,渲染座標是在渲染幀的時候,將當前渲染座標與邏輯座標進行比較,再用差值平滑過渡

同理的還有:

  • 碰撞框的計算
  • 各組件
  • 各實體

做好分離,也便於之後做快照相關的優化。



支持本地戰鬥

創建聯網戰鬥場景基類繼承自單人戰鬥場景基類,用來統一控制聯網相關的特殊操作,如 傳送,協議交互 等。

然後,設置本地戰鬥變量,用來進行控制,若是本地戰鬥,交由基類處理。



同步模式 及 處理幀策略 [過程一致性]

同步模式

  • 服務器: 固定推幀 30幀/秒
  • 客戶端:
    1. 30幀/秒,每次執行一次處理幀
    2. 60幀/秒,每次執行一次處理幀
    3. 30幀/秒,每次執行一次處理幀,繪製幀到來時,若有幀積壓,再執行一次處理幀

處理幀策略

每次執行一次處理幀操作,具體釋放幀數量

  1. 釋放1幀
  2. 逐步釋放幀
    • 累計幀數 < 2幀,釋放1幀
    • 累計幀數 < 5幀,釋放2幀
    • 累計幀數 < 10幀,釋放3幀
    • 累計幀數 >= 10幀,釋放所有幀
  3. 可變釋放幀
    • 釋放幀數量由 PlayFrameScale 變量控制,可 加速/減速 播放(一般用於處理回放)
  4. 釋放全部幀


斷線重連

斷線重連,主要由 聯網戰鬥數據緩存類(CacheNetworkedFight)負責。

  1. 從服務器中獲取重連過程中的戰鬥幀
  2. 進入 追幀模式進行追幀,在追幀模式中,服務器發來的推送幀會被緩存起來
  3. 追幀完畢後,退出追幀模式;並將追幀期間的 服務器推送幀壓入 同步管理器中

聯網戰鬥斷線重連



同步校驗

驗證多個客戶端是否同步,主要依賴於隨機數及調用隨機數的位置。

在聯網戰鬥運行時,會將使用的隨機數都打印出來,由於我們隨機種子一致,所調用的隨機數序列也應該是一致的,輔助以調用隨機數的位置信息,戰鬥結束後對不同客戶端的隨機種子文件日誌比對,可以校驗同步。

我處理這塊的方式是使用兩個日誌文件,

  • 一個用來做同步校驗:大部分內容是 使用隨機數的模塊 + 隨機數範圍 + 最終生成的隨機數,還有一些必然一致的過程日誌。
  • 另一個用來做同步排查:包含更詳細的日誌信息

兩場戰鬥結束後,用對比工具比較日誌,一旦有差異,用更詳細的日誌信息,進行排查。




4. 優化項

聯網戰鬥同步向來不是一個做完就行的東西,而且也沒有一套東西,在各個類型遊戲通喫的情況。

所以,在實現完基礎的同步架構後,還有很長的路要走。

目前只是搭建了一個基礎的框架,要真正投入還有下面這些優化項可以做。

下面這些東西,有些已經做了,有些正在做,有些是一些設想,即將做的;歡迎各位夥伴一起來討論。


快照的支持

在幀同步基礎上,進行優化;就是 幀同步+快照 的模式。

其實已經不屬於幀同步了,偏向狀態同步。

快照作用就是將整個現場備份,缺點是數據量過大。

但是,我們以房間爲單位的戰鬥,尤其適合 幀同步+快照;因爲有明確的劃分單位;並且房間初始,很多東西都是不需要存儲的。

  • 房間內的快照,所有實體的狀態(怪物、NPC、傳送門 等等),HP、EP、受損狀態 等等
  • 房間間的快照,實體都是初始創建,且實體的創建是不通過幀的,可以本地處理

這三者區別,

  • 幀同步 => 沒有進度條的播放器;想要看到第6分30秒的內容,必須從頭開始看

  • 狀態同步 => 有進度條的播放器;知道時間,就可以直接切到相應時間開始播放

  • 幀同步+快照 => 有進度條,但單位是5分鐘;要看 6分30秒的內容,不需要從頭看,但是也要從第5分鐘開始播放,直到6分30秒



安全性

幀同步的安全性也是一個重大的問題,可以分爲幾大部分。

  1. 客戶端的安全模塊,遊戲的核心戰鬥邏輯演算都在客戶端進行,所以對於數據的加密,防篡改等都是由安全模塊統一處理。

  2. 網絡模塊,對於網絡層的外掛,由底層網絡模塊的加密等處理。

  3. 聯網戰鬥系統的防外掛模塊

    基礎的幾個方案

    • 每隔一段時間,進行玩家信息收集並上傳(如血量、技能使用、buff使用),出現結算不一致,由服務器裁決,可以解決部分外掛
    • 服務器新開一個“客戶端”,在那個客戶端上跑所有的幀,作爲評判依據。
    • 等等

防外掛這個東西,就是魔高一尺,道高一丈,不斷優化,不斷調整的過程,有些東西也不好講太細,只能說個大概。



不同步的處理

解決不同步問題,也是幀同步方案的一大痛點。

對於不同步的處理,可以分爲三個部分:發現 -> 重現 -> 解決

作爲開發,應該深有感觸,如果方便重現,那解決問題就很簡單了。

下面的處理方式都是針對傳統的不同步處理各個步驟,進行優化設想。

一般出現不同步: 發現不同步 -> 打開日誌開關 -> 使用同樣的數據源 -> 復現問題 -> 解決問題

發現

發現不同步,最簡單粗暴的方式,肯定是人力跑,沒有技術成本,純跑…

但是,缺點很多:

  • 人力不足
  • 時間不足
  • 不夠全面
  • 不方便收集日誌
  • 不能體現技術實力
  • 等等等等

所以,需要一種自動化的測試工具,來進行大量全面的測試。

目前打算是使用 python + jenkins 來部署自動化測試流水線,等測試完,再單獨來說一說。


重現

重現不同步,也是很重要的一個步驟,能完美重現,那距離解決就不遠了。

這裏預期採用的方案是,固定數據源 + 回放機制。

  • 固定數據源

    需要和服務器配合,服務器需要存儲參戰玩家信息及幀內容,便於回放。

    前期可以全部存儲,但是這樣服務器壓力會比較大;後期可以將本地戰鬥產生的同步文件形成MD5,發給服務器;服務器判斷各客戶端MD5不同,採取緩存錄像。

  • 回放機制

    需要客戶端實現一套根據幀內容回放機制,理論上來講幀同步的回放還是比較好實現的。

    畢竟 確定的輸入,確定的運算,確定的過程,都與時間無關聯,可以得到確定的輸出。

    但是,我們需要的是日誌文件,所以繪製幀內容可以忽略掉,儘量做到邏輯幀的播放,這樣在時間上也會更快。


解決

解決不同步問題,那就相對簡單很多了。

實現了上面的發現 與 重現,可以無數次反覆執行不同步數據源,驗證是否解決也很便捷。


運行過程中的日誌收集

這應該屬於發現不同步的部分。

在實際項目中,日誌的實現都是比較粗暴的,一般來說線上運行的模塊,都不會開啓日誌文件。因爲一般日誌文件都會比較大,尤其是查同步問題的日誌文件,涉及模塊繁多,產生文件體積大。

所以,線上出不同步問題,往往也很難復現並解決,就是無法固定數據源。(不產生校驗文件,就不能上傳MD5,不能傳MD5,服務器無法判斷是否不同步,就不會緩存)

如果有一套性能損耗小一些的日誌收集系統,會對同步問題的解決有很大的幫助,

正好最近看到了 《騰訊遊戲開發精粹》- 第六部分 - 第14章 - 一種高效的幀同步全過程日誌輸出方案 。

上面的方案也對我有一些啓發,之後可以去實驗一下。



延遲處理

在實際測驗中,會有玩家反饋卡頓情況。

延遲、卡頓的玩家體驗,一般可以分爲:

  • 延遲高
  • 波動大

而且,不同遊戲類型對延遲的敏感度也不同,現在實現的這種偏格鬥類型的遊戲,對延遲敏感度還是比較高的。

再者,傳統幀同步的處理,邏輯上就是比本地操作要慢一幀:

A幀操作 -> B幀上傳 -> C幀執行

B ≥ A,C ≥ B+1

最終,還是要用數據來驗證延遲的具體位置,可以按照下流程打時間戳,再收集各個數據,來分析並解決延遲與卡頓:

同步幀延遲數據收集

這裏列出幾個方向:

  • 玩家的位置

  • 玩家的機型

  • 戰鬥的時間

  • 玩家的運營商

數據收集的選項:

  • 最小值
  • 最大值
  • 平均值
  • 波動值

這裏還要注意設置閾值,防止某個異常操作,導致數據不準確,拉高或拉低平均值。

甚至可以設置一些字段,來做篩選剔除異常數據。




5. 感悟

不信推送,相信幀

推送缺點

  • 每個客戶端處理的時機不同
    • 收到的時機:服務器 推送A,再推送B;對於客戶端肯定是先收到A,再收到B
    • 處理的時機:由於各客戶端阻塞狀態等,每個客戶端處理推送時機是不一致的
  • 推送可能丟失
  • 推送內容,不支持在回放中處理

比如:

  • 玩家復活
  • 玩家掉線,刪除角色


邏輯幀的更新流程是確定的

遊戲進行過程中,所有的相關模塊:

  • 實體管理器 - EntityManager

  • 場景管理器 - SceneManager

  • 碰撞管理器 - AECollision

  • 攝像機管理器 - CameraManager

  • 等等

這些模塊的更新,都是固定順序執行,所有參數都是確定的,所用隨機都是指定隨機方法

需要客戶端同步的東西,必須通過幀來驅動。



同步的實質

同步,就像一個管理器,它的策略項設計項不難,難點主要在於管理的各個模塊的內部實現。因爲一場戰鬥涉及的模塊很多,只要有一個模塊實現有不同步的地方,整場戰鬥就不同步了。

在到後期查找不同步原因,也往往是去排查下屬模塊的實現,可能就是在於遍歷方式,隨機數的使用,邏輯幀繪製幀等。

主要還是要求:

  • 實現同步下屬模塊的責任人,有聯網戰鬥的意識,儘量的不使用本地數據,能區分出哪些代碼可以使用繪製幀的更新,哪些堅決不允許使用繪製幀的更新。
  • 做同步的責任人,對各個下屬模塊的涉獵廣泛,不能只做完同步就可以了,還需要輔助下屬模塊進行不同步的排查。

解決這個問題的方向:

  • 讓所有實現模塊的人都有聯網戰鬥的意識,對整個邏輯幀繪製幀等更新都有概念。(難度很大)
  • 實現自動化同步測試,在發現不同步問題,輔助去定位解決。






參考資料:




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章