基于行为树的MOBA技能系统:数值系统
前言
基于行为树的Moba技能系统系列文章总目录:https://www.lfzxb.top/nkgmoba-totaltabs/
在战斗系统中数值系统也是一个核心的系统,当今主流做法是将一个属性分为两个相关联的属性,比如最大生命值就会被分为基础最大生命值 + 额外最大生命值两者之和
基础最大生命值一般而言是初始恒定的
额外最大生命值一般而言是受英雄自身属性,等级,装备,Buff影响的,比如对于力量英雄而言+1力量会为英雄提供20最大生命值,提升一级会为英雄提升80最大生命值,一件装备会提升20%额外最大生命值
其他的例如攻击力,移速,魔法值,法强,护甲,魔抗等都是如此。
此外,还有常见的伤害处理,减速处理等间接影响属性的类型。
分类
战斗数据处理主要分为两大类
- 直接作用于属性上,例如最大生命值,魔法恢复速度,移速等
- 间接作用于属性上,例如伤害,减速,魔法消耗等
并且直接作用复杂度 < 间接作用复杂度
由于间接作用类型的存在,我们就不能用诸如
final = ((base + add) * (100 + pct) / 100);
的形式来处理属性变更了,我们需要找到一种更加灵活的方式来处理这些内容,为什么这样说呢?因为间接作用类型它的变化更加多样,流程也更加复杂,
比如一次A向B发起一次普攻伤害,会先获取A总攻击力,然后计算A身上的相关Buff,比如虚弱(伤害减少40%),石像鬼板甲(开启后会降低50%的输出),振奋光环(提升5%伤害)等,
随后这个伤害值来到B,B接收伤害要先计算暴击,然后格挡伤害,护甲减伤,buff减伤,buff增伤等,
这里我采用的是抽象每一个会更改属性的行为为一个DataModifier,这个DataModifier又可以分为两大类,一类是常量类型的改变(ConstantModifier)(比如增加400最大额外生命值),另一类是百分比类型的改变(PercentageModifier)(比如提升35%移动速度),其中常量类型的需要先于百分比类型的执行。
1 | /// <summary> |
对于DataModifier的管理,使用
1 | private Dictionary<string, List<ADataModifier>> AllModifiers = new Dictionary<string, List<ADataModifier>>(); |
其中Key表示数值修改器的一个类别,例如额外生命值,减速时长,眩晕时长,接收伤害,释放伤害等,Value就是具体的修改器
(PS:这里为了方便讲解就直接以攻击方->受击方的过程直接来描述伤害计算流程了,实际情况是一个伤害数据可能会在攻击方和受击方之间跳来跳去,比如有个Buff是提升实际造成伤害的30%,就需要在受击方计算完成后在把数据发回攻击方计算Buffd的伤害,看起来很复杂,并且似乎打乱了我们的既有架构。其实细想一下,这也只不过是一个特殊的DataModifier罢了,首先我们的伤害数据是一个数据集合,里面包含很多信息,伤害/暴击率/伤害类型等,可以在ADataModifier提供一个虚方法,然后在特殊的DataModifier中重写这个虚方法,在BaptismData(这个BaptismData方法也要修改,第二个参数修改为接受指定Class类型,比如我们的伤害数据,耗魔数据等)的时候执行这个方法即可)
直接作用
对于直接作用类型来说,我们需要保存各类属性的初始值,因为大多数Buff会有持续时间,持续时间结束后需要移除Buff,同样的就得移除对应DataModifier。
1 | public Dictionary<int, float> OriNumericDic; |
这里以DOTA2中的恐鳌之心为例,它的效果为
+400 生命值
+1% 生命恢复
就可以拆分成两个DataModifier
1 | public class ConstantModifier: ADataModifier |
1 | public class PercentageModifier: ADataModifier |
然后调用
1 | //添加DataModifier到字典中 |
这种类型的数值处理往往需要经过两次处理,即发起方-接收方。具体例子在分类那一块已经举过了。
有了直接作用的前置,我们就可以进行形如
1 | //发起方进行伤害处理 |
技能数值计算
技能数值也是非常重要的一部分,有些技能需要根据法强,攻击力,生命值,命中目标数量,护甲等各种各样的加成方式进行数据修正,这一部分的处理,其实是要提前于我们前面说到的两大类处理的。因为那时候的初始伤害是技能已经打出去的伤害,根据角色身上已有的Buff数据进行伤害修正,而技能数据的计算,是与他们无关的,纯粹是技能自身伤害的计算,可能有点抽象,下图比较形象地描述了技能数值计算在整个数值系统中的位置(第二和第三块)
之所以会把技能数值的计算分为两块,是因为其本身的特殊性
第二块偏向于“技能本身无关”属性的计算,例如根据对方最大生命值造成伤害,根据自身护甲值加成伤害等
第三块偏向于“技能本身效果强相关”的计算,例如碰撞到了多个敌人会有伤害衰减,技能已施放时间越长伤害越高
有些类似于一静一动的模式,所以分开去处理
对于第二块的加成处理,目前主流方式是Code硬编码 + Excel配置公式,使用可视化编辑器的话可以更加方便直观的去处理配置这个流程
当然也是少不了手写大量switch硬编码操作的
事件通知
我们的数值往往会显示在UI上,并且是可以实时更新的,所以需要构建一个数值改变时自动分发事件的系统
首先不同的数值类型需要分开
1 | public enum NumericType |
用一个Dic在数值类型和数值真正的值之间做好映射
1 | public Dictionary<int, float> NumericDic = new Dictionary<int, float>(); |
最后在我们更新数值的时候进行事件分发
1 | public void Update(NumericType numericType) |
事件通知会通过AddDataModifier和RemoveDataModifier与前面提到的数据修改器相结合,因为数据修改器集合的修改势必会影响数值,以AddDataModifier为例
1 | /// <summary> |
这样,一个健壮的数值系统就构建完成了
优化
其实在一些情况下我们是不需要让数据经过一大串数据修改器的,比如我们的基础攻击力是40,各种Buff(修改器)加成下会达到50,如果我们的基础攻击力没变,数据修改器也没变,是不是我们就可以直接再使用50这个最终数字了呢?实现方式也很简单,计算Hash即可,典型的空间换时间做法。