你的浏览器不支持canvas

让我们携手共创奇迹!

AI决策方法

Date: Author: LBD

本文章采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。转载请注明来自WindCrazy

这是一篇关于游戏人工智能开发的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巡逻两个地方A和B并在一个状态上附加一些状况,例如当NPC在巡逻时,让他接一个电话然后再恢复巡逻,此时如果使用有限状态机的话我们必须要新建一个打电话的状态来做过渡,但是此时的巡逻有两个再加上原来的逻辑就会很复杂

像这样就会很复杂,多出很多要处理的状态转换逻辑

或者这样

这时,我们可以用分层有限状态机来解决这个问题(启用StateTree插件),

把多个状态机归为一层,如下图:状态树的元素组成:State分层嵌套成树、Condition(进入条件)、Transition(过渡条件)和Task(行为);

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

当然真实情况可能比这个复杂许多,层次结构的想法扩展了管理许多状态的能力,但是并没有消除FSM随着大小和复杂性的增长而变得无法调试和管理的问题。最终,这种状态分层组织的想法导致了以树状结构(也称为行为树)代替组织任务的想法。

行为树

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

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

效用系统

utility-based system,基于效用的系统,会根据权重、比率、队列和许多需要考虑的事项来做出最优选择,使AI比普通的行为树更有头脑。Sims模拟人生的人工智能就是使用的效用系统,在sims中,小人结合当前环境和自身的状态,来做出行动的选择。例如:小人“非常饿”结合环境“没有食物”会比只有“有一点饿”更加吸引人的眼球。

基本概念

Utility AI 会根据当前所处的游戏环境对所有需要执行的 action 进行评分(确定 score),然后从中选取分数最高的一项任务执行。一个简单的 Utility AI 的方式表述如下:

这里的分值 score 是一个归一化的值,取值范围在 [0, 1],这么做是有原因的。因为有时候不同的 action 其实是无法直接比较的,其打分的维度就不一样,此时就需要从全局的角度将所有的维度统一调整,对应的 score 归一化为 [0, 1] 这个范围。基于该 Utility AI 的 Sense/Think/Act 模型表达如下:

在这里,孤立的每个 Utility 值(score)是没有意义的,因为它本质是一种权重值,权重只有在整体评估时才有意义。

回忆一下之前的行为树,Utility AI 其实可以类似的看成将整棵行为树拉平,所有的分支子树都放在一层,其身份都是平等的,唯一不同的就是其权重。

具体使用

1)score 值函数

基于分数的决策,权重值(score)怎么设置得好,是需要大量测试得,比较难调参。

通常来说 score 的设置是需要确定一个 action 在不同的环境输入对应输出的函数模型。如果用 f 来表示一个函数,就是如下的表示方式:

// 某个 action 的 score
score = f(环境input)

例如,一个游戏中的 NPC AI 在不同的饥饿程度时,决定是否去吃饭的情况。如果环境系统探测到 NPC 极度饥饿,那去吃饭的 action 的 score 就可能是 1;相反,如果 NPC 判断自己很饱,去吃饭的 action 的 score 值就是 0。

一种常见的函数是线性函数:

img

或者二次函数:

img

更可能适合实际情况的分段线性函数:

img

具体应该使用哪种函数来拟合,需要大量的实验、调优和测试,最终可能会综合使用上述的各种函数等。

2)数学期望

utility 决策系统中,具体采取哪种 action 有时候也是不确定的,这也比较好理解:现实世界中很多事情就是有不确定性,一个 action 在执行之前,我们无法判断是否应该执行它:它可能会有 N 个后果,每一种后果会有一个概率 prob。

针对这种 action,我们就需要确定该 action 每一种后果的 score 及其发生的概率,然后计算多种后果的加权平均,也就是其数学期望值:

score = score1 * prob1 + score2 * prob2 + ... + scoreN * probN

使用该数学期望值作为 action 对应的 score。

当然以上只是一种简单描述,实际要构造出这样的AI系统是一项复杂的工程。也并不是所有游戏都适用 Utility AI,对于绝大多数游戏,状态机或行为树就能很好满足需求。但是对于某些追求真实代入感的游戏Utility AI更加擅长创造出更复杂,更智能的AI,如:为AI添加情绪、模拟人类行为、战术推理、过程推演、群体行为、与上下文相关的动态规划等等这些高级特性。

目标导向型行动计划

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

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

分层任务网络

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

伪代码:
增加根复合任务到分解列表中
对于每个在我们分解列表中的任务(for)
   移除任务
   如果任务是复合任务
     找到满足当前条件状态并且能处理该复合任务的方法
     如果该方法找到了,增加方法的任务到分解列表中
     如果没找到,恢复到之前分解任务的状态中
   如果任务是原始任务
     在当前状态下执行任务
     增加任务到最终计划列表

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

参考链接

1,游戏人工智能开发之6种决策方法

2,Managing Complexity in the Halo 2 AI System

3,【技术·真相】谈一谈游戏AI - Utility


对于本文内容有问题或建议的小伙伴,欢迎提出问题