游戏中,常见用AI实现方式有2种,状态机和行为树。下面主要简介绍行为树,行为树是用一棵多叉树来表示AI,树的中间节点为控制节点控制着AI的执行流程,叶子节点为行为节点,描述了AI的具体行为。
一、案例描述
山贼的AI需求描述如下:
- 视野内没有敌人则在一定范围内巡逻
- 视野出现敌人则走过去攻击敌人
- 当自己血量 < 20% 则逃跑
下面将分别用状态机和行为树来描述山贼的AI
二、状态机
上图是用状态机来描述山贼的AI, 目前来看状态机的逻辑还是非常清晰,代码实现也比较简单。但随着项目的推进,策划随时可能增加需求。假如现在需要加一个 “石化”状态,描述如下:在逃跑的过程中,如果追击者数量>2,则立即把自己石化,持续5s,此时自己不能被攻击也不能攻击目标
在状态机中加入“石化”状态时,我们需要考虑它跟现有每个状态之间是否有联系,输入输出状态分别有哪些。
1.1 状态机存在的不足
在项目中随时可能增加新状态、减少状态或者改变状态之间的迁移关系,如果状态越来越多,任何一点小修改都会产生很大的工作量,代码中会出现大量的判断跳转,代码的逻辑会变得越来越混乱,如果负责状态机的同事离职了,这会带来很大的问题。
行为树的出现从根本上解决了这些问题,它把每个行为作为一个原子项,提供给策划编辑,让策划来决定AI的执行流程,程序只需要集中精力根据需求增加新的行为,不用关心具体流程。用种方式,程序的工作会得到很大程度的解放,即使有一天交给其他同事维护也比较容易。
三、行为树
行为树是由控制节点、装饰节点、行为节点组成的一棵n叉树,中间节点一般为控制节点和装饰节,用于控制行为树的执行流程,它们相对固定,一旦确定几乎不会变化;叶子节点由行为节点或条件节点组成,它实现了AI中的各种行为,程序的大部分工作都是丰富行为节点。需要注意的是,行为树的每个节点都有一个返回值,它们分别是:
- 成功(Success)
- 失败(failed)
- 运行中(running):表示当前帧没有执行完成、下帧继续执行。
下面列出了行为树常用的节点类型,主要用于说明行为树的原理,类型可能不全面;但只要明白了原理,根据项目需求增加类型即可。
1.1 控制类节点
控制节点一般为中间节点,用于控制行为树的执行流程,决定了其子节点是以顺序、并行、随机或其它方式执行。
顺序节点(Sequences)
依次执行所有子节点,若当前子节点返回成功,则继续执行下一个子节点;若子当前节点返回失败,则中断后续子节点的执行,并把结果返回给父节点。
如下图所求:节点1返回成功,继续执行节点2;节点2返回失败,则把结果返回给Sequences的父节点,节点3并不会执行。顺序节点相当于and
语义。选择节点(Selector)
依次执行所有子节点,若当前子节点返回成功,则中断后续节点运行,并把结果返回给父节点。如下图所示:
相当于or
语义并行节点(Parallel)
依次执行所有子节点,无论失败与否,都会把所有子节点执行一遍。至于Parallel
节点该返回什么值给父节点,这要看需求。比如:成功数 > 失败数返回成功,否则返回失败。
如下图所示:随机节点(Random)
随机选择一个子节点来运行。记忆节点(MemSequences、MemSelector)
功能和顺序节点、选择节点类似,唯一不同是会保存当前执行进度(比如:保存当前子节点索引),下一帧继续执行当前节点,如果当前节点是中间节点,则会跳过前面的节点。
1.2 装饰节点(Decorator)
- 逆变节点(Inverter):对子节点的返回值取反,相当于
not
语义,它只会有一个子节点。 - 成功节点(Succeeder):不管其子节点返回何值,都会返回Success给父节点
- 重复节点(Repeater):重复执行n次子节点。
- 重复直至失败节点(Repeat Until Fail):重复执行子节点,直到失败为上;同样也有类似的
重复直至成功节点
这里就不列出了。 - 执行一段时间(MaxTime):重复执行子节点一段时间
节点的类型是灵活多变的,不同的项目有不同的需求,上面只列出了常用的。
1.3 行为节点(Action)
行为节点都是叶节点,控制节点用于控制行为执行的流程,行为节点则表示具体功能,比如:战斗,逃跑,巡逻等。它至少包含两个函数:
- Init:用于初始化节点,比如读取配置数据初始化当前节点,只会执行一次。
- OnTick:每一帧都会执行,节点的主要逻辑都在此函数中实现或调用。
1.3.1 行为节点代码实现
这是一个在指定范围内查找道具的行为节点例子(Ation):
1 | //节点结构 |
1.3.2 用行为树表示山贼AI
用行为树来表示的山贼AI,并加上了“石化”需求,下图黄色部分:
需求描述:在逃跑的过程中,如果追击者数量>2,则立即把自己石化,持续5s,此时自己不能被攻击也不能攻击目标。
对于程序来说,只需要写”石化”的逻辑即可,至于这个行为用在哪里,执行顺序以及和其它行为的关系,则由策划来决定。在本例中,条件判断其实可以放在行为节里,这里把它独立出来主要是为了方便表达。
三、状态机与行为树比较
1.1 状态机
- 优点:实现简单、执行效率高。
- 缺点:随着状态数量的增多,状态之间的关系会越来越复杂,代码变得难以维护。
1.2 行为树
- 优点:结构清晰、节点间关系弱,程序大部分工作是丰富行为节点,AI流程交由策划完成。
- 缺点:每次tick都会遍历整棵行为树(Mem子节点除外),若树的深度很深,效率将变得低下。
四、关于状态机行为树的思考
从状态机和行为树的特征可以看出,状态机和行为树都存在明显的优缺点。我们可不可以只取它们的优点呢?
在游戏中,若主动怪的视野范围内没有目标它的行为是很简单的,一般会在一定范围内巡逻。如果用行为树,不管视野内有没有人,每帧都会遍历所有非行为节点,这造成了很大的资源浪费。
如果用状态机实现巡逻、死亡、逃跑,进入战斗后的行为用行为树,这会是一个有效的优化。尤其是怪物很多时,大部分时间段,大部分怪都处于巡逻或idle状态,完全没有必要遍历行为树。
如果项目的AI比较简单,比如小游戏之类的。用状态机是个不错的选择。