战斗框架设计
游戏内的战斗框架涉及多个模块,包括技能,施法单元等。
大芒果对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,//受击,这里会判断是否播放特效等一些操作
从职责上看,battlemanager收到这些事件后会尝试改变精灵的当前状态机 。是战斗的入口,操作skillmanager
基类里的castskill做的是切换玩家的动作,告知状态机说现在切换状态了,切换到attacking状态,但是他这个状态可不是mecanim的状态,mecanim的状态仅仅只是动画。
技能
SkillManager技能管理器,每个精灵有这么一个,使用的是skilldata数据,这个数据是如何初始化的?
关于SkillData有通用属性,学习限制属性,施放条件三大类属性
通用属性:
技能名称,技能等级,技能描述,技能ICON,技能职业,技能组,武器,依赖的技能列表。
学习属性:
等级限制,职业限制,消耗的金币,可增加的伤害,增加的掉率等
施放条件:
一组CD(注意公共CD的意义是:当你一个技能完成,无论如何都要等一个CD才能开始下一个技能)
施放时间,
施放范围,
有依赖的buff列表,独立的buff列表,当前的buff,对应的skillaction列表,全部action的持续时间。
技能树和技能池的区别?
关于SkillAction
包括表现效果和技能效果两部分,这个和动作表现关系非常密切,打出来的效果,哪些美术配置出来的参数需要程序运行时了解,都配置在这个对象里。
表现效果包括:
相机震屏ID,声音,持续时间,攻击动作到的是哪一帧,技能的特效列表,是否要关闭碰撞,是否冻结周围,是否分身
技能效果:
目标类型,攻击对象的上限,当前的buff,
各种带表现效果的buff(释放buff,目标buff等),动作开始时长,结束时长
每个skillData里有一组SkillAction,其实是职责上进行了拆分。skillaction更多的是跟动画表现相关。
技能cd在哪里做的?PlayerSkillManager里有一个is skill cool down函数,此函数将判定当前的技能cd是否到了。
关于SkillBuffData
名称,优先级,持续时间,可用的技能列表,vip等级,特效列表,互斥buff列表,替换buff列表等。
时间变量
skill的定义里有castTime,这算是什么呢,施法时间?这个时间不跟着具体的skillaction走,从现在来看,好像是没什么用处。
skillAction里的时间变量,
duration 这个变量没看出有什么用处。
actionTime,这个时间貌似就是攻击时长,时长结束后,就执行伤害
nextHitTime,好像是可以有两次伤害,一个skillaction里可以连续有两次伤害。貌似只在前端使用。
damageTriggerFrame,貌似只在xml配置了,但是并没有起到作用。
actionDuration,并没有用到,xml里也配置了
actionBeginDuration,这个应该算是前摇
actionEndDuration,这个算是后摇
Data的配置文件,每种data有个成员是 filename,指出了文件所在的目录。例如MapData的filename 是xml/map_setting
SkillManager会收听一个onAttacking事件,负责执行真正的action,触发attackingfx产生特效,然后告知entity播放特效,再告知fxmanager播放具体的特效。attackingMove产生移动,因为SkillAction有附加的速度,因此会告知Entity的motor执行移动操作。
里面执行AttackEffect,查找出客攻击的Entity列表,对于主角来说,执行AttackDummy攻击怪物;对于怪物来说,执行AttackPlayer;其中将执行CalculateDamage计算伤害,然后抛出一个OnHit事件,如果怪物死亡(仅针对客户端怪物),就执行RPC调用,通知服务器。
每个精灵的BattleManager都会监听OnHit事件,当收到这个事件后,处理受击逻辑,包括播放被击特效,击退,击飞等效果(击退等移动效果由Entity内部的moto管理)。
看起来,它的状态机是因为当时4.3版本没有Mecanim提供的状态机脚本支持而自己实现的状态机,耦合还是很重的。
做的时候,要先设计下技能数据,玩家点击战斗按钮进入战斗状态,同时传入当前的战斗技能,从技能表里取出对应的技能项。
但是战斗的判定该怎么做呢?例如,角色发起了攻击,必然要根据技能找到目标。
现在的问题是,角色发起攻击,动作到了目标点,技能系统需要同时拿到攻击者的属性和目标的属性,计算出伤害值。需要有个SkillCalculate脚本,来进行计算一个攻击者,一个目标的各种伤害值。
===========================================================================
关于行为树的应用。行为树只负责精灵的逻辑控制,务必整理一个架构。
角色:当玩家开启自动战斗时,就让行为树启动逻辑。
伤害计算放在了CalculateDamage,这个类里只有静态方法,相当于一个静态对象,它没有数据成员,所有的数据都是从Entity里拿到的,例如计算攻击者此次的伤害值,就是 传入一个攻击动作ID,攻击者ID,被击者ID,然后函数将取出攻击者和被击者的数据,进行计算,然后返回结果给SkillManager。SkillManager里会执行对攻击者消耗的计算以及血量的计算。
因此skillmanger里做了攻击属性的逻辑,那么battlemanager的职责是什么呢?
===========================再次整理下流程==============
当玩家进入攻击状态StateAttacking.cs,执行Process方法,这里用到一个技能ID, 即spellID,根据这个spellID,从SkillData管理组里找出对应的SkillData来,这就是一个技能ID对应的技能数据了。
然后会对SkillData里的每一个skillAction,执行ProcessHit方法,这里根据skillaction里的duration会做一个定时器,时间到了后会触发OnAttackingEnd的事件,最重要的是执行了Entity的OnAttacking ===》BattleManager的OnAttacking ===》SkillManager的OnAttacking,这里是攻击的具体表现,根据SkillAction里的各种表现效果的开关执行
播放特效AttackingFX===》震屏,entity playsfx ===》sfxManager playsfx (spellID)
播放战斗移动 AttackingMove===》 moter move
播放buff AttackBuff===》循环skill action里的加buff和减buff, 对每个buffer id ===》entity clientAdd(Del)Buff ===》BuffManager clientAdd(Del)Buff ===》entity stateflag
然后是最重要的两个步骤,AttackEffect以及通知服务器攻击指令(RpcCall : CliEntitySkillReq)
AttackEffect会先找出本次的攻击列表,然后根据施法者自己是什么类型,比如自己是玩家或者普通怪,会执行不同的攻击函数,随后对每个被攻击者执行OnHit操作。这些攻击函数包括了以下类型:
======================服务器端的设计====================
Skill相关的文件夹在server/scripts/cell/skill目录下,很奇怪,为啥VS解决方案mono里并没有这些文件夹。SkillSystem里的castskill方法里执行了施法操作,遍历每个skillaction,取出每个skillaction的beginduration,endduration,相加就是整个技能的持续时间,按照顺序以及间隔时间做计时器,执行每个skillaction的cast方法。
在每个skillaction的cast里,首先检查自己是否死了,然后检查当前这个skillaction有没有位移,然后看这个skillaction是不是需要召唤怪物,然后调用skillcalculate的FindTargets方法找出技能目标队列,根据其他条件对这个技能目标对象进行过滤,然后对目标队列执行伤害判定,加血判定,buff判定。
其中伤害判定,SkillAction:doDamageAction,通过SkillCalculate的GetDamage方法取出当前技能的伤害值和伤害类型,被攻击者的血量执行相应的判定,然后被攻击者将执行DoDamageAction方法,
buff系统的设计?
=======================我的设计==================
1.如何去创建一个精灵?应该是SpriteModule收到一个创建精灵的消息后,实例化一个精灵。
2.由于精灵身上绑定有状态机组件,因此精灵的状态机是在编辑器里定义的,是否要直接取出这个控制器?
3.编辑功能怎么搞?full inspector?
4.战斗是battlecomponent,监听所有和战斗相关的事件,操作技能模块执行功能。战斗与属性如何管理?
5.