编程文汇

游戏中的AI:新手完全手册(上)

#1

介绍

本文将向您介绍游戏人工智能(或简称“游戏AI”)中使用的一系列入门概念,让您了解那些可用于处理AI的工具,它们如何协同工作,以及如何使用您选择的语言或引擎实现它们。

我们假设您具有视频游戏的基本知识,并掌握几何,三角等数学概念。大多数代码示例都是伪代码,因此不需要特定的编程语言知识。

什么 游戏AI?

游戏AI主要关注实体应在当前条件下应该采取什么行动。这就是传统AI文献所指的控制“ 智能代理 ”的情况,其中代理通常是游戏中的角色 - 但也可以是车辆,机器人,或偶尔更抽象的东西,例如整组实体,或者甚至是一个国家或文明。在每种情况下,都需要观察周围环境,根据环境做出决策,并采取行动。这有时称为Sense / Think / Act循环:

  • 感知:角色检测到 - 或被告知 - 他们的环境中可能影响他们行为的事物(例如附近的威胁,要收集的物品,调查的兴趣点)
  • 思考:角色决定做出什么回应(例如,考虑收集物品是否足够安全,或者是否应该首先关注战斗或隐藏)
  • 行动:角色把之前的决策付诸行动(例如,开始朝敌人或物品移动)
  • ......由于角色的行动,当前情况现在就发生了变化,因此必须收集新数据重复以上过程。

在现实世界的AI问题中,特别是那些在发布消息的人,他们通常会专注于这个周期的“感知”部分。例如,自动驾驶汽车必须拍摄前方道路的图像,将它们与雷达和激光雷达等其他数据相结合,并尝试解释他们所看到的内容。这通常是通过某种机器学习来完成的,这种机器学习尤其擅长于获取真实世界大量嘈杂的数据(如汽车前面的道路照片或几帧视频)并且可以解析它,提取语义信息,如“前方20码有另一辆车”。这些被称为“ 问题归类 ”。

游戏不太一样,它们不需要复杂的系统来提取这些信息,因为它是模拟的固有内容。如果前方有敌人,则无需运行图像识别算法来识别; 游戏 知道 那里有一个敌人,可以直接将这些信息提供给决策过程。因此,周期中的“感觉”部分通常要简单得多,复杂性体现在“思考”和“行为”实施中。

游戏AI开发约束

游戏的AI通常有一些必须遵循的约束:

  • 它通常不像机器学习算法那样“预先训练”; 在开发过程中编写神经网络,观察成千上万玩家并学习他们的最佳对战方式的是不切实际的,因为游戏尚未发布而且没有玩家!
  • 游戏通常被认为是提供娱乐和挑战而不是“最佳” - 所以即使角色可以接受最佳方法来对抗人类,这通常不是设计师真正想要的。
  • 通常需要角色显得“逼真”,这样玩家才能感觉到他们好像正在与人类竞争。AlphaGo程序可以远远超越人类,但下棋步骤却和传统方法不一样,经验丰富的棋手几乎感觉自己是在和外星人对抗。如果游戏模拟人类对手,这通常是不合需要的,因此必须调整算法以做出 可信的 决定而不是 理想 的决策。
  • 它需要以“实时”运行,这就意味着算法不能长时间独占CPU。即使只用10毫秒也是太长了,因为大多数游戏只有16到33毫秒的时间来执行下一帧图形的所有处理。
  • 系统至少有一部分是数据驱动而非硬编码的,因为这样的话非程序员可以进行AI调整,从而可以更快的完善AI。

考虑到这一点,我们可以开始研究一些非常简单的AI,它们有效覆盖整个Sense / Think / Act周期,并允许游戏设计师设计出有挑战性、更像人的AI。

基本决策

让我们从乒乓球这样非常简单的游戏开始吧。目标是移动“球拍”把球反弹回去,规则就像网球一样,当你没有把球打回去时就输了。AI具有相对简单的任务,即决定移动球拍的方向。

硬编码的条件语句

如果我们想编写AI来控制球拍,那么有一个直观而简单的解决方案 - 只需尝试将球拍始终放在球下方。当球到达球拍时,理想情况下球拍已经就位,并且可以使球反弹回去。

一个简单的算法,用下面的“伪代码”表示,可能是:

游戏运行时的每一帧/更新: 
如果球位于球拍的左侧: 向左移球拍 ,
否则,如果球位于球拍的右侧: 向右移动球拍

假设球拍可以至少与球一样快地移动,这对于AI玩家来说应该是完美的算法。在检测数据比较少,并且角色可以执行的动作不多的时候,处理过程不会比这更复杂。

这种方法非常简单,整个Sense / Think / Act循环几乎看不到。但它就 那里。

  • 'sense'部分就是两个“if”语句。游戏知道球的位置以及球的位置。因此,AI会向游戏询问这些位置,从而“感知”球是左侧还是右侧。
  • 'think'部分也在两个“if”语句中。这些决定体现了2个决定,在这种情况下是相互排斥的,并且导致选择三个动作中的一个 - 要么向左移动桨,要么向右移动,要么在桨已经正确定位时什么也不做。
  • 'act'部分是“向左移动桨”和“向右移动桨”语句。根据游戏的实施方式,可能采取立即移动球拍位置的形式,也可能涉及设置球拍的速度和方向,以便在游戏代码的其他位置正确移动。

像这样的方法通常被称为“响应式”,因为有一套简单的规则 - 在这种情况下,就是代码中的'if'语句 - 它们对世界的当前状态做出反应并立即决定采取何种行动。

决策树

这个乒乓球示例实际上等同于正式的AI概念“ 决策树 ” 。这是一个系统,其中决策被安排成树形,并且算法必须遍历它以便到达“叶子”,叶子节点包含了采取哪个动作的最终决定。让我们使用流程图绘制乒乓球球拍算法的决策树的直观表示:

DecisionTree1.png

虽然是颠倒的,但你可以看到它类似于一棵树!

决策树的每个部分通常称为“节点”,因为AI使用图论来描述这样的结构。每个节点都是以下两种类型之一:

  1. 决策节点:基于检查某些条件的两种备选方案之间的选择,每个备选方案表示为其自己的节点;
  2. 结束节点:要采取的动作,表示树所做的最终决定。

该算法从第一个节点开始,指定树的“根”,并根据节点的条件决定移动哪个子节点,或者执行存储在节点中的动作并停止。

乍一看,这里的好处可能并不明显,因为决策树显然与上一节中的if语句做同样的工作。但是这里有一个非常通用的系统,每个决策都有1个条件和2个可能的结果,这允许开发人员从代表树中决策的数据构建AI,避免对其进行硬编码。很容易想象一个简单的数据格式来描述这样的树:

节点号 决定(或'结束') 行动 行动
1 球是否在球拍左边? 是?检查节点2 不?检查节点3
2 结束 向左移动桨
3 球是否在球拍右边? 是?转到Node4 不?转到Node5
4 结束 向右移球拍
5 结束 什么都

在代码方面,你有一个系统可以读取每一行,为每个行创建一个节点,根据第二列连接决策逻辑,并根据第三和第4列连接子节点。您仍然需要对条件和操作进行硬编码,但现在您可以想象一个更复杂的游戏,您可以在其中添加额外的决策和操作,并且可以通过使用树定义编辑文本文件来调整整个AI。您可以将文件给游戏设计师,游戏设计师可以调整行为而无需重新编译游戏并更改代码 - 前提是您已经在代码中提供了有用的条件和操作。

决策树真正强大的地方在于它们可以基于大量示例(例如,使用ID3算法)自动构建。这使得它们成为有效且高效的基于输入数据对情境进行分类的工具,但这超出了简单的设计者的编写范围。

脚本

早些时候,我们有一个决策树系统,利用预先撰写的条件和行动。设计AI的人可以根据需要安排树,但他们必须依赖程序员已经提供了他们所需的所有必要条件和操作。要是我们能给设计师更好的工具,让他们创造一些自己的条件,甚至可能是他们自己的一些行为,该怎么办?

例如,程序员不再编写“球是否在球拍的左边”和“球是否在球拍的右边”这样的代码,他可以只提供一个系统,设计师在这个系统中可以自己编写条件来检查这些值 。决策树数据最终可能看起来更像这样:

节点号 决定(或'结束') 行动 行动
1 ball.position.x <paddle.position.x 是?检查节点2 不?检查节点3
2 结束 向左移动桨
3 ball.position.x> paddle.position.x 是?检查Node4 否?检查Node5
4 结束 向右移动桨
5 结束 什么都不做

这与上面相同,但决策中有自己的代码,看起来有点像if语句的条件部分。在代码方面,这将读取Decision节点的第二列,而不是查找要运行的特定条件(如“Is Ball Left Of Paddle”),它会评估条件表达式并相应地返回true或false 。这可以通过嵌入 脚本语言 来完成,像Lua或Angelscript,它允许开发者获取游戏中的对象(例如球和桨)或者创建可以在脚本中访问的变量(例如“ball.position”)。脚本语言通常比C ++更容易编写,并且不需要完整的编译阶段,因此它非常适合快速调整游戏逻辑,并允许团队中较少技术成员能够在不需要程序员干预的情况下塑造功能。

在上面的示例中,脚本语言仅用于评估条件表达式,但是不代表不能做输出操作。例如,像“Move Paddle Right”这样的动作数据可以变成像“ball.position.x + = 10”这样的脚本语句,因此动作也在脚本中定义,而程序员不需要对MovePaddleRight函数进行硬编码。

更进一步,有可能(通常都这样)会在脚本语言中编写整个决策树,而不是作为作为决策树的数据列表。除了没有被“硬编码”以外,这看起来很像我们上面介绍的硬编码 —— 它们存在于外部脚本文件中,这意味着它们可以在不重新编译整个程序的情况下进行更改。通常,甚至可以在游戏运行时更改脚本文件,允许开发人员快速测试不同的AI方法。

事件响应 Responding to events

像乒乓球这样的简单游戏,被设计成在每一帧运行。这里的思想是,持续运行Sense / Think / Act循环,并根据最新的世界状态有所行动。但是对于更复杂的游戏,一般不会评估所有内容,响应“事件”通常更有意义,这些事件通常意味着游戏环境中的显著变化。

乒乓球游戏在这里不太适合,所以我们换一个例子。想象一下,一个射击游戏,在检测到玩家之前,敌人都是静止的,检测到玩家之后,根据角色的定位而采取不同的行为——打手可能冲向玩家,而狙击手可能藏在后面,瞄准开枪。这本质上仍然是一个基本的反应系统 ——“如果看到玩家,然后做一些事情” ——但它可以在逻辑上被划分为(“发现玩家”)事件和反应(选择一个响应并执行它)。

这使我们回到了我们的Sense / Think / Act循环。我们可能会有一些代码,即“Sense”代码,每帧检查敌人是否可以看到玩家。如果没有,没有任何反应。但如果是看到了,它会创建“看到玩家”事件。代码将有一个单独的部分,其中显示“当'看到玩家'事件发生时,执行”,就是你处理Think和Action所需的响应。在你的打手角色上,你可以将你的“ChargeAndAttack”响应函数连接到玩家看到的事件—— 而在狙击手角色上,你可以将你的“HideAndSnipe”响应函数连接到该事件。与前面的示例一样,您可以在数据文件中关联它们,以便可以在不重新编译引擎的情况下快速更改它们。

高级决策

虽然简单的反应系统非常强大,但在很多情况下它们还不够。有时我们希望根据代理当前正在做的事情做出不同的决定,并将其表示为条件不实用。有时,在决策树或脚本中有效地表示它们的条件太多了。有时我们需要提前考虑并在决定下一步行动之前估计情况会如何变化。对于这些问题,我们需要更复杂的解决方案。

有限状态机

一个有限状态机 (FSM或简称)是说,是一种很特別的方式——例如,我们的AI角色——目前处于几个可能的状态之中的一种,他可以从一个状态迁移到另一个状态。这些状态的数量有限,因此得名。一个真实的例子是一组交通灯,它们将从红色变为黄色,再变为绿色,然后再返回。不同的地方有不同的灯光序列,但原理是相同的 - 每个状态代表一些东西(如“停止”,“开始”,“如果可能的话停止”等),它在任何时候只处于一种状态,并且它基于简单的规则从一个状态转换到另一个。

这适用于游戏中的NPC。警卫可能具有以下不同的状态:

  • 巡逻
  • 攻击
  • 逃离

您可能会在更改状态时提出以下规则:

  • 如果一名警卫看到对手,他们会进攻
  • 如果一名警卫正在进攻但无法再看到对手,回去巡逻
  • 如果一名警卫正在进攻但受到严重伤害,开始逃跑

这很简单,你可以把它写成简单的硬编码if语句,一个变量存储守卫所处的状态,并进行各种检查以查看附近是否有敌人,守卫的健康等级是什么样的,等等。想象我们想要添加更多的状态:

  • 空闲(巡逻之间)
  • 搜敌(当先前发现的敌人隐藏时)
  • 请救兵(当一个敌人被发现但是太强大而不能独自战斗时)

每个状态的可用选择通常是有限的 - 例如,如果他们的健康状况太低,警卫可能不会想要搜寻失去的敌人

最终,对于“if <x和y但不是z>然后

”的长列表,它有点过于笨拙,但是有助于形成一种形式化的方式来考虑状态以及状态之间的转换。为此,我们考虑所有状态,并在每个状态下,列出所有到其他状态的转换,以及它们所需的条件。我们还需要指定一个初始状态,以便在任何其他条件适用之前我们知道该如何开始。

状态 转换条件 新状态
空转 已闲置10秒钟 巡逻
敌人可见,敌人太强大了 寻求帮助
敌人可见,健康高 攻击
敌人可见,健康低 逃离
巡逻 完成巡逻路线 空转
敌人可见,敌人太强大了 寻求帮助
敌人可见,健康高 攻击
敌人可见,健康低 逃离
攻击 没有敌人可见 空转
健康低 逃离
逃离 没有敌人可见 空转
搜索 一直在寻找10秒钟 空转
敌人可见,敌人太强大了 寻求帮助
敌人可见,健康高 攻击
敌人可见,健康低 逃离
寻求帮助 朋友可见 攻击
开始状态:空转

这称为状态转换表,是一种表示FSM的综合(如果没有吸引力)方式。 从这些数据中,还可以绘制状态迁移图,并对NPC的状态如何随着时间的推移 有个一个视觉上的直观感受。

StateMachine1v2.png

这张图捕获了角色根据其所处状态进行决策的本质,每个箭头显示状态之间的转换(如果箭头旁边的条件为真)。

每次update或tick时,都会检查角色的当前状态,查看转换列表,如果满足转换条件,则更改为新状态。空闲状态在每帧查是否超过了10秒,如果是,则转换到巡逻状态。同样,攻击状态将检查角色的健康状况是否较低,如果是,则转换到逃离状态。

它处理状态之间的转换 - 但与状态本身相关的行为呢? 在执行给定状态的实际行为方面,通常有两种类型的“钩子”,我们将动作附加到有限状态机:

  1. 定期采取的行动,比如每帧都执行。
  2. 从一个状态过渡到另一个状态时采取的行动。

对于第一种类型的示例,巡逻状态将在每帧继续沿着巡逻路线移动角色。攻击状态将在每一帧尝试发动攻击或移动到可能的位置。等等。

对于第二种类型,请考虑“如果敌人可见且敌人太强→寻找帮助”过渡。角色必须选择去哪里寻找帮助,并存储该信息,以便“查找帮助”状态知道要去哪里。类似地,在“查找帮助”状态下,一旦找到帮助,代理就会转换回攻击状态,但此时它会想要告诉朋友有关威胁,因此可能会出现“NotifyFriendOfThreat”操作过渡。

同样,我们可以通过Sense / Think / Act的角度观察这个系统。感知体现在逻辑转换使用的数据中。思考体现在每个状态的可用转换中。行动是在状态内或者状态转换过程中执行。

尽管有时候对转换条件的连续轮询的代价可能比较高,但是这种基本系统运行良好。例如,如果每个角色必须每帧执行复杂的计算以确定它是否可以看到敌人,以决定是否从巡逻转换为攻击,这可能会浪费大量的CPU时间。 正如我们之前所见,我们可以将重要的游戏世界状态变化视为事件,在它发生时相应他。因此,可以设置一个独立的可见性检测系统以较低频率(比如每秒5次)检测可见事件并通知状态机,而不是让状态机每帧检查“敌人可以看到玩家吗?”。将系统的Sense部分移动到单独的程序,最终执行的Act是相同的,但性能更好,代价就是略微有些延迟(几乎感受不到)。

分层状态机

到目前为止一切都很好,但是大型状态机可能很难处理。如果想通过用单独的MeleeAttacking和RangedAttacking状态替换来代替扩展Attacking状态,我们将不得不修改所有和Attacking状态相关的转换,这个工作量是非常大的。

您可能还注意到我们的示例中存在大量重复的转换。Idle状态中的大多数转换与巡逻状态中的转换相同,如果还需要添加类似的状态,如何能避免这些重复的工作是再好不过的。这时将“空闲状态”和“巡逻状态”组合在某种“非战斗状态”的共享标签下可能是很有意义的,只需要一个共享的转换就可以进入战斗状态。如果我们将这个共享标签本身视为一个状态,将空闲状态和巡逻状态视为其“子状态”,就使得我们能够更有效地组织整个系统。例如,为新的非战斗子状态使用单独的转换表:

主要状态:

当前状态 过渡条件 新状态
非战斗 敌人可见,敌人太强大了 寻求帮助
敌人可见,健康高 攻击
敌人可见,健康低 逃离
攻击 没有敌人可见 非战斗
健康低 逃离
逃离 没有敌人可见 非战斗
搜索 一直在寻找10秒钟 非战斗
敌人可见,敌人太强大了 寻求帮助
敌人可见,健康高 攻击
敌人可见,健康低 逃离
寻求帮助 朋友可见 攻击
开始状态:非战斗

非战斗状态:

当前状态 过渡条件 新状态
空转 已闲置10秒钟 巡逻
巡逻 完成巡逻路线 空转
开始状态:空转

状态转换图:

HierarchicalStateMachine1.png

基本上和上次的图一样,只不过现在用非战斗状态取代巡逻态和空闲态,非战斗状态本身就是一个状态机,有2个子状态巡逻态和空闲态。由于每个状态可能包含子状态的状态机(如果需要的话,子状态还可以包含自己子状态机),我们有一个Hierarchical Finite State Machine(简称HFSM)。通过对非战斗行为进行分组,我们减少了一堆冗余转换,可以对添加的类似新状态共享状态转换执。例如,如果我们将攻击状态扩展为将来的MeleeAttacking和MissileAttacking状态,它们可能是子状态,根据与敌人的距离和弹药可用性之间相互转换,但是根据健康水平等共享退出转换。只需最少的重复转换,复杂行为和子行为可以通过这种方式轻松表示出来。

(译者按:Unity的动画状态迁移图,就采用了分层状态机的形式,如果熟悉它,理解分层状态机就很容易。)

行为树

通过HFSM,我们能够以相对直观的方式构建相对复杂的行为集。然而,这个设计中的决策过程有个问题:决策规则与当前状态耦合紧密。在许多游戏中,这正是你想要的。谨慎使用状态分层可以减少转换重复。但有时你会想要适用于所有或者大部分状态的转换。例如,如果角色的血量低至25%,那么无论是当前处于战斗状态,还是空闲状态,说话状态,还是任何其他状态,您都可能希望它逃离,并且不论你以后给角色添加任何状态,都希望立马逃离。

在这种情况下,理想的方式是在状态之外有一个系统,在该系统中决定当前应该处于什么状态,这样就可以在同一个地方更改规则,并且仍然能够正确转换。这正是是行为树的用武之地。

有几种不同的方法来实现行为树,但其核心部分是一样的,并且与前面提到的决策树非常相似:算法从“根节点”开始,树中的节点代表决策或行动。但是,有一些关键的区别:

  • 节点现在返回3个值中的一个:成功(执行完成),失败(执行失败)或正在运行。
  • 不再有二选一的决策节点在,取而代之的是'装饰'节点,它只有单个子节点。如果它们的条件“成立”,将执行其单个子节点。装饰器节点通常具有条件,它们决定它们是成功(执行它们的子树)还是失败(什么都不做)。They can also return Running if appropriate。
  • 执行操作的节点将返回Running值以表示活动正在进行中

可以组合这一小组节点可以表达大量复杂的行为,并且非常简洁。例如,我们可以将前一个示例中的警卫的分层状态机重写为行为树:

BehaviorTree1.png

使用这种结构,不需要从空闲或巡逻状态到攻击状态或任何其他状态的显式转换 - 如果树从上到下,从左到右遍历,则基于以下方式做出正确的决定。目前的情况。如果敌人可见并且角色的生命值很低,那么无论先前执行的节点是什么,巡逻,怠速,攻击等都会在“逃离”节点停止执行。

您可能会注意到目前没有从Patrolling状态返回到Idling的转换 - 这是非条件装饰器进入的地方。一个常见的装饰器节点是重复 - 这没有条件,但只是拦截返回'Succeeded'的子节点再次运行该子节点,返回“Running”。新树看起来像这样:

BehaviorTree2.png

行为树是相当复杂的,因为通常有许多不同的方法来绘制树,并且很难找到装饰器和复合节点的正确组合可。还有一些问题是检查树的频率(我们是希望每帧都遍历它,还是只有当某些事件发生时才遍历?)以及如何存储与节点相关的状态(我们如何知道空闲了10秒钟?为了能够正确的执行行为序列,我们怎么知道上一次我们执行的是哪个节点?),因此有许多不同的实现。例如,像虚幻引擎4行为树系统这样的系统已经用内联装饰器替换了装饰器节点,只在装饰器条件改变时重新评估树,并把service附加到节点,定期执行update。行为树是强大的工具,但学习有效地使用它们,特别是面对多种不同的实现时候,难度可能是令人望而生畏的。

基于效用的系统(这节先不翻译了)

有些游戏希望有很多不同的可用操作,因此需要更简单,集中的转换规则的好处,但可能不需要完整行为树实现的表达能力。而不是依次设置一组明确的选择,或者树形结构设置的隐式回退位置的潜在动作树,或许可以简单地检查所有动作并选择现在看起来最合适的动作?

基于实用程序的系统就是这样一个系统,其中代理可以使用各种操作,并根据每个操作的相对 效用 选择一个执行,其中实用程序是对执行重要性或期望执行的任意度量该行动是对代理人的。通过编写实用程序函数来根据代理程序及其环境的当前状态计算操作的实用程序,代理程序可以检查这些实用程序值,从而随时选择最相关的状态。

同样,这非常类似于有限状态机,除了其中转换由每个潜在状态(包括当前状态)的得分确定的转换。请注意,我们通常选择最高得分的行动来过渡(或保留,如果已经执行该行动),但是对于更多种类,它可以是加权随机选择(有利于最高得分行动,但允许选择其他一些) ,从前5(或任何其他数量)中选择一个随机动作,等等。

典型的效用系统会将一些任意范围的效用值 - 例如0(完全不合需要)分配给100(完全合乎需要),并且每个动作可能有一组影响该值计算方式的因素。回到我们的守卫示例,我们可以想象这样的事情:

行动 效用计算
FindingHelp 如果敌人可见并且敌人强壮且健康低返回100否则返回0
逃离 如果敌人可见且生命值低,则返回90,否则返回0
攻击 如果敌人可见返回80
空转 如果当前已经空转并已经完成了10秒钟,则返回0,否则返回50
巡逻 如果在巡逻路线结束时,返回0,否则返回50

关于此设置需要注意的最重要的事情之一是操作之间的转换是完全隐含的 - 任何状态都可以合法地遵循任何其他状态。此外,操作优先级隐含在返回的实用程序值中。如果敌人是可见的,并且敌强人物的健康低,则既 外逃FindingHelp 将返回高非零值,但 FindingHelp 总得分更高。同样,非战斗行动永远不会超过50,所以他们将永远被战斗行动打败。行动及其效用计算需要考虑到这一点。

我们的示例使操作返回固定的常量实用程序值或两个固定实用程序值之一。更现实的系统通常涉及从 连续 值范围返回分数。例如,如果代理的健康状况较低,则 逃离 行动可能会返回更高的效用值,如果敌人难以击败,则 攻击 行动可能会返回较低的效用值。这将允许 逃离 行动优先于 攻击 在任何情况下,代理人认为它没有足够的健康来接受对手。这允许相对动作优先级基于任何数量的标准而改变,这可以使这种类型的方法比行为树或有限状态机更灵活。

每个动作通常都有一堆计算效用所涉及的条件。为了避免对所有内容进行硬编码,可能需要使用脚本语言编写,或者以一种可理解的方式聚合在一起的一系列数学公式。请参阅Dave Mark的@IADaveMark讲座和演示文稿,了解更多相关信息。

一些试图模拟角色日常生活的游戏,如模拟人生,增加了额外的计算层,其中代理有一组影响效用分数的“驱动”或“动机”。例如,如果一个角色有饥饿动机,这可能会随着时间的推移而逐渐上升,并且随着时间的推移,EatFood动作的效用得分将返回越来越高的值,直到角色能够执行该动作,减少他们的饥饿程度,然后EatFood行动回到零或接近零的效用价值。

选择基于评分系统的行动的想法非常简单,因此将基于效用的决策制定作为其他AI决策过程的一部分显然是可能的 - 也是常见的 - 而不是作为其完全替代品。决策树可以查询其两个子节点的效用分数并选择得分较高的子节点。类似地,行为树可以具有Utility复合节点,该节点使用效用分数来决定执行哪个子节点。