1. 程式人生 > >表演的藝術,妖尾回合制戰鬥系統客戶端設計

表演的藝術,妖尾回合制戰鬥系統客戶端設計

妖尾歷經幾年開發,終於在今年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 回合按需載入 戰後解除安裝

上圖簡述了戰鬥系統涉及的主要資源及載入快取策略,一言蔽之,就是既要體面,又要節約。

我們希望遊戲體驗儘量流暢,在社群場景遭遇戰鬥時能秒切進入戰鬥,所以:

  1. 最基本的戰鬥場景和全屏圖資源,高低配機都會預載入好並常駐記憶體;
  2. 本著節約原則,其他資源儘量做到不影響體驗,用時載入,不用時戰後解除安裝。
  3. 骨骼動畫,技能,Buff等資源每回合根據表演內容再按需載入;
  4. 模型方面可以預判像玩家自己,隊友等己方模型經常複用,高配機會快取至下一場戰鬥,無命中再戰後解除安裝,戰鬥HUD也會常駐記憶體;低配機資源比較緊張,還是用戰後即時解除安裝的策略。
  • 資源使用策略

另外,一場戰鬥表現少說也會涉及數十個資源的非同步載入,如果每處表演邏輯都要非同步等待資源載入回撥,很容易導致回撥地獄。因此戰鬥狀態機特意將資源載入,資源使用劃分成兩個階段。每回合等待表演所需資源全部非同步載入完畢,才能進入到表演階段,表演邏輯按同步方式使用資源即可。由於資源載入粒度細分到以回合為單位來載入,實測資源載入等待並不會影響戰鬥表演的流暢體驗。

  • 資源ab打包策略

講到資源管理,ab打包是個繞不開的話題。ab打包粒度越細,包數量越多,IO壓力大;ab打包粒度越粗,資源越冗餘,包體,熱更新資源量都會變大,說到底是平衡的藝術。

資源 打包策略
戰鬥場景 單獨打包
全屏背景圖 每張圖單獨打包
戰鬥HUD HUD集合打包,HUD上的動態小圖示按類別集合打包
功能模組UI 按模組集合打包
通用特效 所有通用特效集合打包
主角模型 每個主角各個模型部件單獨打包,各個骨骼動作單獨打包
非主角模型 每個角色為單位打包
技能 每個技能單獨打包,技能引用資源分普通技能,合體技兩類,再按角色為單位打包
Buff 所有Buff集合打包

簡單羅列了戰鬥相關資源的ab打包策略,原則上是儘量按資源使用耦合程度劃分打包,可能一起使用的資源,打包到一起,如果資源過多,就要進一步拆分ab包。再者,做好提前設計,確保打包策略在未來資源量堆起來後仍能適用。比如,主角模型的模型部件,骨骼動作非常多且在未來很有可能新增,可以每個資源單獨打包;非主角模型模型,骨骼動作數量相對固定,就能以角色單位打包。規劃好ab打包策略後,跟美術約定好規則來提交資源目錄及資源,就能編寫工具根據配表,不同目錄執行不同的ab打包策略。

戰鬥表演

戰鬥表演大體分為技能和Buff兩類表演。技能是有開始結束的一段表現,小到普通攻擊,大到多人合體技,都是技能表演;Buff則是附在單位上的持續性狀態表現,如人物的中毒,封印狀態表現。

  • 技能表演

正如前面的框架圖裡提到妖尾戰鬥有很多表演指令碼,可綜合對角色,UI,場景,鏡頭,節奏做全方位的排程控制,從而表現一段技能。伽吉魯和蕾比兩個角色的合體技是非常有代表性的一段技能表演,涵蓋了對很多技能指令碼的應用,簡單舉例講解這個合體技的實現,就可以瞭解技能是怎麼編輯,表演的。下圖是合體技的遊戲表現:

總體來看,這個合體技由鏡頭動畫+技能打擊兩部分構成,這兩部分都是在同一條時間軸通過指令碼組合運用編輯出來的,最後生成一個合體技預製件。

圖中紅色部分是鏡頭動畫實現指令碼:

  1. 動畫掛靠指令碼帶有Animator元件,控制相機和角色的位置旋轉。
  2. 動畫掛靠指令碼帶有相機掛點,啟用場景鏡頭動畫相機並放到掛點下
  3. 動畫掛靠指令碼帶有角色掛點,生成對應角色高模例項,放到掛點下,並播放合體技動作
  4. 動畫掛靠指令碼每幀根據角色表情掛點更新角色表情
  5. 特寫角色特效指令碼例項生成包括角色底下的IRON文字特效,光影粒子特效,速度線UI特效等,放置到相對鏡頭相機指定位置播放
  6. 線光源設定指令碼修改戰鬥環境光
  7. 替換材質指令碼替換伽吉魯模型材質
  8. 相機徑向模糊指令碼啟用相機後處理特效

不難看出鏡頭動畫的主要邏輯是由動畫掛靠指令碼實現的,主要鏡頭,角色走位排程由美術實現Animator進行控制。視鏡頭動畫的複雜效果,可能會堆多一些特寫特效指令碼同步播放,豐富畫面效果。

戰鬥系統運用了幾個透視相機,按相機深度由低到高分別是:

  • 戰鬥背景圖相機
  • 戰鬥單位名字相機
  • 戰鬥UI相機
  • 戰鬥主相機
  • 戰鬥鏡頭動畫相機
  • 戰鬥鏡頭動畫UI相機

鏡頭動畫播放完,緊接著就是綠色部分指令碼,配合完成技能釋放:

  1. 移動指令碼控制伽吉魯跑到戰場中央
  2. 播放特效,動作指令碼控制伽吉魯做出打擊表現
  3. 受擊指令碼控制目標做出受擊動作,扣血反饋
  4. 震屏指令碼,角色抖動指令碼配合著加強打擊感
  5. 回位指令碼控制伽吉魯回到自己的站點

技能釋放需要由更多的指令碼組合完成,一般不需要美術產出很多資源,利用一些簡單攻擊特效,配置角色走位,動作,受擊,鏡頭控制就能做出漂亮打擊感的技能。

  • Buff表演

Buff表演相比技能表演更簡單,容易編輯,實現。每種Buff都可以分為Buff新增,Buff持續,Buff觸發,Buff移除4個階段,視需求自由決定每個階段是否有具體表現,Buff編輯器只需配置每個階段的特效,人物動作,替換材質即可。下圖是反擊Buff的遊戲表現,4個階段都有特效表現。當然,也存在一些Buff是設計成完全無表現的。

結語

至此本文就結束了,主要還是就美術資源,資源管理,協議互動,戰鬥表演做了些介紹,內容並沒有涵蓋整個戰鬥系統,不過已是戰鬥系統核心設計內容,特此記錄,也希望能提供一些經驗借鑑