表演的藝術,妖尾回合制戰鬥系統客戶端設計
妖尾歷經幾年開發,終於在今年6月底順利上線,筆者從2017年初參與開發,主要負責妖尾戰鬥系統開發。戰鬥作為遊戲的核心玩法系統,涉及很多技術點,希望能借幾篇文字,系統性總結MMORPG戰鬥系統的開發經驗。
本文主要從巨集觀層面總結回合制遊戲戰鬥的美術資源規範,系統框架設計和主要技術點,比如斷線重連,技能表演等。
系列博文傳送門:
記錄戰鬥記錄你,詳解妖尾戰鬥錄影系統
美術資源規範
模型
模型分為低模(1500-2000面)、高模(6000-10000面)兩種規格,戰場單位統一使用低模,但在合體技等鏡頭動畫表演使用高模。主角模型是由頭、上衣、武器、下裝4部分組成的,遊戲中通過網格、貼圖合併成1個完整模型進行展示,這樣可以實現部件換裝。非主角模型比較簡單,直接載入完整模型。
掛點
高低模都有頭、腳、血量、受擊、左右手、左右腳等掛點,高模相比低模額外多了表情掛點(下文解釋掛點作用)。
材質球
高模跟低模使用不一樣的材質球。低模全身只用了一種材質球,而高模用了兩種材質球,臉部和身體分別是不同材質球,臉部材質球實現了uv動畫用於做表情變化。高低模的身體材質球都實現了描邊,高模額外開啟了自陰影。高模貼圖為256x256大小png圖片,低模為128x128大小png圖片,貼圖都是寬高相等的POT尺寸,這樣Android/IOS可以分別使用ECT2/PVRTC壓縮格式。
表情實現
人物表情是通過shader uv動畫實現的,索引0-3從分別對應下面貼圖的4個表情。由於shader是專案TA編寫輸出的,要讓動作美術能夠控制表情變化,我們定了個表情掛點位置對映索引的規則,表情掛點x軸數值除以100向下取整即為索引,動作美術在動畫時間軸裡只需要編輯表情掛點的位置,通過程式轉換設定shader引數,就能控制表情變化。
動畫
戰鬥單位的動畫狀態機具有非常多的狀態,有多達60+多個動畫,但常用動畫只有其中幾個,所以戰鬥單位不會在進入戰場時一次性載入所有動畫,預設只加載站立、受擊、奔跑、死亡等4種動畫。其他動畫則每回合按需載入,我們會按角色預先儲存動作和對應資源路徑的配置表,需要用到的動作查表獲取路徑載入資源,作為AnimationClip載入到RuntimeAnimatorController上。另外,像受擊浮空等動畫還需要處理好依賴,相關的過渡動畫也要一併載入。
技能
技能是使用Flux編輯器製作出來的,通過時間軸上建立多個Sequance軌道來組成一段技能表演,每個Sequance指令碼負責1種表現,如角色移動、播放特效等,Sequence指令碼共同作用就能表現出一段技能。1個技能最終生成動作、音訊共2個Prefab。1個戰鬥單位擁有的技能也非常多,不會在進入遊戲時一次性載入,也是每回合按需載入要用到的技能。
Buff
Buff相比技能表現要簡單,因為最多隻有新增、持續、觸發、移除等4個階段需要做表現,每個buff prefab掛相應的4個指令碼,配置特效資源,人物動作,替換材質即可。
美術資源流水線
主角模型與非主角模型的資源提交規範稍有不同,但製作流水線基本是一樣的。美術提交包括模型fbx、動作fbx、動畫機,材質球、貼圖等資源,通過工具指令碼進行資源檢查、預處理,生成預製件到指定目錄。
圖解戰鬥框架
一套戰鬥框架其實包括了很多內容,一篇文章難以講清所有細節。不過筆者嘗試畫圖總結了戰鬥按功能劃分的各個模組,希望儘量講清基本模組的內容,模組之間的關係,從而在巨集觀層面瞭解戰鬥系統。
骨架——戰鬥狀態機
架構圖紫色部分為PlayMaker指令碼集合,如果將戰鬥框架理解為人,那PlayMaker狀態機就是人的骨架,它串聯了整個戰鬥流程。妖尾戰鬥採用了PlayMaker外掛視覺化編輯整個戰鬥流程,這樣易於編輯,追蹤整個戰鬥流程,直觀地將戰鬥分成始化、表演、指令選擇三大戰鬥狀態。各戰鬥狀態基本為線性流程,戰鬥狀態之間則通過全域性事件進行轉移。
另外一點是,我們希望儘量用lua實現戰鬥邏輯,PlayMaker外掛原生只支援C#,為了支援Lua,我們實現了繼承C#狀態機行為基類(FsmStateAction)的子類,該類負責驅動Lua指令碼,Lua指令碼實現跟FsmStateAction類同樣的介面和行為,這樣就可以用Lua編寫狀態機邏輯了,程式碼基本實現如下:
namespace HutongGames.PlayMaker.Actions
{
public class LuaFsmStateAction : FsmStateAction
{
public string luaFileName = "";
private LuaTable _luaTable;
private LuaFunction luaOnEnter = null;
private LuaFunction luaOnExit = null;
private LuaFunction luaOnUpdate = null;
public override void OnEnter()
{
if (_luaTable == null && !string.IsNullOrEmpty(luaFileName))
{
LuaSupport.DoFile(luaFileName);
LuaFunction luaFunction = LuaSupport.lua.GetFunction(luaFileName + ".create");
if (luaFunction != null)
{
_luaTable = luaFunction.Invoke<LuaFsmStateAction, LuaTable>(this);
luaFunction.Dispose();
}
if (_luaTable != null && _luaTable.IsAlive)
{
luaOnEnter = _luaTable.GetLuaFunction("OnEnter");
luaOnExit = _luaTable.GetLuaFunction("OnExit");
luaOnUpdate = _luaTable.GetLuaFunction("OnUpdate");
}
else
{
Debug.LogError("Cannot find lua class " + luaFileName);
Finish();
return;
}
}
SafeCall(luaOnEnter);
}
public override void OnExit()
{
SafeCall(luaOnExit);
}
public override void OnUpdate()
{
SafeCall(luaOnUpdate);
}
private void SafeCall(LuaFunction func)
{
if (func != null && func.IsAlive)
{
func.Call();
}
}
}
}
大腦——戰鬥控制器
戰鬥的核心管理器就是架構圖底下藍色部分的戰鬥控制器,它是戰鬥系統的大腦。戰鬥控制器負責接收協議資料,驅動戰鬥邏輯。
戰鬥控制器有兩種方式接收資料輸入。對於通常的聯網戰鬥,底層網路層接收後臺協議資料,再傳輸給戰鬥控制器。妖尾還在新賬號進入遊戲時,設計了一場戰鬥用於展示關鍵劇情,這場戰鬥則是離線模擬戰鬥。我們單獨實現了模擬戰鬥控制器,它負責根據策劃配表生成模擬協議資料,傳輸給戰鬥控制器驅動戰鬥邏輯。
協議設計
整個戰鬥流程的協議設計如下圖所示,可以分為戰場初始化,等待加入戰場,戰前表演,回合選招,回合表演,戰鬥結束等6個階段。戰鬥控制器收到不同的協議包切換PlayMaker狀態,進而改變戰鬥流程。
一場戰鬥是由一組連續的協議資料組成的。如果由於客戶端卡頓,切出後臺等原因,出現前一個協議包還未處理表現完,下一個協議包已經到了,忽略協議包不處理,或者粗暴切斷當前邏輯,直接處理下一個協議包都是不可取的,可能導致戰鬥表現異常。因此戰鬥控制器設計了協議快取佇列,用於快取順序處理協議資料,然而快取佇列並不是簡單地順序處理資料就能萬事大吉了,如果不加以考慮處理斷線重連的情況,就會碰到像戰鬥進度嚴重延遲,甚至卡死等情況。
斷線重連
戰鬥控制器的一大要務就是處理好戰鬥中的斷線重連,恢復並修正戰鬥流程。簡單來看,戰鬥中斷線重連有兩大類情況:斷線重連後戰鬥已結束;斷線重連後戰鬥未結束。
第一種情況比較簡單,斷線重連的登陸包帶有玩家是否處於戰鬥中的標誌位,如果當前不處於戰鬥中,前臺卻仍處於戰鬥場景中,則清掉所有戰鬥協議快取,執行退出戰鬥的邏輯。
第二種情況則要細分多種情況討論。一般斷線重連後,戰鬥協議快取佇列可能存有多個戰鬥協議,需要確認協議資料是否仍為原來那場戰鬥的。簡單判斷原則就是,如果佇列中收到初始戰場包,且其戰場ID與之前協議不同,可以認為斷線重連回來後已開始了另一場新戰鬥,舊戰鬥資料已失效,直接清出快取,開始處理表現新戰鬥。
接著考慮斷線回來後還在原戰鬥的情況,戰鬥設計上斷線重連必然會收到初始戰場包,戰場包帶有當前戰鬥階段的標誌位,根據標誌位即可還原戰場狀態:標誌位為戰前表演,回合表演階段,該客戶端馬上傳送表演結束Req,等待服務端通知下回合開始,避免拖慢戰鬥進度;標誌位為回合選招階段,客戶端切為選招介面,並根據階段開始時間戳修正剩餘選招時間。
肉身——戰鬥資源
戰鬥資源理所當然就是戰鬥系統的肉身了。管理資源的難點在於合理載入解除安裝,如人有四肢五官,協調越好,運動效能越強,越節省體力。
資源管理策略
資源 | 載入策略 | 快取策略 |
---|---|---|
戰鬥場景 | 登入預載入 | 常駐記憶體 |
全屏背景圖 | 根據場景切換 | 常駐一張圖 |
戰鬥HUD | 高配登入預載入; 低配入場預載入 |
高配常駐記憶體; 低配戰後解除安裝 |
功能模組UI | 戰中即時載入 | 出戰鬥解除安裝 |
通用特效 | 高配登入預載入; 低配入場預載入 |
常駐記憶體 |
己方模型 | 進戰鬥預載入 | 高配快取到下一場戰鬥,無命中則戰後解除安裝; 低配戰後解除安裝 |
敵方模型 | 進戰鬥預載入 | 戰後解除安裝 |
骨骼動畫 | 入場載入基本動畫, 其餘動畫按需回合載入 |
戰後解除安裝 |
技能 | 回合按需載入 | 快取一回合,不命中則淘汰 |
Buff | 回合按需載入 | 戰後解除安裝 |
上圖簡述了戰鬥系統涉及的主要資源及載入快取策略,一言蔽之,就是既要體面,又要節約。
我們希望遊戲體驗儘量流暢,在社群場景遭遇戰鬥時能秒切進入戰鬥,所以:
- 最基本的戰鬥場景和全屏圖資源,高低配機都會預載入好並常駐記憶體;
- 本著節約原則,其他資源儘量做到不影響體驗,用時載入,不用時戰後解除安裝。
- 骨骼動畫,技能,Buff等資源每回合根據表演內容再按需載入;
- 模型方面可以預判像玩家自己,隊友等己方模型經常複用,高配機會快取至下一場戰鬥,無命中再戰後解除安裝,戰鬥HUD也會常駐記憶體;低配機資源比較緊張,還是用戰後即時解除安裝的策略。
資源使用策略
另外,一場戰鬥表現少說也會涉及數十個資源的非同步載入,如果每處表演邏輯都要非同步等待資源載入回撥,很容易導致回撥地獄。因此戰鬥狀態機特意將資源載入,資源使用劃分成兩個階段。每回合等待表演所需資源全部非同步載入完畢,才能進入到表演階段,表演邏輯按同步方式使用資源即可。由於資源載入粒度細分到以回合為單位來載入,實測資源載入等待並不會影響戰鬥表演的流暢體驗。
資源ab打包策略
講到資源管理,ab打包是個繞不開的話題。ab打包粒度越細,包數量越多,IO壓力大;ab打包粒度越粗,資源越冗餘,包體,熱更新資源量都會變大,說到底是平衡的藝術。
資源 | 打包策略 |
---|---|
戰鬥場景 | 單獨打包 |
全屏背景圖 | 每張圖單獨打包 |
戰鬥HUD | HUD集合打包,HUD上的動態小圖示按類別集合打包 |
功能模組UI | 按模組集合打包 |
通用特效 | 所有通用特效集合打包 |
主角模型 | 每個主角各個模型部件單獨打包,各個骨骼動作單獨打包 |
非主角模型 | 每個角色為單位打包 |
技能 | 每個技能單獨打包,技能引用資源分普通技能,合體技兩類,再按角色為單位打包 |
Buff | 所有Buff集合打包 |
簡單羅列了戰鬥相關資源的ab打包策略,原則上是儘量按資源使用耦合程度劃分打包,可能一起使用的資源,打包到一起,如果資源過多,就要進一步拆分ab包。再者,做好提前設計,確保打包策略在未來資源量堆起來後仍能適用。比如,主角模型的模型部件,骨骼動作非常多且在未來很有可能新增,可以每個資源單獨打包;非主角模型模型,骨骼動作數量相對固定,就能以角色單位打包。規劃好ab打包策略後,跟美術約定好規則來提交資源目錄及資源,就能編寫工具根據配表,不同目錄執行不同的ab打包策略。
戰鬥表演
戰鬥表演大體分為技能和Buff兩類表演。技能是有開始結束的一段表現,小到普通攻擊,大到多人合體技,都是技能表演;Buff則是附在單位上的持續性狀態表現,如人物的中毒,封印狀態表現。
技能表演
正如前面的框架圖裡提到妖尾戰鬥有很多表演指令碼,可綜合對角色,UI,場景,鏡頭,節奏做全方位的排程控制,從而表現一段技能。伽吉魯和蕾比兩個角色的合體技是非常有代表性的一段技能表演,涵蓋了對很多技能指令碼的應用,簡單舉例講解這個合體技的實現,就可以瞭解技能是怎麼編輯,表演的。下圖是合體技的遊戲表現:
總體來看,這個合體技由鏡頭動畫+技能打擊兩部分構成,這兩部分都是在同一條時間軸通過指令碼組合運用編輯出來的,最後生成一個合體技預製件。
圖中紅色部分是鏡頭動畫實現指令碼:
- 動畫掛靠指令碼帶有Animator元件,控制相機和角色的位置旋轉。
- 動畫掛靠指令碼帶有相機掛點,啟用場景鏡頭動畫相機並放到掛點下
- 動畫掛靠指令碼帶有角色掛點,生成對應角色高模例項,放到掛點下,並播放合體技動作
- 動畫掛靠指令碼每幀根據角色表情掛點更新角色表情
- 特寫角色特效指令碼例項生成包括角色底下的IRON文字特效,光影粒子特效,速度線UI特效等,放置到相對鏡頭相機指定位置播放
- 線光源設定指令碼修改戰鬥環境光
- 替換材質指令碼替換伽吉魯模型材質
- 相機徑向模糊指令碼啟用相機後處理特效
不難看出鏡頭動畫的主要邏輯是由動畫掛靠指令碼實現的,主要鏡頭,角色走位排程由美術實現Animator進行控制。視鏡頭動畫的複雜效果,可能會堆多一些特寫特效指令碼同步播放,豐富畫面效果。
戰鬥系統運用了幾個透視相機,按相機深度由低到高分別是:
- 戰鬥背景圖相機
- 戰鬥單位名字相機
- 戰鬥UI相機
- 戰鬥主相機
- 戰鬥鏡頭動畫相機
- 戰鬥鏡頭動畫UI相機
鏡頭動畫播放完,緊接著就是綠色部分指令碼,配合完成技能釋放:
- 移動指令碼控制伽吉魯跑到戰場中央
- 播放特效,動作指令碼控制伽吉魯做出打擊表現
- 受擊指令碼控制目標做出受擊動作,扣血反饋
- 震屏指令碼,角色抖動指令碼配合著加強打擊感
- 回位指令碼控制伽吉魯回到自己的站點
技能釋放需要由更多的指令碼組合完成,一般不需要美術產出很多資源,利用一些簡單攻擊特效,配置角色走位,動作,受擊,鏡頭控制就能做出漂亮打擊感的技能。
Buff表演
Buff表演相比技能表演更簡單,容易編輯,實現。每種Buff都可以分為Buff新增,Buff持續,Buff觸發,Buff移除4個階段,視需求自由決定每個階段是否有具體表現,Buff編輯器只需配置每個階段的特效,人物動作,替換材質即可。下圖是反擊Buff的遊戲表現,4個階段都有特效表現。當然,也存在一些Buff是設計成完全無表現的。
結語
至此本文就結束了,主要還是就美術資源,資源管理,協議互動,戰鬥表演做了些介紹,內容並沒有涵蓋整個戰鬥系統,不過已是戰鬥系統核心設計內容,特此記錄,也希望能提供一些經驗借鑑