战斗系统设计概述
同步方案:
在同步方案上采用的是状态同步的方案,主要的考虑是状态同步是网易多款实时手游均采用的方案,得到了验证,风险性较小。客户端接受用户的操作输入发送给服务器,所有战斗逻辑在服务器计算完成,服务器将客户端表现指令发送给客户端执行。其中,为了精确匹配远程攻击和相应扣血,所有的远程的扣血由客户端判定命中后向服务器请求触发。
代码架构:
战斗代码结构battle.lua是整个战斗逻辑的最上层。battle.lua处理消息的接受和分发、战斗的主逻辑think的执行,战斗开始和结束的管理。
fighter.lua管理每个玩家的行为和卡牌状态。
base_entity.lua、entity.lua、entity_moveable.lua、tower.lua、arrow.lua、...等等是各类Entity的类,他们之间有继承关系,处理Entity状态机。
battle_base.lua是处理所有战斗的基础功能类,功能包括Entity创建、Entity选择、Entity管理、Entity死亡处理、数学运算处理、寻路入口。
battle_net.lua是处理所有战斗消息的发送。
skill_base.lua是处理所有技能的释放,skill_.lua以及buff_.lua是所有技能和buff具体的实现。
astar_path_find.lua和collide.lua分别处理网格注册管理、Entity碰撞。
AI:
(1)在人人的PVP战斗中,两方玩家只需要将单位拖动到场上,上场后单位由AI托管。每个Entity的AI采用状态机的方式来实现。Entity的Think函数在每帧中:1. 判定state是否满足转化条件,如果满足进行state的转化 2.执行state内的行为。Entity的主要的state包括:fixed、idle、ahead、walk、run、attack、pursue、dead、skill。
(2)在人机的PVP战斗中,有专门的机器人AI来模拟玩家的行为。首先,机器人在选牌阶段,会尽量保持牌组的均衡,避免选出过于极端的阵容,具体的做法是会在可选的牌中选择一个攻击评分top2、防御评分top2的卡牌,然后随机选择剩余的牌。战斗中,AI每隔1s会进行一次think,每次think只会产生一次操作(为了模拟人类的思考间隔)。采用行为树来实现具体的think,如下图所示。
行为树另外,在选择了要放置的卡牌之后,它的放置位置的选择也比较重要。位置选择的主要原则有:(1)进攻时,围绕英雄或者聚集式放置单位(2)防守时,根据对方的来犯单位放置单位。英雄的技能的自动释放也由AI机制进行管理,它的触发由独立的逻辑判定触发,会根据当前场上英雄的技能的特性判定是否到达最佳的释放时机,然后决定是否释放。
(3)PVE战斗怪物和Boss的AI触发条件和行为比较复杂,每个副本都需要可配置具体的AI,采用的ACT(Action、Condition、Trigger)方案,可以支持比较灵活的AI配置。每个怪物和Boss都可以配置多条ACT逻辑。它们的think函数每帧判定自己的ais里是否有Trigger被触发,触发后判定Condition是否通过,通过则触发Action。
技能和buff系统:
由于我们的游戏技能设计比较复杂,技能和buff系统是整个战斗最重要也是最庞大的部分。
从释放方式上来说,分为:
main_skill --普攻技能
init_skill --出生直接释放一次的技能,没有技能动作
birth_skill --出生自动释放一次的技能
auto_skill --自动释放技能
manual_skill --手动释放技能
dead_skill --死亡释放技能
init_buff --出生自带的buff
另外,技能可以拥有子技能(主技能释放时会触发子技能的释放)、计数技能(根据使用次数触发的替代技能)。
技能的具体类型分为16种:
attack =1, --攻击
create =2, --召唤
move =3, --位移
buff =4, --buff
field =5, --法术场
dispel =6, --驱散
taunt =7, --嘲讽
heal =8, --治疗
field_arrow =9, --移动法术场
field_aura =10, --光环
evocate =11, --复活
energy =12, --回费
purge =13, --净化
gather =14, --聚人
transfer =15, --传送
push =16, --推人
buff是技能系统的重要组成部分,具体类型分为19种:
fixed =1, --定身
perporty =2, --属性
attack =3, --伤害
heal =4, --治疗
soul_link =5, --灵活链接
trigger =6, --触发技能
power_scale =7, --系数
disapear =8, --消失
dead =9, --定时死亡
stealth =10, --隐身
immune =11, --免疫
subdue =12, --征服
follow =13, --跟随
absorb =14, --吸住
suck =15, --吸血
dying =16, --垂死
protect =17, --护盾
ability =18, --基础能力
accum_power_scale =19, --累加系数
技能的释放过程包括选择目标、对选择的目标释放技能。选择的类型包括:
RANGE_TYPE= {--主类型
single =1, --单体
circle =2, --圆形
rect =3, --矩形
self=4, --自己
sector =5, --扇形
focus =6, --当前目标
}
SUB_RANGE_TYPE= {--子类型
random =1, --随机
closet =2, --最近
exclude_target =3,--除了当前目标
}
选择目标的时候会采用主类型和子类型结合选择出目标。另外,还支持一些特殊类型的选择目标需求,包括:
SPECIAL_SELECT_TYPE= {
random =1, --随机
random_buff =2, --筛选buff
random_hp =3, --筛选hp
random_type =4, --筛选目标type
}
以上是对技能系统的概述,还有很多细节,这里不做过多的讨论。
寻路碰撞系统:
寻路系统随着地形的需求不同做了几次迭代,也陆续实现了几种寻路方式,最终地图采用了无路障的方式,也就抛弃了寻路系统的使用。这里还是说明一下我们在寻路系统上做的一些工作。最早,实现了astar的寻路算法,astar的优点是能够解决动态碰撞的问题,缺点在于计算量比较大(有考虑过预先bake路线)、路线比较直有明显的走格子的痕迹。又尝试了导航网格的寻路算法,对于我们的障碍物比较大块的地图来说,导航网格对地图的划分比较少,计算量相对于astar算法会比较小。另外,导航网格寻路的路线比较自然。但是它不能解决动态碰撞问题。
碰撞系统在效果上是要模拟皇室冲突兵种单位之间推来推去的效果。网上没有现成的库可以用,我自己实现了一个算法。在设计这个算法时,最担心是性能问题,所以整个算法的出发点是如何降低性能开销。首先,我们把整个地图划分为1x1的格子单位,这个大小和我们的单位的直径比较接近。单位在移动的过程中会更新自己的所在格子信息。单位移动时会触发一次碰撞处理,根据自己当前的格子以及移动的方向,确定有可能产生碰撞的单位所在的格子,然后取出格子中的单位做碰撞处理。一个单位被碰撞后会产生位置的移动,会发起一次子碰撞处理,这是一个递归的过程。这个过程不加特殊处理很容易陷入死循环,我做了约束,一个单位在一次碰撞处理中,只会被移动一次。这个算法在实际使用中,发现有些速度相同的单位叠在一起后,会陷入互相挤压无法分开。为此,我加了一个简单的处理,判定两个单位是否处在重叠状态,如果处在重叠状态,会直接强制拉开。经过反复的算法调整,目前效果没有出现比较明显的问题。
天梯匹配的机器人难度控制:
在游戏测试时,由于玩家的导量不足,需要有机器人去模拟玩家跟玩家作战。机器人有两大问题,第一就是如何尽可能的模拟玩家去作战,第二就是如何给玩家匹配一个水平相当的机器人。这里主要讨论第二点,如何给玩家匹配水平相当的机器人。其他游戏有很多可以参考的做法,比如经典的war3、dota。一般采用的方法分为两类:1. AI分级 2.获得钱的速度分级(属性分级) 。AI分级比较难,而且很难做到天梯这么多档次的分级。属性分级相当比较容易线性的调节。我们主要的做法是通过属性分级来做。我以玩家和机器人作战的胜率作为算法的核心目标,为了让玩家打电脑没有那么强的挫败感,我们定的胜率是65%(是下一场的期望胜率,不是总体胜率)。这个胜率作为权重计算出玩家对电脑的净胜场数。我们将电脑的难度分为上下一共20个level,每个level对应一定的属性加成或者减少,根据净胜场数得到对应的level。
净胜场是比赛机制中常用的用来判定水平的标准、另外一个判定水平的标准是胜率。