从“核心战斗”讨论复杂系统的设计方法论


对于多数手游来讲,游戏可分为两大块,一是核心玩法,多数表现为某种战斗形式;二是周边系统,也就是超多的各种业务模块,表现为UI(用户界面)。
其中核心战斗(玩法)的开发是最复杂的,一般由主程来做,并且至少打磨到项目研发中期才能交给团队中的骨干成员来接手维护。
所以,能够研发核心战斗是对主程的基本要求,能够把核心战斗的架构设计的清晰易扩展则是优秀主程的一个标志。

零、综上,讨论核心战斗的价值有以下3点
1. 它是可复用的
任意战斗系统的本质就是“放触发器“,其他部分不过是设计一套架构来对它进行优雅的封装,所以复用性极高。
2. 掌握复杂系统的设计思想
面对一个复杂系统,对它的设计和实现在思想层面是有方法论的。本文通过战斗系统来介绍这种方法论。
3. 用架构保证代码的优雅性
烂代码一定是没有经过设计的架构产生的,它仅仅是实现功能,隐患无穷。经过设计的优雅的架构则会拔高代码质量的下限,用架构引导代码写在它应该在的正确的位置。
一、背景
最初是开发一个2D版本,在它已经开发完成并非常稳定时,项目方向调整需要把战斗系统3D化。
在3D化过程中发现架构层面一点都不用动,要做的不过是把各种接口按照3D技术进行删减和修改罢了。
所以为了方便,本文会把2D版和3D版糅合在一起展开讲述。

二、在设计战斗系统时我的思考过程
面对复杂系统,在设计阶段有3个步骤可作为方法论——拆分模块,明确核心难点,调研技术方案,下面展开讲。
1. 拆分模块
这一步可再细分为两步
a. 独立拆分
先独立的思考复杂系统的关键模块,我在这一步的产出是 打击判定、地图、寻路、相机跟随、角色、状态机、AI、技能、buff、血条、飘血、shader 12个模块
b. 搜索资料验证
这一步是为了验证拆分是否合理,是否有遗漏等。因为我从小玩游戏,对战斗系统了解,所以跳过此步骤。
2. 明确核心难点
这一步是思考以上模块的实现方案,有个方向就行。没有方向的就是核心难点,需要在后续步骤重点调研。
我的难点是打击判定。
怎么判断击中敌人?首先想到的就是用触发器,但是每一招放几个触发器?怎么放?放在什么位置?这几个方向不清晰,所以对我来讲它就是核心难点。
其他的要么已经做过,要么技术方案很明确。

3. 调研技术方案
a. 打击判定
在creator商店找到几个战斗demo,只翻找其打击判定的代码,很快就能找到。验证了我认为的战斗就是”放触发器“的猜想,也学到”放触发器“的套路。
b. 地图
好办,2D用tiledMap,3D就是一个3D模型而已
c. 寻路
好办,2D用A*,3D用navMesh(cocos商店有工具)
d. 相机跟随
好办,2D本质上是相对角色反向移动地图,3D战斗只需要固定视角跟随角色即可,cocos商店也有插件
e. 角色
好办,纯设计层面的东西,没有技术难点
f. 状态机
好办,纯设计层面的东西,我有一套自己写的在N个项目中经过验证的状态机具体实现,拿来用就行
g. AI
好办,游戏研发的AI实现不过两个经典方案而已,一是状态机,二是行为树。因为行为树更能满足复杂AI的需求,且在creator商店有行为树插件,所以选择行为树。
h. 技能
好办,纯设计层面的东西,没有技术难点
i. buff
好办,纯设计层面的东西,有自己写的一套可跨项目跨引擎使用的buff系统,拿来用即可
j. 血条
好办,2D就是和角色在一起的一个节点,3D就是在UI层显示2D的血条节点,每帧保持与3D角色位置一致即可,creator有把3D坐标转到2DUI坐标的接口,所以很好解决
k. 飘血
好办,与上同理
l. shader
好办,虽然我写shader不太熟,但我知道在这里用到的shader都很经典,很容易就能找到案例,移植修改下即可。

4. 架构先行
当要执行的时候,一定得先做架构设计。原因有三:
a. 架构设计图是校验技术调研质量的试金石,如果做不出设计那一定是调研没有做够,东西没想明白
b. 执行期的每一分每一秒都有具体的编码压力,很难再有平静的心态进行架构设计
c. 所谓的重构机会不会有的,烂代码只要稳定,在项目排期的压力下也不会给到机会去动它,动了就是bug
架构设计工具有两种,一是类图,表达静态设计;二是时序图表达动态设计。


三、计划管理,分模块的产出与可见迭代
到这里就要启动编码工作,最重要的一点是按照持续输出可见迭代的原则每周出个有明显可见内容的版本,我的计划如下
1. 第一周目标,”要有小人在地图上跑动“ 。
这就需要搞定 地图加载+一个没有任何属性的角色+寻路逻辑+相机跟随+一个随机算则位置的AI逻辑。这周就是在搭架子,关键模块本周都会有,但它们又都只是刚刚能用,对产品侧也是一个比较大的可见进展。
2. 第二周目标,”小人能打“ 。
这周工作量比较大,需要角色接入完整的属性+技能架构+触发器架构+用状态机连理它们+血条/飘血+要给能够寻敌并攻击的AI。
因为执行前已经设计好架构,并且状态机已有代码,技能、触发器等的细节已经想明白,所以也是能够搞定的。
3. 第三周,”第一波优化,战斗看起来更合理“
经过两周的开发战斗系统已经能有5V5的小人在地图上自动寻敌战斗,该有的都有。这个时候就需要想产品需求放要一波反馈和优化需求,本周先打磨一波,AI会更聪明,使得战斗系统与产品的需求一致。
4. 第四周已经以后,“buff,shader与持续优化”
这个时候战斗系统会更多的与具体业务关联,不只是技术上的事。
需要持续与产品沟通,持续优化打磨。

四、从类图讨论模块之间的静态联系
如果把每个类的属性和接口都画进类图,那么类图就会特别大,反而不好体现类之间的联系,所以这里只列举核心类与核心接口

1. 管理类
标注为蓝色的是管理类,可以看出有BBattleMgr,BRoleMgr,BMapMgr,BuffMgr4个。
其中BBattleMgr作用有两个,1>统筹管理其他战斗模块,表现在对BRoleMgr和BMapMgr的直接管理上 2> 是战斗系统对外的唯一入口,提供startBattle()启动战斗,也提供setBattleCamera()、setUICamera()等接口接受外部的传参。
BuffMgr则比较特殊,它管理的是每个角色的所有buff,也就是说每一个角色都会有一个BuffMgr的实例。
2. 角色类是如何放技能的?
标注为粉色的是角色和技能相关的模块。
可以看出BroleCtrl持有BSkillBase,然后BSkillBase持有TriggerBase;也就是角色放技能,再由技能把触发器放出去,而打击判定则由触发器来负责——碰到敌人则按照公式扣血这样子。
3. 为什么要使用状态机?
标注为绿色的则是状态机,可以看到BRoleCtrl持有两个状态机实例,BSkillBase持有1个状态机实例。
在这里状态机的核心作用是把逻辑拆分抽象为一个个独立的状态,用状态去封装逻辑,管理逻辑变换。
比如技能被拆分为eNone、eFrontStage、eRunningStage、eBackStage、eEnd、eGNone、eGCDing状态。
其中从eNone到eEnd状态是对 技能从被使用到释放完成 的抽象,在每个状态(阶段)只需要处理本状态自身的逻辑即可。
其中eGNone、eGCDing则是全局态,也就是一个状态机有两套状态在并行运作。这也很好理解——我在释放技能的同时,我的技能也被置为CD态。
4. 策略模式,解耦利器
标注为黄色的则是对策略模式的应用
策略模式定义一系列的算法,把它们一个个封装起来, 并且在运行时可以将它们相互替换
纯战斗系统在游戏业务中有多个包装,比如敌我双方的生成数量、生成规则不同,结算规则不同,结算后的处理伙计不同。
如果把这些逻辑写进BBattleMgr,那么一定是有大量的if else,逻辑严重耦合难以维护。
这里用策略模式把对纯战斗系统的每一种包装抽象为一个类,在运行时根据具体业务动态切换。实现了每种包装之间的解耦,也实现了BBattleMgr与每种包装的解耦。
也就是遵循了一条重要的设计原则——对扩展开放,对修改关闭

五、从时序图讨论模块间的动态关联
1. 从点击按钮到进入战斗发生了什么?

核心逻辑是
1. 设置战斗运行在前台的标志位
2. 打开loading页
3. 清理上一次战斗数据
4. 根据战斗类型创建新的战斗流
5. 加载地图角色技能等战斗资源
6. 创建地图
7. 通过流创建角色
8. 地图创建完毕后关闭loading页
9. 角色启动AI逻辑
2. 一个技能的前世今生

这里有几个显著个特点
1. 使用到3个状态机,
2. 很明显用状态机管理技能生命周期,管理角色施法周期。并且用BRoleFSM和BAniFSM把角色逻辑态和动画态拆分开
3. 触发器的前世今生

可以看出,在SkillAct会在update中实例化一个或多个Trigger对象。它只管按照节奏释放,至于打击判定则由触发器自己处理,如下图

可以看出触发器的逻辑非常简单
1. 注册一个引擎的碰撞时间
2. 很发生碰撞时执行攻击逻辑
3. 存活时间到了就销毁自己

4. 行为树AI的应用
行为树如图

每个叶子节点抽象一个独立的类实现,每个类中最核心的任务是返回节点运行状态即可。
状态包括

类图

行为树插件单开一篇文章来讲
6. 用策略模式实现的Flow类
可以看出,策略实例(Flow类实例)提供了多个接口供BBattleMgr调用。不同的Flow提供同样的接口,但是接口内算法不同,就可以在运行时动态地改变执行逻辑,这就是策略模式。
至此从调研、设计到具体实现的全过程都有了,总之调研>设计>具体实现,优先级和执行顺序不能错。

可爱的伙伴,都看到这了~ 右下角点个“赞”和“在看”呗
到顶部