Unity教程之-游戏人工智能开发之6种决策方法

 

人工智能遵循着:感知->思考->行动
决策方法:有限状态机(Finite-State Machines),分层状态机(Hierarchical Finite-State Machines),行为树(Behavior Trees),效用系统(Utility Systems),目标导向型行动计划(Goal-Oriented Action  Planners),分层任务网络(Hierarchical Task Networks)

有限状态机

有限状态机是目前游戏AI中最常见的行为模型。状态机的代码简单又快速,使用上强大、灵活、计算开销小。
状态机的一个好处是可以可视化,如下图所示:

图中有四个状态:巡逻(patrol),查看(investigate),攻击(attack),逃走(flee),我们把实心圆当做初始状态。
简要过程:假设NPC士兵正在保卫他的阵地,当前状态为巡逻,当他听到什么动静时就会转到查看状态,跑到声音源去查看,如果看到敌人就转到攻击状态,如果没看到过一段时间又会回到巡逻状态。在攻击状态中如果血值低下就会进入逃跑状态。如果击败了敌人,又会回到巡逻状态。
状态机状态类的一个主要结构如下,onEnter函数就相当于unity中的Start()函数,在类开始时调用,作为对旧状态的过度和新状态产生的开始,比如当从巡逻转向攻击状态时,可以在攻击状态的开始让NPC大喊“发现敌人!进攻!”等等。onUpdate()就相当于unity中的Update(),你可以让它每帧都执行,或者几秒钟执行一次,是循环执行的,每次执行时间间隔由你来决定。onExit()就是在退出一个状态之前要执行的,比如,杀死敌人之后由攻击状态转向巡逻状态之前,让NPC做一个欢呼手势并大叫胜利了。FSMTransition列表为将要转到的所有可能的状态。
class FSMState
{
virtual void onEnter();
virtual void onUpdate();
virtual void onExit();
list<FSMTransition> transitions;
};
每个状态还存储着FSMTransition的类,代表能从当前状态可以转到的状态
class FSMTransition
{
virtual bool isValid();
virtual FSMState* getNextState();
virtual void onTransition();
}
当转换条件满足时isValid()返回true,比如当发现敌人NPC就从巡逻状态转到攻击,getNextState()返回将要转到的状态,onTransition()是状态之间转换的过渡,和上面说的onEnter()差不多。
最后是有限状态机类FiniteStateMachine
class FiniteStateMachine
{
void update();
list<FSMState> states;
FSMState* initialState;
FSMState* activeState;
}
有限状态机类包含一个包含所有状态的列表states,initialState为初始状态,activeState为当前状态。
伪代码如下:
在  activeState.transtitions中循环调用isValid(),检测是否符合达到下一状态的条件
如果符合转换条件
调用activeState.on Exit(),退出当前状态
设置activeState 为 validTransition.getNextState(),把当前状态赋值为下一状态
调用activeState.onEnter(),下一状态的开始
如果不符合转换条件,调用activeState.onUpdate(),让NPC执行当前状态需要做的事

在编写有限状态机的代码只前最好画一个上面的草图,这样既可以明确转换关系,又可以不漏掉该有的状态。

分层有限状态机

有限状态机虽然好,但是它有很大的缺点,当状态少的时候可以运用自如,当状态多的时候10个以上就已经结构非常复杂,而且容易出错。
如果我们让NPC巡逻两个地方,比如安全的室内,和门口

如果我们想在一个状态上附加一些状况,例如当NPC在巡逻时,让他接一个电话然后再恢复巡逻,此时如果使用有限状态机的话我们必须要新建一个打电话的状态来做过渡,但是此时的巡逻有两个,所以能接到电话的状态也有两个,然后加了两个相同的状态,很多这样的重复的小状态使得状态机越来越复杂。如下图

这时,我们可以用分层有限状态机来解决这个问题,把多个状态机归为一层,如下图,把巡逻安全处和门口归为看守建筑,这样我们只需要有一个打电话状态就可以了。

分层有限状态机增加了一个滞后,在有限状态机中并没有,在一个普通的有限状态机中,是从初始状态开始的,在分层有限状态机中是一个嵌套的状态。注意上图有H的圈,代表历史状态(history state),当我们第一次进入嵌套状态->看守建筑时,历史状态H表示为初始状态,之后历史状态H表示为最近处在的一个状态。
在我们的例子中:初始状态就是看守建筑,,然后进入看到手机按住这个嵌套,巡逻安全处是初始状态。当从巡逻安全处转换到巡逻门口这个状态时,H历史状态就转变为巡逻门口状态,此时来电话了,转换到接电话状态,接电话结束,我们回到嵌套状态中的历史状态,此时为巡逻门口,可见H历史状态就是一个临时的,便于嵌套外的状态返回到之前的嵌套内的小状态,以不至于出错,或者换回了别的状态,如果接完电话回到巡逻安全处,那就出大错了。
分层有限状态机,就这样避免了重复状态,可以实现更大的更复杂的状态。

实例:
Halo2使用了这一技术,如下图
可见:把使用手雷、掩蔽、防御归为自卫,交战部分使用了多层嵌套,但是原理是一样的,向尸体设计和搜查尸体归为战后处理。在撤退和闲置部分只有一个行为被嵌套,但是日后可以继续添加行为,可扩展性良好。
至于如何在嵌套的层里对行为进行选择,可以就按这个顺序执行,也可以加上权重优先级,或者你想让他执行哪个通过代码来控制。

行为树

行为树是树型结构的,每个节点都代表了一个行为,每个行为都可以有子行为。
所有行为都有一个先决条件,就是产生的这些行为的条件。
整个算法先从树的根部开始,然后开始检查每一个先决条件。树的每一层只可以执行一个行为,所以当一个行为正在执行,它的兄弟节点都不会被检查,但是它们的子节点还是要检查的。相反如果一个行为的先决条件当前并不满足,则跳过判断它的子节点,继续判断它的兄弟节点。一个树全部检查完毕之后,决定执行优先级最大的,然后再依次执行每个动作。
伪代码:
使根节点为当前节点
当存在当前节点
判断当前节点的先决条件
如果先决条件返回true
把节点加到执行清单
使子节点为当前节点
否则
使兄弟节点为当前节点
执行执行清单上的所有行为

不同于状态机,行为树是无状态的,不需要记下之前执行的行为,只是判断行为该不该执行。
行为树的节点之间是不相关的,删除或增加节点,对其他节点都无影响。所以,可扩展性也是行为树的一个优势。另外还可以为决策树添加灵活性与随机性,父节点可以随机决定是否检查子节点。
缺点:决策树做的选择并不一定是最优的,结果也不一定是我们想要的。而且决策每次都要从根部往下判断选择行为节点,比状态机要耗费时间。每次决策都要经过大量的条件判断语句,会变得非常慢。
另外还有一个问题,例如:一个农民要收割作物,敌人出现了,农民逃跑,逃出了距离敌人的一定范围之后,又回去收割作物,走到敌人的范围又逃出,这样来回往复,是一个弊端,,可以根据情况来写代码避免,否则会被玩家***的。

效用系统

人工智能的逻辑->电脑的逻辑,是基于简单的bool问题,比如:“我能看到敌人吗?”,“我有弹药吗”,是简单的是或者不是的问题,所以做出的行为通常是极端化的,一个单一的行动。比如:
if (CanSeeEnemy())
{
AttackEnemy();
}
if (OutOfAmmo())
{
Reload();
}
即时有多条件的行为,bool判断带来的也是一个单一的行动。
if (OutOfAmmo() && CanSeeEnemy())
{
Hide();
}
所以有些情况,只是做这些布尔判断是不合适的,会遗漏很多情况,判断也不妥当。比如:我们可能需要同时考虑与敌人的距离、有多少弹药、饥饿程度、HP值,等等。这些判断条件能映射出许多动作,比我们单一的判断做不做这个动作要好很多。utility-based system,基于效用的系统,会根据权重、比率、队列和许多需要考虑的事项来做出最优选择,使AI比普通的行为树更有头脑。根据上面的例子,使用效用系统我们的AI可以做出我们想要的动作,并根据当前情况做出不同强度的动作,使AI真实、更具可能性,也不再是只有一个正确的选择了。决策树就是对AI说,“只是你将要做的一个行为”,效用系统就是对AI说:“这些是你可能要做的行为”
Sims模拟人生的人工智能就是使用的效用系统(sims的人工智能让我膜拜至今),在sims中,小人结合当前环境和自身的状态,来做出行动的选择。例如:小人“非常饿”结合环境“没有食物”会比只有“有一点饿”更加吸引人的眼球。如果“有一点饿”小人会以接近“美食”为第一执行行为。注意,这里的“美食(的美味程度)”、“食物很少(食物储备程度)”、“一点饿(饿的程度)”,都是一个有范围的数值(常用的是0-1的浮点值)。
当需要选择新的行为时,我们通过分数(上面说的各种程度)来选择相对最优的选择,或者加上一个随机值再选择,使得接近优选的几个选择都有一定几率(几率可根据所加随机值决定)被选中。

目标导向型行动计划

GOAP来源于STRIPS方法,这两种都是让AI创造他们自己的方法去解决问题,我们提供给它一系列可能的动作作为对这个世界的描述,和每个动作使用的先决条件,和行动带来的影响。AI拥有一个初始状态和他需要达到的目标。有一组目标,AI可以通过优先级或当前状态选择一个。计划系统决定一个动作序列来满足当前目标,计划出一个像路径一样的能最简单达到目标状态的动作序列。
GOAP是一个反向链接搜索,从要实现的目标开始,找到什么动作能实现目标,在寻找刚才动作的先决条件,一直往前推,知道达到你的当前(初始)状态。这种反向链接搜索替代了启发式的前向链接搜索。
伪代码:
把目标加到未解决事件列表
对于每个为解决事件(for)
移除这个为解决事件
找到达成事件的动作
如果动作的先决条件已经满足
增加动作到计划中
往回推需要达到先决条件的动作到计划中
否则
添加该先决条件到未解决时间中

例如:我们建立一个NPC士兵,把它的目标设为杀死其他敌人,我们设置它的目标为Target.Dead。为了让目标去死,NPC必须要有一个武器用来射击,这是一个先决条件,但是现在NPC并没有正在装备的武器,NPC就需要执行找到武器这个动作,如果NPC有武器库,他就会从武器库中拿一个,如果病没有武器库,就需要寻路去找一个武器装备了。得到武器装备之后就要找到敌人,实现方式多种多样,徒步寻找、或者NPC周围有车也可以开着车去寻找。我么发现,我们给NPC大量的动作选择,让NPC自己决定该做什么,因而产生动态不可预知又有趣的行为,而且表现得很自然,比开发者创建行为好多了。

分层任务网络

HTN也是寻找一个计划来让AI执行,不同之处在于怎样找出这个计划。开始拥有一个初始状态和一个跟任务代表我们需要解决的问题。原理是最高级的任务分解成更小的任务再继续分解直到我们解决问题。每个高级任务都有很多方式被完成,当前世界状态决定高级任务要分解成哪组小任务。HTN与GOAP相反,HTN是前向链接搜索,是从当前状态一直推到目标状态,向前推直到问题解决。世界状态分散成几种属性,它的HP、精力,敌人的HP、相距距离,计划根据这些来制定。
我们有两种任务:原始任务和复合任务。原始任务是可以只解决问题的任务,也就是可以直接达到目标的任务。在游戏中,它可以为开火、装填子弹、移动到掩蔽物。这些人物可以影响世界状态,开火这个任务需要先有子弹,并执行装填子弹这个任务。复合任务是高级别的任务,可以看作方法。一个方法是一组任务可以完成复合任务,这一组任务是由先决条件决定的。复合任务让HTN推断出世界并且决定该做什么动作。
使用复合任务,我们就能构建一个HTN域,这个域是一大层任务,代表我们解决问题的方法。
伪代码:
增加根复合任务到分解列表中
对于每个在我们分解列表中的任务(for)
移除任务
如果任务是复合任务
找到满足当前条件状态并且能处理该复合任务的方法
如果该方法找到了,增加方法的任务到分解列表中
如果没找到,恢复到之前分解任务的状态中
如果任务是原始任务
在当前状态下执行任务
增加任务到最终计划列表

HTN就是从最高级的根任务分解更小的任务再分解成更更小,分解是需要判断当前状态和条件的。当我们终于分解为原始任务,我们把原始任务加到最终计划中,每一个原始任务都是一个可操作步骤,我们可以直接执行它。

好了本篇unity3d教程到此结束,下篇 我们再会!