天天看點

聯網戰鬥實作前言1. 簡介2. 實作3. 重點處理4. 優化項5. 感悟

前言

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

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

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

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

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結構

圖例

聯網戰鬥實作前言1. 簡介2. 實作3. 重點處理4. 優化項5. 感悟

狹義

關聯類

  • 同步管理類(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. 追幀完畢後,退出追幀模式;并将追幀期間的 伺服器推送幀壓入 同步管理器中
聯網戰鬥實作前言1. 簡介2. 實作3. 重點處理4. 優化項5. 感悟

同步校驗

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

在聯網戰鬥運作時,會将使用的随機數都列印出來,由于我們随機種子一緻,所調用的随機數序列也應該是一緻的,輔助以調用随機數的位置資訊,戰鬥結束後對不同用戶端的随機種子檔案日志比對,可以校驗同步。

我處理這塊的方式是使用兩個日志檔案,

  • 一個用來做同步校驗:大部分内容是 使用随機數的子產品 + 随機數範圍 + 最終生成的随機數,還有一些必然一緻的過程日志。
  • 另一個用來做同步排查:包含更詳細的日志資訊

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

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

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

聯網戰鬥實作前言1. 簡介2. 實作3. 重點處理4. 優化項5. 感悟

這裡列出幾個方向:

  • 玩家的位置
  • 玩家的機型
  • 戰鬥的時間
  • 玩家的營運商

資料收集的選項:

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

這裡還要注意設定門檻值,防止某個異常操作,導緻資料不準确,拉高或拉低平均值。

甚至可以設定一些字段,來做篩選剔除異常資料。

5. 感悟

不信推送,相信幀

推送缺點

  • 每個用戶端處理的時機不同
    • 收到的時機:伺服器 推送A,再推送B;對于用戶端肯定是先收到A,再收到B
    • 處理的時機:由于各用戶端阻塞狀态等,每個用戶端處理推送時機是不一緻的
  • 推送可能丢失
  • 推送内容,不支援在回放中處理

比如:

  • 玩家複活
  • 玩家掉線,删除角色

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

遊戲進行過程中,所有的相關子產品:

  • 實體管理器 - EntityManager
  • 場景管理器 - SceneManager
  • 碰撞管理器 - AECollision
  • 錄影機管理器 - CameraManager
  • 等等

這些子產品的更新,都是固定順序執行,所有參數都是确定的,所用随機都是指定随機方法

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

同步的實質

同步,就像一個管理器,它的政策項設計項不難,難點主要在于管理的各個子產品的内部實作。因為一場戰鬥涉及的子產品很多,隻要有一個子產品實作有不同步的地方,整場戰鬥就不同步了。

在到後期查找不同步原因,也往往是去排查下屬子產品的實作,可能就是在于周遊方式,随機數的使用,邏輯幀繪制幀等。

主要還是要求:

  • 實作同步下屬子產品的責任人,有聯網戰鬥的意識,盡量的不使用本地資料,能區分出哪些代碼可以使用繪制幀的更新,哪些堅決不允許使用繪制幀的更新。
  • 做同步的責任人,對各個下屬子產品的涉獵廣泛,不能隻做完同步就可以了,還需要輔助下屬子產品進行不同步的排查。

解決這個問題的方向:

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

參考資料:

  • DonaldW-網絡遊戲同步技術概述
  • KCP - A Fast and Reliable ARQ Protocol、
  • 《騰訊遊戲開發精粹》

繼續閱讀