記錄戰鬥記錄你,詳解妖尾戰鬥錄像系統

  • 2019 年 10 月 3 日
  • 筆記

妖尾歷經幾年開發,終於在今年6月底順利上線,至今運營兩個多月,筆者從2017年初參與開發,主要負責妖尾戰鬥系統開發,一路解決了一些技術問題,踩了一些坑,感覺有不少點是值得記錄和分享的,希望能借幾篇文字,系統性總結MMORPG戰鬥系統的開發經驗。
本文主要介紹戰鬥錄像系統,戰鬥錄像基本是所有MMORPG遊戲的標配系統,它同時也能成為開發調試利器,在整個開發階段扮演重要角色。

首先是調試利器

一些項目組開發戰鬥系統時,可能會優先開發涉及表現的相關功能,迭代的新增戰鬥表現,修復Bug,直到整個戰鬥表現看起來相當完整了,到了後期再應策劃要求,補充戰鬥錄像系統。筆者項目是在開發中期加入戰鬥錄像功能的,在經歷完整個戰鬥開發階段後,得出的經驗是儘可能在基礎框架搭建、前後台開始聯調階段就同步開發戰鬥錄像系統,利用戰鬥錄像來輔助系統開發、調試。到了項目中後期,戰鬥錄像會發揮更大的用處,此時戰鬥系統已經提交到SVN版本控制,項目組所有人都可以體驗到戰鬥系統,所有人都或多或少地扮演測試人員的角色,項目群會頻繁地反饋戰鬥系統的表現問題,諸如報錯、卡死,單位詐屍等等,什麼反饋都會有,總之發揮你的想像力。當時開發會頻繁地奔波於各個項目組成員的電腦面前,溝通、查看日誌,嘗試弄清問題,有了戰鬥錄像後,我們會讓對方給錄像文件,在本地環境重放戰鬥錄像,重現現場慢慢定位問題。
戰鬥錄像能多大程度的輔助開發調試,取決於相關工具鏈有多完善,下面介紹妖尾項目對於工具鏈的打造。首先簡單看下戰鬥錄像框架:


一般來說,網絡底層往上還會有一層業務網絡層,妖尾的業務網絡層分成兩個,一個負責普通業務邏輯,一個網絡層供戰鬥專用。通過在戰鬥網絡層接口插樁,戰鬥錄像模塊就能收集一場戰鬥的所有數據。戰鬥結束後,自動將該場戰鬥數據保存成本地錄像文件,當然,我們還要提供手動保存錄像接口,以便戰鬥中途卡死了也能保存錄像。雖然有了戰鬥數據,還要配備一套完整功能的GUI工具才能提高調試效率,因此筆者基於Unity開發了戰鬥錄像播放器工具。

 

上圖是戰鬥錄像播放器經過幾次迭代後的截圖,除了實現最基本的播放錄像、查看數據功能,還有查看設備數據,上傳/下載錄像、生成戰鬥播報、差量構建指定回合戰場包等功能。筆者覺得在開發初期,先實現播放錄像、查看數據的功能就能滿足大部分調試需求了,開發時間成本只有2-3天,但它會在之後1-2個月甚至更久的前期開發階段幫你縮短調試定位時間,節省更多時間早(bu)點(cun)下(zai)班(de),或幫策劃做更多需求,重要的是解放心態,不再疲於溝通Bug,構造現場,因為現場就在錄像里。

簡單描述這套調試工具的使用姿勢:

  • 開發過程中遇到了戰鬥Bug,如果在第一時間無法判斷Bug原因,先保存錄像,再逐步分析問題。
  • 選擇報錯的戰鬥錄像,通過時間戳/快速模式重跑戰鬥,逐步縮小問題範圍:觀察戰鬥錄像播到第幾個回合報錯,是資源加載、選招還是表演階段報錯,通過報錯前的日誌,逐步定位是哪個類哪個接口的問題,再猜測並驗證某行代碼,直到問題解決。
  • 如果不是卡死報錯,戰鬥也能跑完,但策劃反饋某個技能/Buff表現與預期不同,就要查看關鍵表演包的數據,看是後台傳的有問題,還是前台表現沒做對。
  • 上面兩類問題的排查通常是無法一步到位的,排查過程會不斷追蹤代碼給可疑代碼打Log,會臨時修改某些變量,會臨時修改某段代碼邏輯,依靠不斷重跑戰鬥來驗證。
  • 解決Bug的過程也少不了跟後台的溝通,在這之前,後台重數據輕表現,前台重表現輕數據,導致一種現象就是後台找前台問表現,前台找後台問數據,溝通成本比較高。有了這套工具,前台開發對於這場戰鬥包括服務器、角色ID、戰鬥ID、戰場ID,協議數據等信息都了如指掌,快速分析出是前台問題就直接修復,是後台問題就告訴對方去修復哪塊數據。

這裡另外分享1個Bug調試修復的經驗。個人認為Bug修復總時間 = 問題溝通時間 + 問題定位時間 + 代碼修改時間 + 編譯驗證時間,像戰鬥這類大型系統,可能會經歷多輪問題定位、代碼修改、編譯驗證才能修好1個Bug。Lua代碼做好Hot reload開關,最好做到修改某處代碼,重進戰鬥就能驗證最新代碼。每次重啟遊戲至少花費30+秒,1個Bug平均幾次重啟驗證就是幾分鐘時間,做好Hot reload節省下的時間相當可觀。

初期在項目組內推行用錄像反饋戰鬥Bug時,我們讓大家把保存下來的錄像文件單發給戰鬥開發來調試,很快發現用戶體驗並不友好,不是所有人都是開發,大家不清楚錄像保存到哪個目錄了,找到目錄,他們也弄不清楚要發哪個錄像給開發。在忍受了一段時間的靈魂三連問後,筆者又加上了錄像上傳/下載功能。

上面兩張圖是錄像上傳/下載流程及錄像下載頁面。我們將Bug反饋操作簡化成遊戲內一鍵反饋,點擊按鈕就能自動保存錄像文件,並將二進制文件數據Base64編碼成字符串,利用魔方質管組幫忙搭建的Web服務,通過Http請求將數據上傳到Web服務器保存數據庫,開發通過Web頁面就可以搜索/下載base64字符串格式的錄像文件,最後錄像播放接口做適配,支持二進制/base64字符串兩種格式數據的錄像播放,整個環節就打通了。

開發階段我們自行開發了戰鬥錄像來輔助調試,確實也是到了戰鬥系統基本穩定後,策劃們才前後提了戰鬥錄像的正式需求,先做了一版基於服務器保存的活動錄像,又做了一版基於客戶端保存的戰鬥錄像大廳。

前後做這兩版錄像需求,雖然都是觀看錄像,但其實現大不相同,因此需要謹慎設計整個錄像模塊,讓兩套邏輯獨立並行,能共用底層功能,並盡量保持外部接口一致性。上圖是整個戰鬥錄像的模塊劃分,可劃分為實現戰鬥錄像基礎功能的核心模塊,及涉及界面UI的兩版業務功能模塊。BattleReplayManager是核心類,它對外接收錄像相關的控制請求,對內調度其他核心模塊類,獲取/保存/構造數據,控制錄像播放流程,並通過給戰鬥網絡層發送協議數據影響戰鬥表現。

服務器錄像

基於服務器保存的活動錄像,所有數據都由服務器提供。前台首先發送觀看錄像請求,接收錄像概要數據包,獲取戰鬥波次/回合等信息用於顯示和跳回合。收到初始戰場包後進入戰鬥,在每回合表演完後請求下一回合表演數據。正常播放錄像時,收到的協議數據跟普通戰鬥是一樣的,但如果在戰鬥中途跳回合,除了新回合的表演包,還會收到新回合的戰場包,用於恢復新回合初的戰場單位狀態。這個過程跟戰鬥斷線重連恢復戰場是同一套邏輯,因此把戰鬥斷線重連的坑填完,實現服務器錄像基本沒有難點。

客戶端錄像

相對服務器錄像,實現基於客戶端保存的錄像功能要考慮比較多問題:

  1. 確定錄像數據結構,用什麼數據結構存儲一場戰鬥的所有協議及相關信息較優?
  2. 保證錄製數據完整性。網絡抖動、切出遊戲再切回來等場景可能會導致少了某回合表演數據怎麼辦?
  3. 如何實現跳回合。一場正常戰鬥的協議包,除了初始戰場包,每個回合只有表演包,沒有戰場包,跳回合怎麼恢復戰場狀態?
  4. 錄像上傳/下載的傳輸策略。協議收發有64kb限制,錄像文件大小超過了怎麼辦?
  5. 保證用戶體驗。評估極限情況的錄像文件大小,保證流暢的錄像觀看體驗。

模塊開發初期就考慮這些問題,就可以避免基礎設計出錯,後期積重難返的尷尬情況。

1. 錄像文件結構

首先是確定錄像文件格式,由於妖尾協議基於pb通信,錄像文件一開始就沒有打算自定義二進制格式,而是直接基於pb定義數據結構,這樣有幾點好處:

  1. pb傳輸效率高,而且開發熟悉pb,不像自定義格式還有理解成本,開發效率也高。
  2. 協議與錄像文件採用同種格式,比較容易根據查看列表,上傳/下載錄像等業務去反推最優的錄像文件數據結構。讓每份錄像文件既可以有戰鬥錄像數據,也有關於錄像大廳的業務數據,一次設計,解決兩個問題。
  3. pb支持數據結構嵌套,列表,能做出錄像頭、錄像數據塊設計,上傳/下載協議也容易切分錄像文件做分塊傳輸。

基於幾點考慮,錄像文件由BattleReplayFile錄像頭、BattleReplayFileBlock錄像數據塊兩部分組成。BattleReplayFile的blocks字段用於存放BattleReplayFileBlock列表,BattleReplayFile其他字段是概要信息。這樣查看錄像列表時,後台只需要返回不帶blocks數據的BattleReplayFile列表即可。上傳/下載錄像時也可以先傳錄像頭、再批量分次傳錄像數據塊。

message BattleReplayFile
{
    optional string name = 1;                       // 錄像文件名
    repeated BattleReplayFileBlock blocks = 2;      // 協議文件塊
    optional uint32 block_num = 3;                  // 協議文件總塊數
    repeated string ext_info_keys = 4;              // 錄像額外信息參數Key
    repeated string ext_info_values = 5;            // 錄像額外信息參數Value
    ... // id、時間、雙方成員、回合、波次等錄像概要信息
    ... // 簡介、點贊、收藏等錄像大廳業務信息
}

message BattleReplayFileBlock
{
    optional uint32 index = 1;                  // 協議塊序號
    optional string name = 2;                   // 協議類名
    optional bytes data = 3;                    // 協議數據
    ... //時間、回合等其他信息
}

2. 錄像文件校驗

網絡抖動、切出遊戲再切回來等情況導致斷線重連,可能導致戰鬥錄像數據損壞,因此保存本地前先做錄像文件校驗,判斷有沒有丟關鍵協議包,包括初始戰場包、入包表演包、各回合表演包及退出戰場包,保證協議包序,通過校驗才保存錄像文件,不通過就提示玩家錄像數據損壞無法保存。

3. 錄像回合跳轉

一場戰鬥錄像單靠收到的協議包,可以正常順序播放整個戰鬥,卻不能跳轉回合播放,因為中間跳過了幾回合的表演演算,戰鬥邏輯層無法將戰場數據修正成跳轉回合的狀態。服務器錄像可以依靠後台發跳轉回合戰場包做恢復,客戶端錄像就要靠前台自己處理,用錄像表演包演算出跳轉回合的戰場狀態。

第一直覺是在戰鬥邏輯層處理跳出的表演包,只是跳過表演,直接做數據演算,但稍加思考會發現有很多問題:戰鬥邏輯層里,數據與表現基本耦合在一起,畢竟這樣的編碼實現方式最直觀。想抽離表現只演算數據,只能在原有代碼里加ifelse分支,重寫數據演算邏輯。幾十個表演類,新增這麼多分支,編碼再加調試,必然失去對代碼的把控,也破壞了原有系統穩定性。即使哼哧哼哧硬寫下來,也會發現只實現了向後跳轉回合,沒實現向前跳轉回合,因為戰鬥邏輯層實現的是按回合往下演算的邏輯。

跳出這個誤區,我們認為戰鬥錄像數據應該要有每個回合的戰場包,跳轉時供戰鬥邏輯層重置回合戰場,因此後台修改了戰鬥邏輯,每回合都會發當回合戰場包,這些戰場包做了特殊標記,只用於錄像存儲,不會影響戰鬥邏輯,實現起來很快,但也清楚有明顯效率問題。

基本上,戰場包都會比表演包大,甚至大很多,如果某個回合技能不太複雜,那表演包數據其實非常小,為了實現跳回合,由後台給每個回合加發戰場包,會非常影響戰鬥的協議數據量,保存錄像文件變大,也會增加上傳/下載錄像時的負擔。這麼實現不合理的點在於,每回合戰場包其實是冗餘數據,每回合狀態是可以通過初始戰場包加表演包推算出來的。為了優化這個問題,前台實現了一個戰場包構建器,以初始戰場包、回合1~n-1表演包為輸入,輸出目標回合n的戰場包。這樣在保存錄像時不需要保存回合戰場包,錄像跳轉回合時由構造器動態生成戰場包即可。編寫調試戰場包構建器時,要注意檢查前後台的戰場包差異,我們會打印戰場包數據,通過Beyond Compare查看差異,不斷調整代碼,直到構建的關鍵數據一致為止。戰場包構建器調試好後,只要後續不新增表演類型,就可以保證構建器可信可用,即使新增表演,代碼工作量也很少。

優化完做下簡單測試,打了一場40回合的5v5 pvp戰鬥保存錄像,比較兩種方案的保存錄像文件大小:優化後文件大小是優化前的65%,減少了252KB,由於5v5pvp表演複雜,因此回合表演包數據本身也非常多,換做是一般的戰鬥,數據優化比率會更高。

4. 錄像上傳/下載策略

妖尾一次協議收發有64KB大小限制,看前面的數據可知,回合數比較多的戰鬥錄像文件大小肯定會超過64KB,我們既不希望上傳/下載錄像單次傳輸的數據量超過64KB,又不希望單次傳輸數據量太少,導致協議發送次數過多,浪費太多時間在RRT上,因此採用的錄像傳輸策略是,首次傳輸單獨發送錄像頭,後續傳輸錄像數據塊切塊傳輸,保證每次傳輸的所有BattleReplayFileBlock的data總大小不超過50KB。採用這樣的策略,5回合以內的小型戰鬥基本都能分2次傳輸完畢,像上面的5v5 pvp大型戰鬥則需要進行11次傳輸。這就引出了下個問題思考,大型戰鬥的錄像觀看會不會有體驗問題。

5.流式傳輸及錄像緩存

戰鬥錄像大廳的設計初衷,是讓玩家可以自主分享/觀看他們覺得滿意的戰鬥錄像,所以我們猜測玩家會比較多的上傳/下載/觀看大型pvp戰鬥錄像,對於上傳而言並不會有什麼問題,因為就是一次性操作,但對下載/觀看場景就要盡量進行優化,我們不希望玩家每次看錄像,都要有感知地等待一會,等上10次網絡回包,下載完錄像文件才能觀看錄像,也不希望玩家每次看錄像都得重複下載文件,對玩家的手機流量也很不友好。

針對這兩點問題,戰鬥錄像參考網絡視頻的做法,加上了流式傳輸及錄像緩存的特性。

如上圖所示,流式傳輸的目的在於優化玩家觀看新錄像的體驗,不管一個完整的錄像有多大,需要多少次傳輸才能完成,只需要先獲得部分頭部數據,就能觀看錄像。前台只需要頭2次回包,獲取錄像概況、初始回合戰場包和表演包,就足以表演第1回合的戰鬥,進入錄像戰鬥後,靜默下載其餘的錄像數據,一般後續的錄像數據下載速度遠遠快於戰鬥表演速度,這樣完全不影響整場戰鬥的錄像觀看。假設網絡環境極端惡劣,表演完當前回合戰鬥後,後續錄像數據還沒返回,BattleReplayManager會每幀輪詢等待下個回合表演數據,即使網絡斷掉了拿不到數據,玩家仍然可以點擊按鈕退出戰鬥錄像。

錄像緩存的目的則在於優化玩家重複觀看錄像的體驗,減少流量消耗。當看過一次錄像,下載了完整的錄像數據後,前台就會把錄像保存到本地緩存起來了,儘管錄像頭裡存儲了部分戰鬥錄像大廳的字段,比如點贊、收藏數等,這些字段數據會失效,但戰鬥數據是不會變的。查看大廳的錄像列表時,後台會返回只有錄像頭BattleReplayFile,沒有數據塊BattleReplayFileBlock的列表,玩家請求觀看時,判斷本地緩存有沒有該錄像緩存,有就不再走原來的下載流程,直接讀取緩存文件播放即可。

洋洋洒洒寫了一些關於戰鬥錄像的總結,也確實是因為錄像系統對戰鬥開發調試有所幫助,作為一個功能系統,也需要在早期考慮一些問題,做設計和優化,希望本文能對MMORPG或其他類型遊戲戰鬥的設計開發,提供一些借鑒經驗。

附上我們的遊戲官網[妖精的尾巴:魔導少年],快來玩吧~