背景

游戏中的AI,大多数都是按照规则设定好的,没有太多花哨的技术。原因有几个:一是出于风险成本的考虑,用一个新技术,需要程序员开发相应的算法和框架,游戏设计师重新上手设计方法和摸索算法的适用范围,小团队承受不起这样的开销,大团队又怕做坏名声;
二是“高级”的AI算法并不直观,难以设计。举一个例子,怎么利用神经网络来训练一个士兵AI?我见过的一个例子是设计师用几个按键来操作士兵移动攻击,然后程序自动记录敌我双方的数据和设计师的操作,最后利用这些信息作为input构建一个行为树。这个方法的问题有三个,一是效率奇慢,因为如果数据不够,行为树就可能覆盖不了所有情况,但多少才是“够”,又没有定论;二是错误的操作也会被记录下来,当然可以开发一个功能来删除设计师的某个动作;三是行为树的结果是无法解释的,相当于一个黑箱。

由于上述的原因,一般的公司都会使用比较传统的有限状态机。
虽然第一次听的话可能会拗口,其实有限状态机的意思其实很简单,它包含了有限个的状态和状态间的转换条件。最直白的说法就是几个if...else...语句。为了弄明白这个概念,我们讲一个英雄的故事:
从前有一个胆小如鼠的英雄,

  • 他看到哥布林就会跑过去打它;
  • 他看到半兽人就会逃跑;
  • 他看不到哥布林也看不到半兽人就会休息;
  • 他看到哥布林也看到半兽人也会跑。

我们可以从中抽象出这样的一个有限状态机:

有限状态机很符合我们的认知,但它有一个致命的缺点——它随着状态和转换条件的增多而急速地变得错综复杂,以至于很难对它做出改动(想象一下,多加一个状态,则需要增加几条转换线,越多状态,需要增加的转换线越多)。

也由于这个原因,比较多的开发者投入了行为树的怀抱。

行为树定义

行为树的概念会比状态机要复杂些——行为树是一个包含逻辑节点和行为节点的树结构,每次需要找出一个行为的时候,会从树的根节点出发,遍历各个节点,找出第一个和当前数据相符合的行为。
很拗口对吧,下面来一个生动点的解释,继续上面的英雄的故事,但是是用行为树表达出来:

这个行为树是等价于之前的有限状态机的。其中

Root

是根节点,每次需要寻找行为的时候都必须从这里开始。
Priority Selector

是一个逻辑节点,它的意思是让从左到右遍历自己的子节点,如果子节点的准入条件符合信息的话,就执行该子节点。如果英雄只看到哥布林,那么Orc in sight这个准入条件不符合,Escape不执行;Globlin in sight符合,于是执行Fight;因为Fight在Idle的左边,所以Fight的优先程度更高,于是Idle不执行。在我们的例子中,Idle可以看作是default behavior。

优点

从简单的行为树和有限状态机的对比,我们就可以看出,行为树由于引入了逻辑节点,它的转换条件更加少(线更少,更清晰),让拓展AI变得更加容易
行为树还有另外一个优点:行为的重用(reuse)。
例如,Escape有一个跑的行为,而Fight则有跑的行为,和砍怪的行为。请看图:

Sequence同样是一个逻辑节点,它的意思是从左到右按顺序执行子节点,并且仅仅在一个子节点执行完成后才执行下一个子节点。在例子中,Do Run需要有一个自己判断到达目的地的方法,当该方法返回end的时候,才会执行Do Slash。

Escape和Fight的Do Run行为节点是一样的,只是Fight多了一个Do Slash行为节点而已。所以Do Run是一个可以重用的节点。
在行为树中,我们能够编写好Do Run,Do Slash这些基础的行为节点,和设定一些准入条件,就可以组成千变万化的AI了!
我们希望英雄在逃跑的时候群众发出嘘声,而在攻击的时候出现欢呼声:     

省略了其他部分,只画Escape部分。
Parallel是一个逻辑节点,它的意思是让所有子节点同时运行,那它什么时候结束呢,可以使当所有子节点都完成的时候结束,也可以让任一子节点完成时结束,视乎需要来做出选择。
那么在攻击当中会是怎么样实现的呢?大家可以尝试画出来,习惯一下行为树的思维方式。

总结:

  • 行为树拥有3种节点:
    • 根节点 Root
    • 逻辑节点(可拓展):
      • Priority Selector
      • Sequence
      • Parallel
    • 行为节点
  • 行为树在复杂的情况比有限状态机更清晰,更可拓展
  • 行为树有利于逻辑的重用
  • 设计得好的行为树可以千变万化!

之后,我会展示在游戏开发中怎么利用行为树作为框架来使用。

作者:伍一峰
来源:知乎