前言
最近在做聯網戰鬥同步這塊的東西,讀了不少文章、書籍,於是整理了一下。
之前也有在 團隊內部技術分享 中分享過這塊內容,但是有些東西受限於時間,只是大概的略過,重點放在了實現與遇到的難題解決上。
後來,在做優化調整的時候,又有不少新的收穫,改進了之前的分享稿。
歡迎各位小夥伴來一起討論,通過分享討論來不斷進步。
1. 簡介
現狀
網絡遊戲的同步方案,大概由以下三部分搭配組成
- 網絡傳輸協議
- 網絡同步模型
- 網絡拓撲結構
網絡傳輸協議(Network Transport Protocol)
-
分類
- UDP協議
- TCP協議
-
共同點
- 在 TCP/IP 協議族中[物理層,數據鏈路層,網絡層,傳輸層,應用層],位於 傳輸層的協議,均依賴底層 網絡層中的 IP協議。
-
區別
UDP TCP 傳輸可靠性 不可靠 可靠 傳輸速度 快 慢 帶寬 包頭小,省 包頭大,費 連接速度 快 慢 - 此外,TCP還提供了 流量控制、擁塞控制等
-
其他
- 關於 KCP協議
- KCP協議是一個快速可靠協議,它以浪費10%-20%帶寬的代價(相較於TCP協議),換取平均延遲降低 30%-40%,且最大延遲降低三倍的傳輸效果。純算法實現,並不負責底層協議的收發。
- 更詳細信息可跳轉最下方 參考資料2
- 關於 KCP協議
網絡同步模型(Network Model)
-
分類
- 狀態同步(State Synchronization )
- 本質:上傳包含 遊戲外部變化原因集合(玩家操作等)及 中間狀態的子集(客戶端計算的部分);下發包含 遊戲狀態的集合。
- 幀同步(Lockstep)
- 本質:上傳下發只包含遊戲外部變化原因集合(玩家操作等)。
- 對於A客戶端: [輸入A] -> [過程A + 運算A] -> 輸出A
- 對於B客戶端: [輸入B] -> [過程B + 運算B] -> 輸出B
- 確保 [輸入] 相同,再保證 [過程] 與 [運算] 一致,那麼 [輸出] 一定是一致的
- 邏輯幀,遊戲在邏輯層面是離散的過程,即可認爲是一個邏輯幀一個邏輯幀進行邏輯運算。
- 渲染幀,遊戲在渲染層面是離散的過程,即可認爲是一個渲染幀一個渲染幀進行畫面呈現。
- 遊戲邏輯幀 與 渲染幀 需要互相獨立。
- 本質:上傳下發只包含遊戲外部變化原因集合(玩家操作等)。
- 狀態同步(State Synchronization )
-
共同
- 兩種同步方案都分爲 上傳 和 下發 過程。
- 上傳 指客戶端將信息傳輸給 服務器/客戶端
- 下發 指客戶端從 服務器/客戶端 中獲取信息
- 兩種同步方案都分爲 上傳 和 下發 過程。
-
對比
幀同步 狀態同步 流量 通常較低,取決於玩家數量 通常較高,取決於該客戶端可觀察到的網絡實體數量 預表現 難,客戶端本地計算,進行回滾等 較易,客戶端進行預表現,服務器進行權威演算,客戶端最終和服務器下發的狀態進行調節或回滾 確定性 嚴格確定性 不嚴格確定性 弱網影響 大,較難做到預表現 小,較易做到預表現 斷線重連 難,需要獲取所有相關幀且快播追上進度 易,根據快照迅速恢復 實時回放 難,客戶端需要消耗非常大性能去從頭播放到對應序列,回放完後需要快播追趕 易,根據快照進行回放,回放完再根據快照恢復 邏輯性能優化 難,客戶端需要運算所有邏輯,跟客戶端性能強相關 易,大部分邏輯可在服務器進行,分擔客戶端運算壓力 外掛影響 大,客戶端擁有所有信息,透視類外掛影響嚴重 小,服務器可做視野剔除等處理 開發特徵 平時開發高效,不需要前後端聯調;但是開發時要保證各模塊確定性,不同步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
- Ping
- 丟包率
- 原因: 直接原因是由於 無線網絡 和 擁塞控制,根本原因比較複雜。
- 標準:
- 優秀:<= 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)
功能
- 處理聯網戰鬥基礎場景流程、方法
- 房間狀態切換流程
- 創建角色數據及實體
- 傳送邏輯
- 實現本地戰鬥與聯網戰鬥切換
如何支持聯網戰鬥
- Cache,定義自己的Cache
- 通過 聯網戰鬥數據緩存類 解析數據
- 在 聯網戰鬥數據緩存類中 綁定戰鬥類型及Cache
- 重寫需要處理的協議收發
- Scene
- 繼承 聯網戰鬥場景類
- 處理數據 及 處理相應配置
- 是否自動傳送
- 是否本地戰鬥
爲什麼採用幀同步
- 遊戲的核心邏輯在客戶端實現,服務器只負責轉發驗證等
- 遊戲類型及形式,動作類、房間爲單位;更適合用幀同步
- 開發速度快,週期短
3. 重點處理
如同幀同步的簡介中介紹,要保證 輸出的一致性,先要確保輸入、過程、運算的一致性。
浮點數與定點數 [運算一致性]
浮點數的運算在不同的操作系統,甚至不同的機器上算出來的結果都是有精度差異的。
一般解決該類問題方法:
- 使用定點數
- 使用分數
這裏主要麻煩點在於lua支持定點數,lua中的小數是double,需要把lua源碼中的基礎小數全部替換爲定點數。
然後,物理引擎的計算,第三方庫的引用(比如隨機數),都需要使用定點數。
確定的 隨機數機制 [運算一致性]
確定的隨機數機制就是保證各個客戶端一旦用到隨機數,隨機出來的值必須是一樣的。
得益於計算機的僞隨機,通過設定同樣的隨機種子即可實現。
但是,在客戶端內,需要明確區分隨機數的類型
- 戰鬥類
- 設計戰鬥的實體、BUFF、技能 等等
- 非戰鬥類
- 主要是顯示項的隨機,比如 loading期間的 tip選擇
這裏,爲了更明確區分,在客戶端做了一層封裝:
- AEUtil:GRandom,戰鬥類的隨機數方法
- AEUtil:UIRandom,非戰鬥類的隨機數方法
做好區分,也便於相關日誌的打印。
使用戰鬥類隨機數模塊:
- AI
- 行爲樹
- 相機
- 技能、BUFF、特殊能力
- 實體相關
- 地圖相關
使用 非戰鬥類 隨機數模塊:
- UI界面
- 外掛檢測
- 數據收集
- 音樂音效
當然,也不是絕對的,比如實體相關的有些可以不用戰鬥類隨機數,比如NPC彈出個對話,也是純顯示性的。這裏是爲了好區分,方便開發,一刀切了。
確定的 容器及算法 [過程一致性]
- 對於lua語言,不要用 pairs 方式遍歷,要用 ipairs,也相應就要求容器必須是數組
- 所有用到的算法,必須是 穩定 的算法
隔離與封裝 邏輯層 [過程一致性]
所有模塊都可以分爲 draw 與 update 兩部分
- draw 進行繪製,走本地繪製幀更新
- update 進行邏輯計算,走邏輯幀更新,可被幀同步接管
實現幀同步尤其需要對 邏輯層的數據進行封裝與隔離
以位移組件爲例:
- 位移組件有兩套座標
- 邏輯座標
- 渲染座標
- 人物的行走都是通過邏輯座標計算,渲染座標是在渲染幀的時候,將當前渲染座標與邏輯座標進行比較,再用差值平滑過渡
同理的還有:
- 碰撞框的計算
- 各組件
- 各實體
做好分離,也便於之後做快照相關的優化。
支持本地戰鬥
創建聯網戰鬥場景基類繼承自單人戰鬥場景基類,用來統一控制聯網相關的特殊操作,如 傳送,協議交互 等。
然後,設置本地戰鬥變量,用來進行控制,若是本地戰鬥,交由基類處理。
同步模式 及 處理幀策略 [過程一致性]
同步模式
- 服務器: 固定推幀 30幀/秒
- 客戶端:
- 30幀/秒,每次執行一次處理幀
- 60幀/秒,每次執行一次處理幀
- 30幀/秒,每次執行一次處理幀,繪製幀到來時,若有幀積壓,再執行一次處理幀
處理幀策略
每次執行一次處理幀操作,具體釋放幀數量
- 釋放1幀
- 逐步釋放幀
- 累計幀數 < 2幀,釋放1幀
- 累計幀數 < 5幀,釋放2幀
- 累計幀數 < 10幀,釋放3幀
- 累計幀數 >= 10幀,釋放所有幀
- 可變釋放幀
- 釋放幀數量由 PlayFrameScale 變量控制,可 加速/減速 播放(一般用於處理回放)
- 釋放全部幀
斷線重連
斷線重連,主要由 聯網戰鬥數據緩存類(CacheNetworkedFight)負責。
- 從服務器中獲取重連過程中的戰鬥幀
- 進入 追幀模式進行追幀,在追幀模式中,服務器發來的推送幀會被緩存起來
- 追幀完畢後,退出追幀模式;並將追幀期間的 服務器推送幀壓入 同步管理器中
同步校驗
驗證多個客戶端是否同步,主要依賴於隨機數及調用隨機數的位置。
在聯網戰鬥運行時,會將使用的隨機數都打印出來,由於我們隨機種子一致,所調用的隨機數序列也應該是一致的,輔助以調用隨機數的位置信息,戰鬥結束後對不同客戶端的隨機種子文件日誌比對,可以校驗同步。
我處理這塊的方式是使用兩個日誌文件,
- 一個用來做同步校驗:大部分內容是 使用隨機數的模塊 + 隨機數範圍 + 最終生成的隨機數,還有一些必然一致的過程日誌。
- 另一個用來做同步排查:包含更詳細的日誌信息
兩場戰鬥結束後,用對比工具比較日誌,一旦有差異,用更詳細的日誌信息,進行排查。
4. 優化項
聯網戰鬥同步向來不是一個做完就行的東西,而且也沒有一套東西,在各個類型遊戲通喫的情況。
所以,在實現完基礎的同步架構後,還有很長的路要走。
目前只是搭建了一個基礎的框架,要真正投入還有下面這些優化項可以做。
下面這些東西,有些已經做了,有些正在做,有些是一些設想,即將做的;歡迎各位夥伴一起來討論。
快照的支持
在幀同步基礎上,進行優化;就是 幀同步+快照 的模式。
其實已經不屬於幀同步了,偏向狀態同步。
快照作用就是將整個現場備份,缺點是數據量過大。
但是,我們以房間爲單位的戰鬥,尤其適合 幀同步+快照;因爲有明確的劃分單位;並且房間初始,很多東西都是不需要存儲的。
- 房間內的快照,所有實體的狀態(怪物、NPC、傳送門 等等),HP、EP、受損狀態 等等
- 房間間的快照,實體都是初始創建,且實體的創建是不通過幀的,可以本地處理
這三者區別,
-
幀同步 => 沒有進度條的播放器;想要看到第6分30秒的內容,必須從頭開始看
-
狀態同步 => 有進度條的播放器;知道時間,就可以直接切到相應時間開始播放
-
幀同步+快照 => 有進度條,但單位是5分鐘;要看 6分30秒的內容,不需要從頭看,但是也要從第5分鐘開始播放,直到6分30秒
安全性
幀同步的安全性也是一個重大的問題,可以分爲幾大部分。
-
客戶端的安全模塊,遊戲的核心戰鬥邏輯演算都在客戶端進行,所以對於數據的加密,防篡改等都是由安全模塊統一處理。
-
網絡模塊,對於網絡層的外掛,由底層網絡模塊的加密等處理。
-
聯網戰鬥系統的防外掛模塊
基礎的幾個方案
- 每隔一段時間,進行玩家信息收集並上傳(如血量、技能使用、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
-
等等
這些模塊的更新,都是固定順序執行,所有參數都是確定的,所用隨機都是指定隨機方法
需要客戶端同步的東西,必須通過幀來驅動。
同步的實質
同步,就像一個管理器,它的策略項設計項不難,難點主要在於管理的各個模塊的內部實現。因爲一場戰鬥涉及的模塊很多,只要有一個模塊實現有不同步的地方,整場戰鬥就不同步了。
在到後期查找不同步原因,也往往是去排查下屬模塊的實現,可能就是在於遍歷方式,隨機數的使用,邏輯幀繪製幀等。
主要還是要求:
- 實現同步下屬模塊的責任人,有聯網戰鬥的意識,儘量的不使用本地數據,能區分出哪些代碼可以使用繪製幀的更新,哪些堅決不允許使用繪製幀的更新。
- 做同步的責任人,對各個下屬模塊的涉獵廣泛,不能只做完同步就可以了,還需要輔助下屬模塊進行不同步的排查。
解決這個問題的方向:
- 讓所有實現模塊的人都有聯網戰鬥的意識,對整個邏輯幀繪製幀等更新都有概念。(難度很大)
- 實現自動化同步測試,在發現不同步問題,輔助去定位解決。
參考資料: