1. 程式人生 > >記錄戰鬥記錄你,詳解妖尾戰鬥錄影系統

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

妖尾歷經幾年開發,終於在今年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或其他型別遊戲戰鬥的設計開發,提供一些借鑑經驗。

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