妖尾歷經幾年開發,終於在今年6月底順利上線,筆者從2017年初參與開發,主要負責妖尾戰鬥系統開發。戰鬥做爲遊戲的核心玩法系統,涉及不少技術點,但願能借幾篇文字,系統性總結MMORPG戰鬥系統的開發經驗。
本文主要從宏觀層面總結回合制遊戲戰鬥的美術資源規範,系統框架設計和主要技術點,好比斷線重連,技能表演等。html
系列博文傳送門:
記錄戰鬥記錄你,詳解妖尾戰鬥錄像系統緩存
模型分爲低模(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個戰鬥單位擁有的技能也很是多,不會在進入遊戲時一次性加載,也是每回合按需加載要用到的技能。ide
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 | 回合按需加載 | 戰後卸載 |
上圖簡述了戰鬥系統涉及的主要資源及加載緩存策略,一言蔽之,就是既要體面,又要節約。
咱們但願遊戲體驗儘可能流暢,在社區場景遭遇戰鬥時能秒切進入戰鬥,因此:
另外,一場戰鬥表現少說也會涉及數十個資源的異步加載,若是每處表演邏輯都要異步等待資源加載回調,很容易致使回調地獄。所以戰鬥狀態機特地將資源加載,資源使用劃分紅兩個階段。每回合等待表演所需資源所有異步加載完畢,才能進入到表演階段,表演邏輯按同步方式使用資源便可。因爲資源加載粒度細分到以回合爲單位來加載,實測資源加載等待並不會影響戰鬥表演的流暢體驗。
講到資源管理,ab打包是個繞不開的話題。ab打包粒度越細,包數量越多,IO壓力大;ab打包粒度越粗,資源越冗餘,包體,熱更新資源量都會變大,說究竟是平衡的藝術。
資源 | 打包策略 |
---|---|
戰鬥場景 | 單獨打包 |
全屏背景圖 | 每張圖單獨打包 |
戰鬥HUD | HUD集合打包,HUD上的動態小圖標按類別集合打包 |
功能模塊UI | 按模塊集合打包 |
通用特效 | 全部通用特效集合打包 |
主角模型 | 每一個主角各個模型部件單獨打包,各個骨骼動做單獨打包 |
非主角模型 | 每一個角色爲單位打包 |
技能 | 每一個技能單獨打包,技能引用資源分普通技能,合體技兩類,再按角色爲單位打包 |
Buff | 全部Buff集合打包 |
簡單羅列了戰鬥相關資源的ab打包策略,原則上是儘可能按資源使用耦合程度劃分打包,可能一塊兒使用的資源,打包到一塊兒,若是資源過多,就要進一步拆分ab包。再者,作好提早設計,確保打包策略在將來資源量堆起來後仍能適用。好比,主角模型的模型部件,骨骼動做很是多且在將來頗有可能新增,能夠每一個資源單獨打包;非主角模型模型,骨骼動做數量相對固定,就能以角色單位打包。規劃好ab打包策略後,跟美術約定好規則來提交資源目錄及資源,就能編寫工具根據配表,不一樣目錄執行不一樣的ab打包策略。
戰鬥表演大致分爲技能和Buff兩類表演。技能是有開始結束的一段表現,小到普通攻擊,大到多人合體技,都是技能表演;Buff則是附在單位上的持續性狀態表現,如人物的中毒,封印狀態表現。
正如前面的框架圖裏提到妖尾戰鬥有不少表演腳本,可綜合對角色,UI,場景,鏡頭,節奏作全方位的調度控制,從而表現一段技能。伽吉魯和蕾比兩個角色的合體技是很是有表明性的一段技能表演,涵蓋了對不少技能腳本的應用,簡單舉例講解這個合體技的實現,就能夠了解技能是怎麼編輯,表演的。下圖是合體技的遊戲表現:
整體來看,這個合體技由鏡頭動畫+技能打擊兩部分構成,這兩部分都是在同一條時間軸經過腳本組合運用編輯出來的,最後生成一個合體技預製件。
圖中紅色部分是鏡頭動畫實現腳本:
不難看出鏡頭動畫的主要邏輯是由動畫掛靠腳本實現的,主要鏡頭,角色走位調度由美術實現Animator進行控制。視鏡頭動畫的複雜效果,可能會堆多一些特寫特效腳本同步播放,豐富畫面效果。
戰鬥系統運用了幾個透視相機,按相機深度由低到高分別是:
- 戰鬥背景圖相機
- 戰鬥單位名字相機
- 戰鬥UI相機
- 戰鬥主相機
- 戰鬥鏡頭動畫相機
- 戰鬥鏡頭動畫UI相機
鏡頭動畫播放完,緊接着就是綠色部分腳本,配合完成技能釋放:
技能釋放須要由更多的腳本組合完成,通常不須要美術產出不少資源,利用一些簡單攻擊特效,配置角色走位,動做,受擊,鏡頭控制就能作出漂亮打擊感的技能。
Buff表演相比技能表演更簡單,容易編輯,實現。每種Buff均可以分爲Buff添加,Buff持續,Buff觸發,Buff移除4個階段,視需求自由決定每一個階段是否有具體表現,Buff編輯器只需配置每一個階段的特效,人物動做,替換材質便可。下圖是反擊Buff的遊戲表現,4個階段都有特效表現。固然,也存在一些Buff是設計成徹底無表現的。
至此本文就結束了,主要仍是就美術資源,資源管理,協議交互,戰鬥表演作了些介紹,內容並無涵蓋整個戰鬥系統,不過已經是戰鬥系統核心設計內容,特此記錄,也但願能提供一些經驗借鑑。