回顧
在上篇文章,主要講述
- 聯網戰鬥的簡介
- 網絡傳輸協議
- 網絡同步模型
- 網絡拓撲模型
- 實現聯網戰鬥的方案
- 實現時的一些重點處理
- 實現後的一些優化改進暢想
- 我的感悟
更詳細的內容,請跳轉:聯網戰鬥同步實現
正文
之前實現了一版聯網戰鬥方案,還比較粗糙,存在許多不足的地方。
秉承着 先實現,再持續交付、快速迭代 的理念。
由於實踐的效果不是很好,所以需要做一版優化。
優化方案
總覽:
- 輪詢機制都改爲即時
- 幀推送間隔時間修改
- 建立幀緩存機制
- 擴充操作幀上傳時機
- 完善的統計機制
- 自動化工具完善
輪詢機制改爲即時
在捋清各個流程步驟時,發現有些輪詢機制,會導致延遲的增高。
- 邏輯處的輪詢
- 協議收發的輪詢
輪詢的作用是減輕各模塊功能的壓力,降低成本。
我個人是覺得,在實現功能的時候,對於這種可控性成本需求,先按最優版本實現,做出我們最優效果。
之後,再根據成本等問題,進行打折簡化。
我們做好對比數據的提供,交由項目負責人決策。
幀推送間隔時間修改
之前,爲了讓客戶端壓力小一些,處於“飢餓”狀態,服務器推幀間隔設置爲 35ms。
但是,在有些模塊進行邏輯處理依舊以 30幀/秒(固定間隔 33.3ms)處理數據。
會導致部分客戶端邏輯 與 實際有差異,造成抖動。
因此,將幀推送間隔時間修改爲 33ms,減緩抖動。
建立幀緩存機制[重點]
基礎方案,過於依賴網絡狀況。
顯然這樣是不現實的,所以需要做一套幀緩存機制,來應對網絡的波動。
釋放幀的時機:
- 邏輯幀執行時
- 繪製幀執行時
- 邏輯幀執行時釋放一次,繪製幀執行時根據需要補充釋放
釋放幀策略:
- 釋放一次,每次釋放一幀
- 可能導致幀積壓,越來越卡
- 釋放一次,每次釋放幀數量可變
- 每次釋放幀數量根據幀緩存池內幀數變化
- 釋放多次,每次釋放數量可變
- 緩存幀模式,緩存幀數量可配置
- 緩存幀模式,緩存一幀
- 如果緩存幀數量可配置,就需要做一個讓所有配置相對平衡的策略;單做緩存一幀,可以更細緻的制定策略
- 但是經用戶體驗,發現這個策略並沒有比大鍋飯更好,甚至更差。。
- 釋放一次,每次釋放所有幀
可配置緩存
第一版做的是可配置緩存。
緩存幀的數量可配,通過實際體驗來決定具體緩存幀的數量。
(最開始設想是,先做成可配;之後再根據實際網絡狀況,動態修改配置緩存的幀數量,類似於流媒體領域的 JitterBuffer)
對於釋放幀的數量方案是:
緩存幀數量爲 n
幀池內幀數量爲 m
- 若 m <= (n+1), 釋放1幀
- 若 m <= (n+1) * 2, 釋放2幀
- 若 m <= (n+1) * 3, 釋放3幀
- 若 m > (n+1) * 3, 釋放 (m-1)幀
流程圖:
緩存一幀
經過實際體驗,緩存一幀是比較理想的,大於一幀的延遲,大家接受不了。
之前的釋放幀策略是針對緩存n幀的普適規則,既然定下來緩存1幀,就做一個更爲詳細的策略。
先說結論再說步驟:
幀池內幀數量爲 m
分爲四個階段
- 正常運行階段
- 0 < m <= 2, 每次釋放1幀
- 大步調優階段
- m <= 5, 釋放1次釋放2幀
- m <= 8, 釋放1次釋放3幀
- 小步快跑階段
- m <= 14, 進入快跑模式, 往後每次釋放k幀, 直到恢復正常, k = math.ceil((m+1) / 2)
- 當不足以釋放k幀時,釋放 k-1 幀, 退出快跑模式
- 瞬間傳送階段
- m > 14, 釋放 (m-1)幀
流程圖:
這個是怎麼算出來的呢?
我列了一個表格,
列爲:
幀池內幀數量 | 延遲時間 | 釋放幀數量 | 釋放後幀數量 | 下次釋放幀時,幀池內可能幀數量 | 恢復正常所需要快播次數 | 往後每次釋放幀數量
行爲:
從1-15, 每幀33ms,15幀大約爲 495ms
接下來開始填釋放幀數量,依據以下原則:
- 每次釋放幀數量要保證在3幀內
- 恢復正常所需要的快播數,保證在3次內
- 儘可能平緩的恢復
- 若無法滿足,更傾向於保證3次快播,恢復正常
然後就開始填表格。
有一點遺憾的是,沒有卡到300ms 和 500ms,都差一幀。
擴充幀上傳時機
之前方案中,客戶端收集完幀,上傳時機在於執行邏輯幀前:先上傳之前的操作,再去執行幀操作。
擴充爲三個方案:
- 執行邏輯幀前
- 執行邏輯幀後
- 執行繪製幀前
完善的統計機制 [重點]
我個人一直特別推崇數據驅動,數據不會說謊,除非數據收集的粒度與廣度不夠。
建立一套完善的統計機制,通過數據來分析用戶狀態、行爲,進而輔助設計,優化體驗,做到有依據有目的有驗證標準的方式的去解決問題是非常必要的。
統計數值大概包括:
- 基本信息
- 機型
- 操作系統
- 位置信息
- 連接的服務器位置
- 戰鬥開始時間
- 戰鬥類型
- 掉線標記
- 服務器項(分爲兩部分,客戶端發往服務器&服務器發往客戶端)
- 丟包率
- 重發率
- 協議平均延遲
- 客戶端項
- 快播率
- 釋放幀時幀數大於1幀
- 卡死率
- 釋放幀時幀數等於0幀
- 正常率
- 釋放幀時幀數等於1幀
- 幀間隔值
- 收到幀間隔
- 幀間隔波動值
- 距離幀應到時間的差值
- PING值
- FPS值
- 快播率
- 幀操作的一生
- 操作的產生
- 操作的上傳
- 操作的下發
- 操作的執行
針對每個數值,都可以包括更詳細的項:
- 最大值
- 最小值
- 閾值達到次數
- 100%
- 70%
- 50%
- 平均值
注意:
- 計算平均值的時候,超出閾值的取閾值,避免一些極端數值影響平均值。
- 這裏我採用了python的 pandas模塊去處理並分析數據
自動化工具
判定&收集不同步
首先對戰鬥產生日誌分級存儲:
- 【同步】幀處理,隨機數,實體狀態 相關
- 【部分同步】實體添加刪除,BUFF添加刪除等(包含1的所有內容)
- 【不同步】時間戳,其他同步信息打印(包含1,2)
注意:
- 包含關係,越往下越詳細,而且下一級的內容包含上一級的所有內容,即當一條信息在 2級日誌內時,不會在1級日誌內出現,但會在3的日誌內出現。這樣做方便定位更詳細的日誌的各級位置
- 【同步】、【部分同步】、【不同步】 代表同樣的戰鬥在不同客戶端上的日誌是否一致,【部分同步】表示不一致的內容只會在開頭&結尾,不會在中間出現
1級日誌內容會比較少(因爲會在戰鬥中生成,每場戰鬥要重置),是重點的同步信息。這份日誌主要是用來判斷各客戶端戰鬥是否同步使用的,同步判斷包括兩部分:
- 隨機數取值一致
- 實體狀態一致
2級日誌,是用來輔助1級日誌查找不同步問題,但是內容會相對3級日誌更偏重聯網戰鬥一些
3級日誌,就是更廣範圍的日誌,包含遊戲其他功能模塊的處理邏輯日誌打印
不同步判定&收集:
-
戰鬥結束後,客戶端戰鬥產生的1級日誌內容,壓縮爲MD5,上傳給服務器
-
服務器收集各客戶端MD5,進行比較;判定戰鬥不同步即分版本存儲本場戰鬥錄像(所有的幀操作)
-
任意客戶端可在debug模式中,拉取不同步的戰鬥進行回放現場
重現不同步
通過不同步收集,可以獲取到不同步的戰鬥信息。
剩下的就是重現不同,這裏採用的方式很簡單,就是不斷的跑這場戰鬥。
比如,跑100次戰鬥,將MD5不同的日誌收集起來,進行比對,進而修改,直至這100場戰鬥的日誌產生的MD5均一致。
其實,只要能重現BUG,就離解決不遠了,而且是能無限重現的BUG。
其他項
-
前期測試時期,可以讓服務器把同步的戰鬥操作也存儲,然後拉去下來本地跑100場。
-
100場也只是一個樣例,嫌多可以跑30場,200場,都隨意。
-
善於利用閒置電腦,讓所有客戶端都能替你跑不同步測試
- 我是在遊戲內開發一套debug工具,配合服務器,可以拉取各個角色的回放信息,並進行不同步測試。在戰鬥時,把自己帶入爲雙方中的任何一方視角進行戰鬥並輸出日誌。
- 後期也支持,只跑邏輯,不跑繪製的戰鬥,這樣使得測試時間大大縮短。(還順便檢測出了一些BUG)
優化效果
通過最新一期的線上測試;大概近6000場戰鬥,總的不同步率 及 各戰鬥模式的不同步率 均已經降低到了 0.1%以下。(包含 1V1 PK,多人組隊戰鬥等)。
可能還是戰鬥場次不夠多,需要更大量的數據來檢測,但是,起碼目前已經到及格線,下一步就是調整優化延遲了。
聯網戰鬥就是這樣:
延遲調完調同步,同步調完調延遲。
下一步方向
現在已經算是一個閉環的方案了。
- 發現問題
- 重現問題
- 解決問題
- 驗證問題
- 收集問題
- 主動收集
- 被動收集
- 提前發現問題
各個方面都有解決方案了,剩下的就是基於這個基礎上再去不斷的優化完善。
在不同步上,目前待測試的:
- 浮點數問題
不知不覺已經做了這麼多,可惜最後無法見證最終的效果,時也命也。
僅願:
功成須獻捷,未必去經年。
番外
這些是在優化過程中遇到的一些問題,採用的一些方法,一些策略
番外:痛苦的不同步
每次接到不同步的反饋,都是異常痛苦的,就如之前所說。
同步模塊做的很多的事情都是大方向框架性的,具體的流暢問題、不同步問題,往往是負責最繁瑣複雜的發現問題的角色,
然後帶着發現的問題找相應模塊負責人,去反饋。(最麻煩的就是發現問題,一旦定位問題,距離解決基本不遠了)
但是,
同步方面的問題,往往是上線前無人問津,上線後銖錙必較。
一旦出了問題,雖然能把鍋分分鐘甩出去,但是最終留下來加班修改的還是自己;所以,解決這個問題纔是關鍵。
分析
上線前爲什麼會無人問津?
- 代碼尚在修改,測試收效甚微
- 數據尚未到位,測試範圍不足
- 待到代碼和數據雙雙齊備,距離上線不足彈指一揮間,甚至上線後依然在修改調整
- 測試方法繁瑣複雜
解決
- 針對代碼修改問題;可以提高版本內相關功能優先級,先修改相關代碼,再做其他相關功能
- 針對數據到位問題;可以分批次到位,分批次測試,而非最終一口吃個胖子
- 針對測試方法繁瑣複雜;可以實現自動化測試,提供測試工具
自動化測試,本不應該屬於這片內容。
作爲聯網戰鬥的實現,處理同步問題,是重中之重。
處理方向有兩個:
- 預防不同步,在出現前,扼殺在搖籃中
- 解決不同步,當出現時,如何快速定位並解決
預防不同步,就是使用自動化測試的方案,針對策劃所配置的所有項都提前做好測試。做一個改動後,就相關影響的戰鬥,都多跑幾遍;就是把人力做的事情,通過技術來自動化跑。
解決不同步,可以分爲三個部分:
- 收集
- 重現
- 驗證
收集,就是當出現不同步問題的時候,我們一定要知道;
重現,就是我們要對現場進行重現,這樣就方便查找問題,進而驗證是否解決問題。
這些通過上面優化方案的自動化工具都已經初具模型。
番外:集思廣益
以前做東西,習慣悶頭開發,因爲爲了做這方面的內容,查閱資料,實驗功能,做了各種各樣的努力,對這方面的認知和理解還是有一定的把控的。
但是,“不識廬山真面目,只緣身在此山中”,有時候往往會自己把自己限制住,對一個問題不同高度,不同角度,不同層次的解析,也是值得嘗試的。
於是,這次優化,準備了以下內容,然後先在內部範圍開了個會,收集大家的意見:
- 整個機制的框架
- 上次測試的數據
- 針對測試數據分析的原因
- 針對分析的原因,準備做的優化方向
事實證明,通過這次會議,收集到了很多有用的建議與方案,回去做了針對性的調整,再去實施。
番外:延遲與平滑的博弈
聯網戰鬥的效果,最終都是歸咎與延遲與平滑的博弈,效果平滑,完全可以通過最粗暴的高延遲實現,但是動作遊戲的高延遲還是比較影響玩家體驗的,所以就需要不斷的調整優化,找到那個平衡點。
下面是一個經典的樣例:
- 以當前幀池數量 n 爲準,差值釋放,假定平均3次釋放完畢,每次釋放 n/3
- 注重延遲,當需要快播,每次選擇最大釋放幀數釋放(3幀),保證3次釋放即可恢復(100ms內)
- 假設幀池中有5幀,選擇
- 釋放3幀 => 5-3=2 ,已恢復現場
- 假設幀池中有5幀,選擇
- 注重平滑,當需要快播,每次選擇儘可能平滑的方式去釋放幀
- 假設幀池中有5幀,選擇
- 釋放2幀 => 5-2=3
- 釋放2幀 => 3-2=1 ,已恢復現場
- 假設幀池中有5幀,選擇
- 平衡平滑與延遲,指定每個數值緩存幀時的緩存策略,
注意:
- 快播時,是否重新計算該釋放的幀數
番外:電腦與收集MD5不一致
在重現不同步時,手機產生的MD5與PC產生的MD5不一致。
經過一系列實驗檢測發現,在時間種子傳輸過程中(Lua -> C++),種子的數據類型發生變化,導致種子數值發生了改變。
番外:卡頓反饋
爲了更近一步手機玩家反饋,同時也不影響玩家正常體驗。
在聯網戰鬥後,結算界面添加反饋按鈕,收集第一手玩家信息。
然後,設定一個卡頓閾值來自動彈出反饋面板(該面板允許玩家選擇永久不自動彈出)。
參考: