Unity人工智能编程精粹学习笔记 AI角色的复杂决策——行为树

要让游戏里的AI角色能执行预设的逻辑,最直接的方法是依照行为逻辑直接编写代码,但是这种方法工作量大,也容易出错。我们也可以用有限状态机来实现行为逻辑,但是有限状态机难以模块化,编写代码麻烦且容易出错。相较而言,行为树层次清晰,易于模块化,并且可以利用通用的编辑器简化编程,简洁高效。

在处理AI角色的行为逻辑时,有限状态机是一种简单易用的方法。但是,在处理规模较大的问题时,有限状态机很难复用、维护和调试。为了让AI角色的行为能够满足游戏的要求,设计者需要增加很多状态,手工转换需要大量的编码,非常容易出错。

行为树很适合用作AI编辑器,它为设计者提供了丰富的流程控制方法。只要定义好一些条件和动作,策划人员就可以通过简单的拖拽和设置,来实现复杂的游戏AI。

示例

AI角色需要进入密室盗宝,他就要根据房间门的当前状况来确定自己的行为。如果密室门是开着的,就直接进入;如果密室门是锁着的,就破坏门锁闯入。AI角色的行为逻辑可以这样描述:

密室的门是否处于打开状态?如果打开,那么进入房间;

否则,首先移动到门前;

然后检查门有没有锁?如果没有上锁,打开密室门;

否则,破坏锁开门;

进入房间。

根据这个行为逻辑,画出的行为树如图6.2所示:

利用这种方法,可以事先编写一个行为树编辑器,然后在编辑器中把这棵树画出来,最后只需要写出密室门打开、走到门口、强行开门等叶子节点的代码就可以了。

行为树技术原理

行为树主要采用4种节点(在行为树种,“节点”也称为“任务”)来描述行为逻辑,分别是顺序节点、选择节点、条件节点和行为节点。每一颗行为树表示一个AI逻辑。要执行这个AI逻辑,需要从根节点开始遍历执行整棵树。遍历执行的过程中,父节点根据自身的类别,确定如何执行、执行哪些子节点并继而执行,子节点执行完毕后,会将执行结果返回给父节点。

节点从结构上分为两类:组合节点、叶节点。所谓组合节点就是树的中间节点,例如,上面提到的顺序节点和选择节点都是组合节点;叶节点一般用来放置执行逻辑和条件判断,上面提到的条件节点和行为节点都是叶节点。

实际应用时,可以事先由策划人员设计好行为树的结构,程序员只需要实现条件节点和行为节点所定义的具体行为即可,也可以由编程人员预先写好各种不同的条件节点和行为节点的相应代码,以供策划人员选用。然后,策划人员可以尝试将这些条件和行为进行不同的组合,画出不同的行为树,从而实现不同的AI逻辑。

行为树基本术语

在行为树种,将AI角色将要执行的行为用“树”来表示。“树”看上去就像一颗倒置的树,他会有一些“分枝”,“分枝”上会有一些“叶子”,它们都可以称为树的“节点”。“叶子”位于树的最底层,每片“叶子”都会长在一个节点上,该节点便是这个“叶子”的“父节点”,而这个“父节点”还会有自己的“父节点”。

在这颗树中,没有父节点的节点称为“根节点”,没有子节点的那些节点称为“叶节点”,其他的都是“中间节点”(也称为“分支节点”)。

以某个节点为根的子树,是指由这个节点和它所有的后裔节点(子节点,子节点的子节点等)组成的树。

行为树的叶节点

叶节点包含两种类型的节点,分别是条件节点(Condition)和行为节点(Action)

1.条件节点

条件节点可以理解为if条件测试,用来测试当前是否满足某些性质或条件,例如:“玩家是否在20m之内?”、“是否能看到玩家?”、“我的生命值是否大于50”、“我剩下的弹药是否足够?”等。

如果条件测试的结果为真,那么向父节点返回success,否则返回failure。

条件节点的符号是一个矩形,里面写着具体的条件,后面要加上一个问号,表明这是一个需要判断的条件节点。在本书中,为了能够更加清晰地区分,条件节点会用浅灰色矩形表示。

图6.4所示地符号表示一个条件节点,它负责测试“能否看到玩家?”,如果能看到,那么向父节点返回success,否则,向父节点返回failure。

2.行为节点

行为节点(Action)用来完成实际的工作。例如播放动画、规划路径、让角色移动位置、感知敌人、更换武器、播放声音、增加生命值等。在执行这种节点时,可能只需要一帧就可以完成,也可能需要多帧才能完成。

绝大部分动作节点会返回success。

行为节点地符号是一个矩形,里面记录着具体地行为。例如图6.5所示地符号就表示一个实现“向玩家移动”的行为,这个行为可以在某个函数中实现。

行为树中的组合节点

条件节点与行为节点都位于树的叶节点,而树的中间节点是由组合(Composite)节点组成的,组合节点用来控制树的遍历方式,最常用的组合节点有选择节点、顺序节点、并行节点、修饰节点等。

1.选择节点

选择节点(Selector)有时也称为优先级Selector节点,他会从左到右依次执行所有子节点,只要子节点放回failure,就继续执行后续子节点,直到有一个节点返回success或running为止,这时它会停止后续子节点的执行,向父节点返回success或running。若所有子节点都返回failure,那么它向父节点返回failure。

选择节点的符号为一个圆圈,里面带有一个问号。

需要注意的是,当子节点返回running时,选择节点除了会中止后续节点的执行,向父节点返回running以外,他还会记住返回running的这个子节点,下个迭代会直接从该节点开始执行。

用伪代码来表示,选择节点的伪代码如图6.7所示

从图6.8选择节点的执行图中可以看出,条件节点首先执行子节点A,子节点A返回failure,因此它继续向右执行子节点B,子节点B也返回failure,它便继续执行子节点C,子节点C返回success,这时它终止后续节点(即位于子节点C右边的其他子节点)的执行,直接向父节点返回success。

子节点返回success很容易理解,可是,什么时候返回running呢?我们直到,有时行为节点对应的代码执行时间较长,例如当行为节点执行的代码中包括播放动画、寻路等行为时,此时,这个行为节点会向父节点返回running,于是Selector便不再执行后续节点,直接向父节点返回running。

选择节点用来在可能的行为集合中选择第一个成功的。考虑一个试图躲避枪击的AI角色,它可以通过寻找隐蔽点,或者离开危险区域,或寻找援助等多种方式来实现这个目标。为它设计行为树时,可以利用Selector;首先尝试寻找cover,如果成功,直接返回,如果失败,再试图逃离危险区域等等。

2.顺序节点

顺序节点(Sequence),他会从左到右依次执行所有子节点,只要子节点返回success,它就继续执行后续子节点,直到有一个节点返回failure或running为止,这时他会停止后续子节点的执行,向父节点返回failure或running。若所有子节点都返回success,那么它向父节点返回success。

与选择节点相似,当子节点返回running时,Sequence节点除了中止后续节点的执行,向父节点返回running以外,它还会“记住”返回running的这个子节点,下个迭代会直接从该节点开始执行。

顺序节点的伪代码如图6.10所示:

在图6.11所示的顺序节点的执行图中,顺序节点会依次执行它的子节点,首先执行子节点A,这个节点返回success,因此它继续向右执行子节点B,子节点B也返回success,他继续执行子节点C,而子节点C返回failure,因此,它终止后续子节点(例如D)的执行,直接向父节点返回failure。

如果某个顺序节点有一个条件子节点,那么假设条件不满足(即条件节点返回failure),则其他子节点不再执行。

顺序节点通常用来表示一系列需要顺序执行的任务。在前面选择节点所述的“躲避枪击”例子中,每个可能的选择(寻找隐蔽点、逃离危险区域)都可以分解为一个行为序列。例如,为了寻找cover,必须首先寻找一个cover点,然后移动到那里,然后播放一个到达躲藏的动画。如果这个行为序列中的任何一个行为失败了,那么便返回failure,只有所有行为序列都成功了,Sequence节点才会向父节点返回success。

3.随机选择节点

前面提到的“选择节点”是隐含了优先级的,它的最左边的子节点拥有最高的优先级,最右边的子节点具有最低的优先级。

而对于随机选择节点(Random Selector),他不是永远按照从左到右的顺序执行,而是会随机选择访问子节点的顺序。以图6.8为例,随机选择节点不会严格按照A、B、C、D的次序执行,而是每次随机选择执行顺序,例如B、C、A、D或D、B、A、C等。

假设AI角色每天会根据自己的心情选择是呆在家里、工作或是出门游玩,这时,就可以利用随机选择节点,使它每天做出看似随机的选择。

4.修饰节点

修饰节点(Decorator)只包含一个子节点,用于以某种方式改变这个子节点的行为。

修饰节点通常用一个菱形来表示,里面描述了这个节点的具体功能。例如,图6.13所示的修饰节点实现的功能是:循环执行子节点,直到子节点执行失败为止。

修饰节点有很多种,其中一些是用于决定是否允许子节点运行的,这种修饰节点有时也成为过滤器,例如UtilSuccess,UnitFail等。UtilSuccess的行为是这样的:循环执行子节点,直到子节点返回success为止,更具体地说,如果子节点返回running,那么它向父节点返回running,如果子节点返回failure,那么它依然向父节点返回running,直到子节点返回success时,它向父节点返回success。UtilFail的行为与UtilSuccess的行为正好相反,他会循环执行子节点,直到子节点返回failure为止。

例如,一个UtilSuccess的子节点是一个条件节点,检测“视线中是否有敌人”,那么在这个修饰节点的作用下,会不停地检测“视线中是否有敌人”,直到发现敌人为止。

Limit节点用于指定某个子节点的最大运行次数。例如:如果子节点地连续运行次数小于3,那么返回子节点地返回值(可能是success或running),如果子节点地连续运行次数大于等于3,那么不再运行,返回failure。假设AI角色正在尝试破门而入,如果尝试次数大于3,门还是没打开,那么不再继续尝试而返回。

Timer节点设置了一个计时器,他不会立即执行子节点,而是等待一段时间,时间到了才开始执行子节点。

TimeLimit节点用于指定某个子节点的最长运行时间。如果子节点的运行时间超出某个预先指定的值,那么取消子节点的运行,向父节点返回failure,否则直接将子节点的返回值传递给父节点。

还有一些修饰节点用于产生某个返回状态,例如Invert节点实现的功能是对子节点的返回结果取“非”,即如果子节点返回failure,那么它向父节点返回success,如果子节点返回success,那么它向父节点返回failure。

5.并行节点

并行节点(Paralle)有多个子节点,与顺序节点不同的是,这些子节点的执行是并行的——不是一次执行一个,而是同时执行,直到其中一个返回failure(或全部返回success)为止。此时,Paralle节点向父节点返回failure(或success),并终止其他所有子节点的执行。

当某个并行节点有一个条件子节点时,一般是持续地检查某个条件是否满足,如果不满足,终止其他子节点的执行。

并行节点的应用很广泛,它与选择节点和顺序节点一起,构成了行为树的骨架。

在行为树中,可能有一些行为(由条件节点或行为节点实现)要持续较长的时间,例如,寻找从A到B的路径、让AI角色从A移动到B、播放行走动画等,这些行为都无法在一帧中完成,在这种情况下,节点一般会向父节点返回running状态码。前面提到过,这种情况下,下一帧再次遍历行为树的时候,会返回到正在running的节点执行。

那么如果下一帧发生了某种事件,需要打断这些节点的执行,该怎么办呢?例如AI角色发现了玩家,并且正在走过一条隐蔽的路径,试图悄悄接近玩家,这时玩家突然跑开或死亡,那么,AI角色显示应该中断当前的行为,重新根据周围环境做出决策,而不是继续沿着原来的路径行走到终点。

来看一个例子,图6.15中用顺序节点来表示当AI角色看见敌人时,向敌人方向移动并准备攻击。但是,由于向敌人移动需要较长的时间,如果在此期间(例如下一帧)敌人变为不可见(例如生命值降低或被其他AI角色击中而倒下,或是移动到不可见的为止),那么显示需要终止原来的移动。而顺序节点无法做到这一点,它会在下一帧回到刚才返回running的节点继续执行,直到这个节点执行完毕,返回failure或success(也可能时error等)。

这时需要用到并行节点,图6.16中采用了并行节点来实现这个AI逻辑,该节点会不停地检测可视性条件是否满足,一旦失败,就停止移动,返回父节点。这里为了确保条件能够被持续检查,还用到了一个UntilFail(执行直到子节点返回failure)地修饰节点。显示,这时能够达到想要地结果。

子树的复用

如果游戏中有多个AI角色,需要为它们实现不同地行为树,例如,一个AI角色可能攻击性很强,见到敌人就会发起进攻,而另一个AI角色很懦弱,只有找不到躲藏点,也无法逃跑地情况下,才会与敌人战斗。那么这两个AI角色显然需要不同地行为树,但可能这两个AI角色与敌人战斗的方式都是相同的——例如,如果距离很近,那么肉搏,如果距离较远,那么开枪射击等。这时为了避免重复工作,就可以复用战斗的子树。

对比图6.18和图6.19中描述的行为树(它们可能是更大行为树的一部分,即某个行为树的子树),它们虽然是不同的,但是却都有“与敌人战斗”节点,这时,就可以设计一个“与敌人战斗”的子树,并且让他们复用这个子树。

如果场景中有很多AI角色需要用行为树来实现,那么通过这种方式,可以简化大量工作。更棒的是,如果改变了子树,那么复用它的行为树都会相应地进行更新。

使用行为树与有限状态机之间地权衡

(1)对于状态机来说,每个时刻它都处于某种状态种,等待某个事件(转换)的发生。如果事件没有发生,那么继续保持在这个状态;如果事件发生,那么转换到其他状态。因此,状态机本质上是“事件驱动”的,即周围游戏世界发生的“事件”驱动角色的“状态变化”。从实现上来看,状态机既可以采用轮询的方式实现,也可以使用事件驱动的方式实现。

(2)对于行为树,处理周围游戏世界的变化的任务是由条件节点来完成的,这相当于每次遍历行为树时,条件节点都要进行一次轮询,以这种方式来监视游戏世界发生的事情。这实际上是一种轮询的方式,虽然目前已经有了一些高级的技术,能够将事件驱动集成在行为树种,但在实现上,绝大多数行为树都是自顶向下,采用轮询的方式实现的。

(3)一般来说,行为树不太适合需要事件驱动的行为。例如,AI角色需要对大量外部事件做出反应——当AI角色正在向某个目标移动时,突然发生了某个事件,如同伴需要救援、玩家被击中等事件,需要立刻终止这个移动过程,重新做出新的决策等。前面介绍过如果将并行节点和修饰节点结合,是能够处理这种情况的。不过在遇到这种情况的时候,还是应该在状态机或行为树之间做一些权衡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值