游戏内的战斗框架涉及多个模块,包括技能,施法单元等。
大芒果对wow的实现
可施法单元Unit将会执行castspell,对某个目标使用某个法术进行施法。看起来所有的AI功能都是由CreatureAI来做的,每个精灵都会有一个CreatureAI指针,用来管理自己的行为逻辑,包括移动,施法等。例如施法的时候,会取出与自己关联的实体对象Unit* pCaster,然后调用Unit的CastSpell的方法对target使用spell法术进行战斗。而CastSpell里,将会根据法术信息创建一个Spell即法术,然后调用法术spell的prepare方法准备开始对目标进行作用。prepare方法里将创建一个spellevent,然后如果需要立即释放就调用cast方法,里面进行一系列检查后,调用handle_immediate方法,如果确认没什么问题 ,将调用DoAllEffectOnTarget方法,对目标进行所有效果的施放,调用DoSpellHitOnUnit对目标执行伤害,此时把攻击者加入被攻击者的威胁列表里,设置为被攻击者的攻击者setAttackBy,此时被攻击者启动战斗逻辑,攻击者把被攻击者放到自己的列表里。
在Unit里,维护了两个Manager,一个是ThreatManager和HostileRefManager用来管理威胁自己的对象列表和自己威胁的对象列表。
在设计面向接口的过程中,我们可以这样做,抽象出一个FightService,此服务将会负责管理两个单元之间的战斗逻辑,由于战斗都是基于技能,那么服务将设立一个FightUnit,一个Sprite和FightUnit有一一对应的关系?
如何结合mecanim,nodecanvas。mecanim是动画行为表现,不同的动画之间的切换通过事件触发,比如角色五连击,要依据玩家是否有战斗按钮输入。主要就区分下各个系统之间的关系。可以参考下别家的做法,看看他们的战斗系统,技能如何管理,与精灵之间的关系,行为树的应用,状态机的应用等。这些都值得参考下。
像暗黑3里,法师按下Q键触发回血,如果此时回血CD已经走完,直接回血,然后播放声音“好多了”,如果此时血量很低,会播放声音“我倒想那么做”,这就可以用行为树编辑出来,而不会用mecanim。
另外一个游戏的设计
每个精灵都有一个battlemanager,一个skillmanager,battlemanager会用到skillmanager,要弄明白整个的结构,可以梳理下流程。
战斗会有一个叫做EntityParent的类,看起来是战斗单元,里面有CastSkill方法。
Entity里会监听一个UI事件,OnNormalAttack,当UI触发这个事件时,将会调用battleManager的NormalAttack方法,normalattack里将会执行castskill,并且设置了个计时器,待一段事件后,再次执行normalAttack。
当调用castskill方法时,需要传入skillID,然后从skilldata里取出相应的技能。
如果是角色自己在请求施放技能,就会执行一个RPC,告诉服务器要执行施法技能;如果不是角色在请求施法,就会调用battlemanager的castskill方法。
精灵的状态机
和我之前做的差不多,流程是在changestate的时候,先做currentstate的exit,再做newstate的enter,然后newstate的process。Entity自己存了一个当前状态
currentMotionState。这些个状态的切换是什么用处呢?
主要有这么几个状态:
呼吸状态(IDLE),//进出这个状态没有多余操作,在process中会根据当前精灵类型设置animator属性,以及速度等变量值。
行走(walking),//进入这个状态,如果是角色自己,那么会给animator设置移动速度,注意是给动画状态机设置的移动速度。为什么只给玩家设置呢?其他是服务器设置?
// 离开这个状态,精灵(所有类型)的移动速度为1,如果是怪物会有别的速度设置,如果是角色自己,给animator设置移动速度为1
// 如果是服务器控制的怪物的话,会设置移动速度,所以进入这个状态主要就是设置速度了,但是在哪里设置精灵现在切换到动画状态的呢?有可能是给 animator状态机设置的speed变量值,导致状态机可以切换了。
死亡(Dead), // 进出此状态没有特殊操作
// process里,会判断当前的动作名称,根据动作名的结尾串,设置相应的动作。animator里的变量action,将被设置成对应的整数值。
然后触发声音组logicSoundEvent里的OnHitYelling事件,应该就是播放怪物的死亡叫声
拾取(picking),//这个状态的进出无操作,process里就是融合了picking这个动作。
攻击(attacking),//在切换到这个状态时,会传入技能ID,在process里,取出skilldata,skilldata里存有skillaction列表
// 这里将会执行延时回调,触发OnAttackingEnd事件,也许是这样的设计,当前这个action有duration,当这个duration到了,就说明action执行完了
// 然后触发OnAttackingEnd事件。有可能它们的设定就是以时间为准,而不是以美术设置的关键帧事件,这样服务器端也可以用这个时间来模拟战斗过程。
// 最后执行entity的onAttacking方法--->battlemanager onattacking(这里没做什么)--->skill manager onattacking 直到这一步才会去设置战斗动画
// 所以有些奇怪,因为其他状态都是自己去setaction,切换animator的动画,到这个战斗状态却是由技能管理器切换,可能是在这个状态里做太复杂了
// 所以最好还是都抛事件,让外面去切换动画状态
被击(Hit), // 当进入这个状态时,会传入攻击者和受击者的ID,通过一些列的动画判定,决定当前精灵受击应当播放的具体action,然后设置animator的action,
// 此时会触发一个OnHitYelling事件,播放被击音效。
准备(prepare,主要是给技能施放前准备的),//这个状态说是在攻击前做准备用,其实什么也没做
锁定(lock),//这个状态就是和locking动画做crossfade,为什么没有让mecanim去做这件事呢?
charging(突进),//这个状态只有一句设置动画的代码 setAction(4),说明就是个切换动画
翻滚(roll),//翻滚状态,在process里,将会直接设置当前精灵的animator事件,没做其他的事情。
切换状态的时机:
fsmstate还是附着在entity身上,battlemanager里做了一些状态切换,还有其他一些地方也做了切换。
BattleManager
是个基类,在创建的时候就要监听自己所归属那个精灵的状态事件,
主要有这么几个:
OnPrepareEnd //这个状态是给精灵做技能前准备的,例如旋转角色,朝向,靠近目标等
OnAttackingEnd //攻击结束状态,回到IDLE状态
OnHitAnimEnd //受击结束
ORollEnd,//滚动结束
OnHit,//受击,这里会判断是否播放特效等一些操作
基类里的castskill做的是切换玩家的动作,告知状态机说现在切换状态了,切换到attacking状态,但是他这个状态可不是mecanim的状态,mecanim的状态仅仅只是动画。
SkillManager
SkillManager技能管理器,每个精灵有这么一个,使用的是skilldata数据,这个数据是如何初始化的?
SkillAction和SkillData是两组数据,当播放一个技能时,需要指定一个actionID。
SkillManager会收听一个onAttacking事件,负责执行真正的action,触发attackingfx产生特效,然后告知entity播放特效,再告知fxmanager播放具体的特效。attackingMove产生移动,因为SkillAction有附加的速度,因此会告知Entity的motor执行移动操作。
attackbuff产生buff
随后会执行delayAttack,里面执行AttackEffect,查找出客攻击的Entity列表,对于主角来说,执行AttackDummy攻击怪物;对于怪物来说,执行AttackPlayer;其中将执行CalculateDamage计算伤害,然后抛出一个OnHit事件,如果怪物死亡(仅针对客户端怪物),就执行RPC调用,通知服务器。
每个精灵的BattleManager都会监听OnHit事件,当收到这个事件后,处理受击逻辑,包括播放被击特效,击退,击飞等效果(击退等移动效果由Entity内部的moto管理)。
看起来,它的状态机是因为当时4.3版本没有Mecanim提供的状态机脚本支持而自己实现的状态机,耦合还是很重的。
关于行为树的应用。行为树只负责精灵的逻辑控制,务必整理一个架构。
角色:当玩家开启自动战斗时,就让行为树启动逻辑。
伤害计算放在了CalculateDamage,这个类里只有静态方法,相当于一个静态对象,它没有数据成员,所有的数据都是从Entity里拿到的,例如计算攻击者此次的伤害值,就是 传入一个攻击动作ID,攻击者ID,被击者ID,然后函数将取出攻击者和被击者的数据,进行计算,然后返回结果给SkillManager。SkillManager里会执行对攻击者消耗的计算以及血量的计算。
因此skillmanger里做了攻击属性的逻辑,那么battlemanager的职责是什么呢?