這篇文檔基于我在《Thor》項(xiàng)目中對(duì)戰(zhàn)斗架構(gòu)的設(shè)計(jì)實(shí)現(xiàn),對(duì)此類卡牌游戲的戰(zhàn)斗進(jìn)行通用型技術(shù)的整理,并梳理記錄當(dāng)前的一些實(shí)現(xiàn)細(xì)節(jié)。當(dāng)再度開發(fā)此類游戲時(shí),可以基于這些內(nèi)容快速搭建起核心戰(zhàn)斗框架,并能迅速實(shí)現(xiàn)功能鋪量。
這篇文檔計(jì)劃基于以下內(nèi)容進(jìn)行分類整理:
- 戰(zhàn)斗框架思路
- ECS模式的設(shè)計(jì)與使用,以及具體實(shí)現(xiàn)的一些優(yōu)化內(nèi)容與思路細(xì)節(jié)
- 技能、buff、子物體架構(gòu)
- 戰(zhàn)斗業(yè)務(wù)邏輯的通用數(shù)據(jù)與組織
- 幀同步技術(shù)的使用與實(shí)現(xiàn)
- 開發(fā)工具整理及思路
因篇幅在整理過程中發(fā)現(xiàn)較大,因而分章節(jié)說明記錄。本章先介紹戰(zhàn)斗框架。
戰(zhàn)斗框架
戰(zhàn)斗框架遵循邏輯與顯示分離的原則進(jìn)行設(shè)計(jì)。在基本原則基礎(chǔ)上,劃分出邏輯層與顯示層,二者中間加入通訊層實(shí)現(xiàn)數(shù)據(jù)傳輸。

邏輯與顯示分離的設(shè)計(jì)原則,來自邏輯可移植的需求。我們要確保邏輯代碼與顯示代碼的徹底剝離,這樣可以收獲幾個(gè)好處:
- 邏輯可單獨(dú)打包,并可以保證在輸入一致的情況下運(yùn)算結(jié)果唯一,這樣可以直接用于邏輯驗(yàn)證、或用于服務(wù)器開房間等;
- 代碼移植過程中,不必?fù)?dān)心因?yàn)橐肓孙@示邏輯而導(dǎo)致的代碼打包時(shí)找不到相關(guān)顯示類的報(bào)錯(cuò);
- 排除掉顯示相關(guān)代碼后,確保了邏輯層代碼運(yùn)行的獨(dú)立性,排除其運(yùn)行結(jié)果被其他代碼干擾的可能;
- 代碼結(jié)構(gòu)清晰。
如果沒有驗(yàn)證需求,那么邏輯與顯示也可以合并實(shí)現(xiàn)(可以參考下文的ECS模式部分),但是在這種情況下,還是需要單獨(dú)剝離一套顯示層,用于戰(zhàn)中的模型等資源管理、特殊顯示需求實(shí)現(xiàn)等功能;而通訊層則可以省去,轉(zhuǎn)而將顯示層以單例實(shí)現(xiàn)供邏輯層調(diào)用。

所以按照邏輯與現(xiàn)實(shí)分離模式,對(duì)各模塊進(jìn)行介紹。
邏輯層
幀率的設(shè)計(jì)
邏輯層即戰(zhàn)斗的核心邏輯,基于上文中的需求,邏輯層需要設(shè)計(jì)為定幀執(zhí)行模式。
定幀模式下,具體幀率有多種參考:
- 跟隨服務(wù)器幀率。如果我們要把核心邏輯用于開發(fā)幀同步游戲,因?yàn)榉?wù)器本身也有幀率以用于向各客戶端同步信息,所以可以直接將邏輯幀率設(shè)置為服務(wù)器幀率,這樣還可以省去邏輯幀與服務(wù)幀速率同步的問題。
- 游戲引擎刷新幀率。如果我們將游戲的刷新率設(shè)置為60FPS,那么也可以將邏輯設(shè)置為60幀,這樣從顯示效果上,可以更為真實(shí)地去模擬計(jì)算戰(zhàn)斗邏輯。
- 其他。除了前兩個(gè)方案,其實(shí)其他值同樣可以,即便邏輯幀我們?cè)O(shè)置為1秒執(zhí)行1次也是可行的,但是帶來的問題則會(huì)在運(yùn)行時(shí)表現(xiàn)得淋漓盡致。
雖然定幀方案就這么幾種,但是實(shí)際開發(fā)時(shí),幀率設(shè)置的具體大小也有很多需要注意的地方:
- 需要盡量去令一些計(jì)算結(jié)果更貼近實(shí)際。從積分的角度上講,需要我們把時(shí)間切割的足夠小,即幀率足夠高,我們的邏輯運(yùn)算才會(huì)更為貼合實(shí)際。
- 需要考慮幀率對(duì)性能的影響。幀率無限大,帶來的問題就是CPU資源被邏輯大量占用,所以從性能的角度分析,幀率又需要足夠小。
- 需要考慮策劃對(duì)時(shí)間的配置。游戲離不開策劃,而他們?cè)O(shè)置的一些CD、持續(xù)時(shí)間等數(shù)據(jù)會(huì)反向促使我們選擇邏輯幀率,即我們的幀率能盡可能的體現(xiàn)出策劃所配時(shí)間數(shù)據(jù)的差異性。舉例來講,這一點(diǎn)就是需要兩個(gè)CD不同的技能同時(shí)計(jì)時(shí),二者在邏輯上是分先后走完CD的,而不是在同一邏輯真內(nèi)完成CD計(jì)算。如果不能保證這一點(diǎn),那么策劃配置的很多數(shù)據(jù)將沒有意義,但是也需要把握度。
所以綜合以上幾點(diǎn),邏輯幀率的設(shè)置是一個(gè)需要在游戲開發(fā)前期就要設(shè)置好的值,具體也需要參考項(xiàng)目規(guī)劃。這里給出一個(gè)參考值是25幀,即每40毫秒計(jì)算一次。在這樣的幀率下,首先能確保我們的一些擬合運(yùn)算能夠盡量貼近實(shí)際,使玩家在觀感上不會(huì)有撕裂感;同時(shí),40毫秒對(duì)于策劃配置的時(shí)間相關(guān)數(shù)據(jù)而言,也已足夠覆蓋9成以上的時(shí)間差需求(因項(xiàng)目而異,如果項(xiàng)目對(duì)時(shí)間差的需求在秒級(jí),那么也就無需考慮這么多了)。
邏輯驅(qū)動(dòng)器(LogicMgr)的設(shè)計(jì)
如果我們把邏輯已經(jīng)封裝成了一個(gè)黑盒,那么邏輯驅(qū)動(dòng)器就是運(yùn)行這個(gè)黑盒的外衣。之所以設(shè)計(jì)邏輯驅(qū)動(dòng)器,本質(zhì)還是為了滿足不同的邏輯運(yùn)行場(chǎng)景需求。其接口結(jié)構(gòu)如下:

當(dāng)有了LogicMgr接口的定義之后,針對(duì)不同的使用場(chǎng)景,只需要實(shí)現(xiàn)對(duì)應(yīng)接口內(nèi)容即可。
關(guān)于使用場(chǎng)景,以我開發(fā)項(xiàng)目舉例:
- 核心戰(zhàn)斗。這部分作為游戲的核心玩法,便是最基本的使用場(chǎng)景,擁有完整的輸入輸出功能。
- 游戲主場(chǎng)景。我們的游戲主場(chǎng)景內(nèi),也需要主角進(jìn)行戰(zhàn)斗演示,雖然這一部分可以是假數(shù)據(jù),但是表現(xiàn)與核心邏輯需要與戰(zhàn)中一致,因而作為一個(gè)特殊的使用場(chǎng)景,這里也需要單獨(dú)進(jìn)行LogicMgr的實(shí)現(xiàn)。
- 多人戰(zhàn)斗。此處包含了多人的GVE游戲模式,也有多人的PVP模式,這兩個(gè)游戲模式的本質(zhì)都是以幀同步為數(shù)據(jù)同步基礎(chǔ)的多人聯(lián)機(jī)游戲。此時(shí)對(duì)應(yīng)心跳以及玩家輸入數(shù)據(jù)的處理都會(huì)發(fā)生較大的不同,因而會(huì)有單獨(dú)的LogicMgr進(jìn)行實(shí)現(xiàn)。
- 戰(zhàn)斗驗(yàn)證服務(wù)器。這也是一種使用場(chǎng)景,因?yàn)檫@種驗(yàn)證設(shè)計(jì),其本質(zhì)就是對(duì)戰(zhàn)場(chǎng)進(jìn)行完整的重演。所以在此情境下,心跳內(nèi)容會(huì)發(fā)生變動(dòng),以使全場(chǎng)游戲能夠瞬間計(jì)算完成,同時(shí)玩家操作也替換成了玩家上傳的操作隊(duì)列,需要LogicMgr去分批次加載并生效。
這就是邏輯驅(qū)動(dòng)器的核心設(shè)計(jì)思路。 在項(xiàng)目的具體使用中,ILogicMgr內(nèi)部調(diào)用戰(zhàn)場(chǎng)邏輯,這一部分內(nèi)容主體使用ECS模式實(shí)現(xiàn),但是一些周邊模塊還需我們單獨(dú)抽象結(jié)構(gòu)出來作為輔助,即下面會(huì)說到的通用環(huán)境組件。
定幀的實(shí)現(xiàn)
通用環(huán)境組件(IEnv)
如果LogicMgr是一個(gè)黑盒,ECS是黑盒內(nèi)的核心邏輯部分,那么通用環(huán)境組件則是黑盒內(nèi)部與基礎(chǔ)數(shù)據(jù)交互的組件。
LogicMgr因不同的用途而具有不同的實(shí)現(xiàn),那么作為基礎(chǔ)的數(shù)據(jù)支持服務(wù),環(huán)境組件也需要適當(dāng)?shù)倪M(jìn)行擴(kuò)展。這部分內(nèi)容可根據(jù)項(xiàng)目實(shí)際開發(fā)進(jìn)行擴(kuò)展,此處僅用《Thor》所用內(nèi)容舉例。
在我的項(xiàng)目中,通用環(huán)境組件核心負(fù)責(zé)下面這幾件事:
- 配置數(shù)據(jù)讀取接口。因不同環(huán)境下,數(shù)據(jù)來源不同,所以可以通過在環(huán)境組件內(nèi)設(shè)計(jì)數(shù)據(jù)接口,以匹配底層不同的數(shù)據(jù)記載機(jī)制;此外,一些配置常量型數(shù)據(jù)也可在此處實(shí)現(xiàn)。
- 特殊數(shù)據(jù)緩存實(shí)現(xiàn)。戰(zhàn)場(chǎng)初始數(shù)據(jù)的緩存實(shí)現(xiàn)也在此,主要用于戰(zhàn)斗驗(yàn)證這種計(jì)算密集型的GC優(yōu)化。
通訊層
通訊數(shù)據(jù)的結(jié)構(gòu)設(shè)計(jì)
所有內(nèi)部通訊數(shù)據(jù),均派生自MsgBase,其結(jié)構(gòu)如下,

現(xiàn)對(duì)內(nèi)部數(shù)據(jù)與方法做必要說明:
- opcode:
Opdefine為自定義的消息類型枚舉,由每個(gè)具體協(xié)議聲明具體內(nèi)容。 -
Write與Read方法:這兩個(gè)方法主要用于協(xié)議的序列化與反序列化。這一部分用處較多,即可用于戰(zhàn)場(chǎng)信息的記錄,也可作為戰(zhàn)斗驗(yàn)證的依據(jù),具體可根據(jù)項(xiàng)目需求考慮是否添加。
這就是通訊數(shù)據(jù)的設(shè)計(jì),具體某一個(gè)協(xié)議內(nèi)的數(shù)據(jù)根據(jù)需求在對(duì)應(yīng)類中進(jìn)行添加,但是所有的協(xié)議需要以MsgBase作為基類,以便于自動(dòng)化代碼生成操作。自動(dòng)化生成相關(guān)內(nèi)容可參考后文了解詳情,此處不作贅述。
通訊數(shù)據(jù)內(nèi)部數(shù)據(jù)類型要求
為了便于后續(xù)的自動(dòng)化處理,也是為了保證通訊數(shù)據(jù)本身的獨(dú)立性,所以需要對(duì)數(shù)據(jù)類型進(jìn)行一些要求:
- 允許值類型的使用,無論C#基礎(chǔ)值類型或是自定義值類型;
- 引用類型需要在聲明時(shí)就執(zhí)行new操作(即默認(rèn)不可為null);
- 自定義的值類型或引用類型需要在制定的命名空間下(便于自動(dòng)化操作);
- 引用類型除自定義類型外,僅支持List(考慮到哈希順序的問題,所以不支持Dictionary等結(jié)構(gòu))。
通訊數(shù)據(jù)交互流程

邏輯通訊流程大致如上圖。
邏輯層向顯示層發(fā)送消息,需要邏輯層先獲取一個(gè)空內(nèi)容的消息實(shí)例,賦值,并將該消息實(shí)例發(fā)送給顯示層。
顯示層收到消息實(shí)例后,不對(duì)該消息實(shí)例本身做任何操作,而是完整復(fù)制一個(gè)消息出來,并將副本加入自身的待處理隊(duì)列。
上述操作執(zhí)行完后,因是同步操作,所以邏輯層會(huì)在流程最后將剛剛發(fā)送的消息進(jìn)行回收操作。
而加入顯示層待處理隊(duì)列的內(nèi)容,會(huì)在顯示層下次心跳時(shí)執(zhí)行對(duì)應(yīng)處理的Action(顯示層Action在下文有詳細(xì)說明),并在執(zhí)行完后清空待處理隊(duì)列,等待下一波協(xié)議的到來。
GC優(yōu)化所做的工作
在前面的流程圖中,存在顯示層與邏輯層各自的消息緩存,這個(gè)緩存的作用就是用于GC優(yōu)化的。
當(dāng)需要用到某個(gè)消息時(shí),便可以從消息緩存中拿到一個(gè)干凈的消息實(shí)例,這樣可以避免某些使用頻率較高的協(xié)議頻繁的new操作導(dǎo)致GC飆升。
當(dāng)邏輯層賦值一個(gè)消息實(shí)例并向顯示層發(fā)送后,需要及時(shí)回收;而顯示層收到的消息實(shí)例會(huì)在隨后被顯示層回收,因而無法直接使用,所以需要立即對(duì)內(nèi)容進(jìn)行拷貝以得到一個(gè)副本,這樣我們對(duì)副本的任何操作都不會(huì)影響原始數(shù)據(jù),原始數(shù)據(jù)的修改也不會(huì)影響副本內(nèi)的數(shù)據(jù)變動(dòng)?;诖耍覀兊南⒕彺婢托枰刑崛?、拷貝、回收操作。
這部分代碼我們通過自動(dòng)化工具進(jìn)行代碼生成,詳情可參考下文。而內(nèi)部的具體實(shí)現(xiàn),僅僅是通過棧進(jìn)行實(shí)例緩存,并提供對(duì)應(yīng)接口,因無甚技術(shù)含量不在此處贅述。
顯示層
顯示層因完全貼合客戶端進(jìn)行開發(fā),所以開發(fā)手段會(huì)更為靈活。較為簡(jiǎn)便的方式,就是通過單例模式創(chuàng)建顯示層單例,這樣代碼開發(fā)起來更為方便。但是即便如此,顯示層依舊有幾個(gè)模塊是不可或缺的,下文就進(jìn)行記錄與介紹。
Action
Action模塊本身由顯示層與邏輯層的通訊功能引申而來。
顯示層會(huì)在收到邏輯層的數(shù)據(jù)之后拷貝出一份副本并入隊(duì),所以在顯示層的心跳中,第一個(gè)要做的就是從消息隊(duì)列中獲取消息并處理,對(duì)不同的消息就是由不同的Action進(jìn)行處理的,所以這一部分整合為Action模塊。

顯示層消息處理流程如上,這也就是說,我們定義了n個(gè)消息類型,就需要定義n個(gè)對(duì)應(yīng)的Action。
如果像我的項(xiàng)目一般存在多種邏輯運(yùn)行的場(chǎng)景,那么這些Action也需要有對(duì)應(yīng)的多種方案,因?yàn)槊糠N邏輯運(yùn)行場(chǎng)景下,消息的處理邏輯都會(huì)有所不同,需要分開單獨(dú)實(shí)現(xiàn)。
顯示實(shí)體數(shù)據(jù)管理
邏輯層可以將各種對(duì)象抽象為數(shù)據(jù)的組合,這點(diǎn)在后面的ECS模式中會(huì)講到,但是在顯示層,英雄、buff這些實(shí)體都是有實(shí)際存在與其一一對(duì)應(yīng)的,所以這就需要顯示層有單獨(dú)的顯示實(shí)體數(shù)據(jù),并對(duì)其進(jìn)行生命周期與邏輯的管理。
API
API可以理解為是方法的集合,遵循后文會(huì)提到的邏輯與數(shù)據(jù)分離的思想。既然我們已經(jīng)將顯示層的實(shí)體抽象出對(duì)象并進(jìn)行生命周期管理,那么讓他們執(zhí)行具體的邏輯,無論是賦值或是播放動(dòng)畫,都可以將這些方法整理到API當(dāng)中。
API的設(shè)計(jì)思路有很多,可以每個(gè)實(shí)體類型對(duì)應(yīng)一個(gè)API集合,也可以每個(gè)顯示功能對(duì)應(yīng)一個(gè)API集合,一切以代碼方便管理為前提。
需要額外說明的是,顯示層很多方法是需要放到API中的,但并不是所有的都需要放在這里。根據(jù)項(xiàng)目需求,很多功能會(huì)需要我們單獨(dú)開發(fā)一些組件或模塊以協(xié)助實(shí)現(xiàn)某些特定的需求,這種情況下,組件與模塊的方法依舊歸于其內(nèi)部,而API更多負(fù)責(zé)的是去調(diào)用這些方法以實(shí)現(xiàn)具體的功能。
輔助模塊
輔助模塊是顯示層依據(jù)需求開發(fā)的獨(dú)立功能,派生自基礎(chǔ)模塊接口,以實(shí)現(xiàn)一些統(tǒng)一時(shí)機(jī)調(diào)用的方法,且為了開發(fā)方便,每個(gè)模塊應(yīng)當(dāng)是以單例形式存在的。
舉例來說,我們的項(xiàng)目需要實(shí)現(xiàn)在特定情況下除核心英雄,其他英雄與場(chǎng)景都置灰的操作,且該狀態(tài)下還要控制角色模型、特效按照規(guī)定的效果去展示,這就需要一個(gè)獨(dú)立的模塊去執(zhí)行對(duì)應(yīng)邏輯
顯示邏輯控制
類似邏輯層有一個(gè)LogicMgr,顯示層也需要一個(gè)DisplayMgr去驅(qū)動(dòng)所有的實(shí)體運(yùn)行,以及邏輯層消息的處理。
在顯示邏輯控制的代碼內(nèi),核心方法就是一個(gè)心跳方法Tick。在該方法中,優(yōu)先執(zhí)行邏輯消息處理,即從消息隊(duì)列內(nèi)取出消息并交由對(duì)應(yīng)的Action處理具體邏輯。然后就是執(zhí)行所有實(shí)體的心跳方法。

戰(zhàn)斗管理器
無論邏輯層或顯示層,想要運(yùn)行起來終究需要外部驅(qū)動(dòng)。因此,就需要引入戰(zhàn)斗管理器(BattleMgr)來進(jìn)行。
需要說明的是,在我的項(xiàng)目中,因?yàn)楦黝悓?shí)際場(chǎng)景需要多個(gè)模塊存在組合關(guān)系,所以戰(zhàn)斗管理器的內(nèi)容已經(jīng)抽象到IBattleUnit接口中。所以此處為了講解方便,我會(huì)把IBattleUnit的內(nèi)容放回至BattleMgr中進(jìn)行講解,這并不影響理解。
戰(zhàn)斗管理器結(jié)構(gòu)簡(jiǎn)介

戰(zhàn)斗管理器內(nèi)持有顯示層與邏輯層的實(shí)例,并在Update函數(shù)中對(duì)兩個(gè)實(shí)例進(jìn)行心跳的調(diào)用。
需要說明的是,這里調(diào)用順序需要先調(diào)用邏輯層的心跳,再調(diào)用顯示層的心跳。這么做的目的,是為了保證邏輯運(yùn)算結(jié)果能夠及時(shí)交給顯示層進(jìn)行渲染。

此處的delta時(shí)間專門開了一個(gè)方法計(jì)算,就是為了應(yīng)對(duì)項(xiàng)目?jī)?nèi)一些變更時(shí)間的需求。如最常用的戰(zhàn)斗倍數(shù)功能,即可在該方法內(nèi)參與計(jì)算,以獲取實(shí)際需要的delta時(shí)間。
暫停組件采用計(jì)數(shù)暫停方案,用于處理戰(zhàn)斗暫停邏輯。當(dāng)戰(zhàn)斗處于暫停狀態(tài)時(shí),則戰(zhàn)斗管理器的心跳停止運(yùn)行。但這并不是唯一方案,有些游戲也可以修改delta時(shí)間為0來實(shí)現(xiàn)暫停效果,并使得顯示層繼續(xù)運(yùn)行,以滿足自己項(xiàng)目的需求。
戰(zhàn)斗管理器的使用
戰(zhàn)斗管理器本身是以單例的形式存在的。因?yàn)檫@部分內(nèi)容僅用于我們的客戶端,基本不必考慮多線程、多實(shí)例的情況。
在此基礎(chǔ)下,當(dāng)通過入戰(zhàn)數(shù)據(jù)將戰(zhàn)斗全部初始化完成后,只需在對(duì)應(yīng)的外部戰(zhàn)斗模塊內(nèi)調(diào)用戰(zhàn)斗管理器單例的心跳方法,即可運(yùn)行戰(zhàn)斗。
此處再無特殊技術(shù)要點(diǎn),一切以項(xiàng)目實(shí)際需求為準(zhǔn)即可。
以上就是一個(gè)完整的卡牌戰(zhàn)斗框架的核心內(nèi)容。在具體開發(fā)中,我們當(dāng)然還會(huì)面臨很多其他需求,需要我們?nèi)?shí)現(xiàn)或引入一些獨(dú)立的插件、公共方法等,這些就依據(jù)各自游戲的需求單獨(dú)添加即可。而框架核心,上述內(nèi)容便已足夠。
下一篇文章,將著重記錄邏輯的ECS模式與實(shí)現(xiàn)。