每天清晨,闹钟把你从梦境里拽出来,大脑里同时涌出几个念头:再睡十分钟、躺床上刷会儿手机、赶紧起床洗漱、出门跑两圈。你并没有按照一张固定的时间表行动,而是飞快地给每个选项打了分——再睡十分钟的诱惑分本来很高,可一想到今天要赶早班飞机,这个选项的分数瞬间暴跌,你最终选择了分数最高的那个动作:起床。这个“给每个选项打分,按最高分行动”的本能过程,正是游戏里 Utility AI(效用 AI)的核心思想。游戏中的 NPC(非玩家角色)每天都在面临类似的抉择:看到敌人,是冲上去攻击、掉头逃跑,还是继续巡逻?传统 AI 让它们照着预设剧本“表演”,而 Utility AI 让角色像你一样,根据当前局势实时评估每个行为值不值得做,选出最优的那个执行。角色因此不再是提线木偶,而像一个会审时度势的“活人”。

岗位关联

学会 Utility AI 评分机制,你就能让 NPC 从“按剧本表演的程序”升级为“会随机应变做决定”的智能角色,这是游戏 AI 程序员最核心的竞争力之一。

目录
  1. 可视化演示
  2. 基础概念:给每个念头打分
  3. 效用函数:需求曲线怎么画
  4. 考虑因素:哪些信息影响评分
  5. 决策流程:从评分到行动
  6. 权重调参:让角色表现更自然
  7. 方案对比:与状态机行为树的取舍
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示观察者设计模式的核心机制:主题状态变更时自动通知所有注册的观察者,实现松耦合的事件驱动通信。

本节配有交互动效(进入视口后自动加载,约 3KB)。

通知模式:状态变更传播 主题(Subject)状态变更后,调用notify方法遍历所有注册的观察者 每个观察者收到update回调后执行各自的业务逻辑 notify()→update() 观察者模式实现一对多的依赖关系,主题不需知道观察者具体类型

基础概念:给每个念头打分

你被闹钟吵醒的瞬间,心里其实在开一场“小型招标会”:再睡十分钟、看手机、洗漱、跑步……每个候选行动都是一份标书,你的大脑根据“现在还困不困”“今天有没有急事”“昨晚睡得好不好”给每份标书打分,最终采用分数最高的那份。游戏里的 NPC 没有大脑,但程序员可以为它搭建同样一套“招标会”。

Utility AI 把 NPC 的每个候选行为看作一份标书,行为得分称为“效用值”(Utility)。效用值不是随手拍的,而是由多个考虑因素加权累加得到的。比如“攻击”这个行为,要考虑敌人的距离、自己的血量、手里的弹药量;距离越近、血量越健康、弹药越充足,“攻击”的效用值就越高。这和评估“再睡十分钟”时综合考虑“被窝舒适度”和“迟到风险”是同一个逻辑。

每个候选行为的效用值用下面的公式计算:

回到起床场景:你之所以在赶飞机那天果断放弃赖床,正是因为“迟到风险”被赋予了极高权重。权重不是固定不变的,它会随情境动态调整——这正是下一节要讲的效用函数。

U(b)=∑i=1nwi×ciU(b) = \sum_{i=1}^{n} w_i \times c_i
一个行为的效用值等于所有“考虑因素得分 × 对应权重”的总和。权重越大,这个因素越能左右最终决定。
  1. 候选行为 $b$$b$ 代表 NPC 当前能做的某一个行动,例如“攻击”“逃跑”“巡逻”。
    b∈{攻击,逃跑,巡逻}b \in \{\text{攻击}, \text{逃跑}, \text{巡逻}\}
  2. 考虑因素 $c_i$$c_i$ 是第 $i$ 个考虑因素归一化后的分数,取值范围是 0 到 1,“敌人距离 0.8”表示敌人近在咫尺。
    0≤ci≤10 \le c_i \le 1
  3. 权重 $w_i$$w_i$ 表示第 $i$ 个考虑因素的重要程度,血量权重高于弹药权重,说明 NPC 更惜命。
    wi>0w_i > 0
  4. 加权累加求和把每个“权重乘以分数”的结果累加起来,就得到该行为的效用值,分数越高越优先。
    U(b)=∑i=1nwi×ciU(b) = \sum_{i=1}^{n} w_i \times c_i

效用函数:需求曲线怎么画

想象你饿着肚子走进自助餐厅,面前摆着一碗红烧肉。第一口下去幸福感爆棚,第二口还不错,吃到第十口你已经只想找点蔬菜解腻。同样一道菜,带来的满足感随着“已经吃了多少”迅速衰减。效用函数(Utility Function)干的就是这件事:把原始数值(距离、血量、饥饿度)映射成一个能反映这种“边际满足感”的分数。

在 Utility AI 里,我们通常不让效用值随输入线性上涨,而用一条“收益递减”的曲线。最常用的是:

回到自助餐厅的例子:第一盘肉让你满足感的边际增量最大,第二盘增量变小,到第十盘几乎为零。$v(x)=x/(x+k)$ 正是这种“先陡后平”的感受曲线。NPC 的攻击欲望也是这样:敌人从 20 格移到 10 格时战斗欲望涨幅明显,但从 3 格移到 1 格时涨幅就很小了——因为已经够近了,再近也差不多。

v(x)=xx+kv(x) = \frac{x}{x + k}
这条曲线把任意非负输入 $x$ 映射到 0 到 1 之间,$k$ 越大曲线越“钝”,需要更大的输入才能达到同样的分数。
  1. 原始输入 $x$$x$ 是某个考虑因素的原始测量值,例如“敌人距离 8 格”或“当前血量 30 点”。
    x≥0x \ge 0
  2. 曲线常数 $k$$k$ 控制曲线形状,$k$ 越大,同一 $x$ 对应的分数越低,曲线越平缓。
    k>0k > 0
  3. 效用分数 $v$$v(x)$ 输出 0 到 1 的分数,$x$ 趋近无穷大时 $v$ 趋近 1。
    lim⁡x→∞xx+k=1\lim_{x \to \infty} \frac{x}{x + k} = 1

考虑因素:哪些信息影响评分

你点外卖时会看哪些信息?距离(太远送达慢)、满减优惠、店铺评分、是否有想吃的菜品。这些信息共同决定你最终选哪家店。NPC 也一样,它的每个考虑因素(Consideration)都来自游戏状态,比如敌人距离、自身血量、弹药余量和技能冷却。

每个考虑因素都需要“归一化”,把不同量纲的数值统一映射到 0 到 1,否则“距离 100 米”和“血量 0.5”根本无法相加。以敌人距离为例,通常按如下公式计算距离分数:

点外卖时“评价 4.9 分”和“距离 5 公里”无法直接比较,必须换算成你心里的“值得程度”。归一化就是游戏世界的换算器,它让血量、距离、弹药这些单位完全不同的量,能放进同一个公式里公平比武。

cdist=1−min⁡(ddmax⁡,1)c_{\text{dist}} = 1 - \min\left(\frac{d}{d_{\max}}, 1\right)
距离分数在 0 到 1 之间:敌人越近分数越高;当距离达到 $d_{\max}$ 后分数归零——远到可以忽略。
  1. 当前距离 $d$$d$ 是 NPC 与敌人之间的当前距离,单位通常是游戏内的格数或米数。
    d≥0d \ge 0
  2. 最大感知距离 $d_{\max}$$d_{\max}$ 是 NPC 的“警戒半径”,超过这个距离就认为敌人不存在。
    dmax⁡>0d_{\max} > 0
  3. 比例截断$\min(d/d_{\max}, 1)$ 把超出半径的数值压回 1,避免出现负分数。
    min⁡(ddmax⁡,1)≤1\min\left(\frac{d}{d_{\max}}, 1\right) \le 1
  4. 取反映射$1 - \ldots$ 让距离越近分数越大,贴合“敌人靠近才值得动手”的直觉。
    cdist=1−min⁡(ddmax⁡,1)c_{\text{dist}} = 1 - \min\left(\frac{d}{d_{\max}}, 1\right)

决策流程:从评分到行动

自助餐厅里你最终夹起哪盘菜?答案是:把每道菜在心里过一遍总分,然后伸手去拿分数最高的那盘。Utility AI 的决策循环同样只有三步:列出候选行为、逐个计算效用值、选出最大值执行。每帧(或每 0.5 秒)重复一次,NPC 就能根据局势变化不断调整行动。

决策的数学表达非常简洁,就是求效用值最大的那个行为:

注意 $U(b)$ 和 $\arg\max$ 的区别:$U(b)$ 是“分数”,$\arg\max$ 是“拿最高分的那个人”。就像在自助餐厅里,你的重点是“端起哪盘菜”而不是“那盘菜打了多少分”。分数只是手段,行为才是结果。每帧重算一次,NPC 就能在敌人接近的瞬间从“巡逻”切换成“攻击”,完全不需要“if 距离小于 5 就攻击”这种硬编码。

b∗=arg⁡max⁡b∈BU(b)b^* = \arg\max_{b \in B} U(b)
在行为集合 $B$ 里,找到让 $U(b)$ 最大的那个行为 $b^*$。分数最高的行为胜出,其余候选全部出局。
  1. 行为集合 $B$$B$ 是当前可执行的所有行为,例如 {攻击,逃跑,巡逻}。
    B={b1,b2,…,bm}B = \{b_1, b_2, \ldots, b_m\}
  2. 效用函数 $U$$U(b)$ 依据加权公式算出每个行为的分数,是决策的依据。
    U(b)=∑i=1nwi×ciU(b) = \sum_{i=1}^{n} w_i \times c_i
  3. 选出最大值$\arg\max$ 返回使 $U(b)$ 取得最大值的行为本身,而不是返回最大分数。
    b∗=arg⁡max⁡b∈BU(b)b^* = \arg\max_{b \in B} U(b)

权重调参:让角色表现更自然

同样是点外卖,你朋友把“优惠”权重调得极高,每单必凑满减;你看重“送达速度”,宁可少省三块钱也要 30 分钟内吃上。同一个加权公式,只要调整权重,角色就能走出完全不同的行为风格。权重调参(Weight Tuning)是 Utility AI 最有意思也最磨人的环节。

权重初值通常是人为拍定的,为了便于比较和调试,一般先做归一化,让所有权重加起来等于 1:

除了权重数值,还要调整效用函数的曲线常数 $k$。$k$ 越小曲线越陡,很小的输入变化就会触发行为切换,角色显得“神经质”;$k$ 越大曲线越钝,角色反应迟缓,显得“木讷”。调参没有银弹,最实用的方法是先让 NPC 跑起来,观察它在极端情况下的表现——比如残血时该不该依然莽撞攻击——再反向修正权重,直到角色的行为风格符合你的设计预期。

wi′=wi∑j=1nwjw_i' = \frac{w_i}{\sum_{j=1}^{n} w_j}
归一化后的权重总和为 1,每个权重可以被理解为“这个因素占决策的百分比”,调参时更有手感。
  1. 原始权重 $w_i$$w_i$ 是设计师设定的初始权重,例如攻击行为先给距离 0.7、弹药 0.3。
    wi>0w_i > 0
  2. 权重总和$\sum_{j=1}^{n} w_j$ 计算全部因素的权重之和,是归一化的分母。
    ∑j=1nwj=0.7+0.3+⋯\sum_{j=1}^{n} w_j = 0.7 + 0.3 + \cdots
  3. 归一化权重 $w_i'$$w_i' = w_i / \text{总和}$,让所有权重加总为 1,互相之间可以直接比较。
    ∑i=1nwi′=1\sum_{i=1}^{n} w_i' = 1

方案对比:与状态机行为树的取舍

传统游戏 AI 常用两种方案。有限状态机(FSM)像一张严格的课程表:早上 7 点起床、8 点上课、12 点吃饭,每个时间点做什么是写死的;行为树(Behavior Tree)像一张优先级清单:先检查“有没有着火”,再检查“要不要攻击”,逐条匹配第一条满足的条件就执行。而 Utility AI 像你面对自助餐厅时的临场发挥——不查表、不按固定顺序,直接给所有选项打分,选最高分。

从设计复杂度看,三种方案差别明显。状态机每增加一个状态,就要考虑它与所有已有状态之间的转移,最坏情况下需要设计的转移数量是状态数的平方;Utility AI 则只需要为每个行为准备一组考虑因素:

但“省设计量”不等于“更好用”。状态机的优势是确定性极强,测试员可以精确预测每一次切换;行为树的优势是逻辑直观,策划和美术也能看懂流程图。Utility AI 的优势在于连续性和丰富性:多个因素同时作用,行为会随数值涨落平滑变化,不会出现状态机那种“跨过某个阈值就瞬间变脸”的生硬感。选哪种方案,取决于游戏类型:格斗游戏的连招判定适合状态机,开放世界的 NPC 日常行为则更适合 Utility AI。

NSM=S2NUA=B×CN_{\text{SM}} = S^2 \qquad N_{\text{UA}} = B \times C
状态机的设计量随状态数平方增长,Utility AI 的设计量则是行为数与考虑因素数的乘积,扩展新行为更省力。
  1. 状态数 $S$$S$ 是有限状态机里的状态数量,例如 {巡逻,追击,攻击,逃跑}。
    S=4S = 4
  2. 状态机设计量 $N_{\text{SM}}$$S^2$ 表示最坏情况下需要设计的转移数量,状态一多就组合爆炸。
    NSM=42=16N_{\text{SM}} = 4^2 = 16
  3. 行为数与因素数 $B \times C$$B$ 是行为数,$C$ 是每个行为平均的考虑因素数,Utility AI 的总设计量是两者的乘积。
    NUA=B×CN_{\text{UA}} = B \times C

AI 应用场景

开放世界 NPC 的日常行为调度

阶段:决策层(行为选择与调度)

解决的问题:传统巡逻 AI 只会按固定路线走,玩家一靠近就突然切换状态,显得机械僵硬。Utility AI 把“巡逻”“闲逛”“观察”“逃跑”等行为同时打分,时间、天气、玩家声望、距离等因素共同影响分数,角色行为平滑自然。

具体案例:《GTA》系列和《荒野大镖客2》中的路人 NPC,会根据天气、时间和玩家的举动综合决定自己是回家、继续闲逛还是报警。守卫发现异常时不会瞬间变脸,而是先“疑惑”再“走近查看”,这正是因为每个行为的效用值在连续变化。

即时战略游戏单位的战场决策

阶段:单位个体决策

解决的问题:RTS 游戏单位数量多、战场局势变化快,用行为树逐条判断容易漏掉关键信息。Utility AI 让每个单位根据自身血量、敌我兵力比、资源余量给“攻击”“撤退”“采矿”“维修”打分,实现混战中个体的合理判断。

具体案例:在类《星际争霸》游戏中,残血单位会自发后撤,满血单位继续顶在前线;工程师在基地被偷袭时会优先维修而非补矿。这些表现都可以通过调节“血量权重”“威胁权重”来实现,无需为每种情况单独写一条规则。

Python 演示

python73 行
import math  class Consideration:    """单个考虑因素: 把原始数值映射为 0~1 分数"""     def __init__(self, name, curve_k, weight):        self.name = name        self.curve_k = curve_k        self.weight = weight     def score(self, raw_value):        return raw_value / (raw_value + self.curve_k)  class Behavior:    """一个候选行为: 多个考虑因素加权求和得到效用值"""     def __init__(self, name, considerations):        self.name = name        self.considerations = considerations     def utility(self, context):        total = 0.0        for cons in self.considerations:            total += cons.weight * cons.score(context[cons.name])        return total  class UtilityAI:    """决策器: 给所有行为打分并按分数降序排列"""     def __init__(self, behaviors):        self.behaviors = behaviors     def decide(self, context):        scored = [(b.name, b.utility(context)) for b in self.behaviors]        scored.sort(key=lambda item: item[1], reverse=True)        return scored  def main():    attack = Behavior("攻击", [        Consideration("敌人距离", 5.0, 0.6),        Consideration("弹药余量", 2.0, 0.4),    ])    flee = Behavior("逃跑", [        Consideration("失血比例", 1.0, 0.7),        Consideration("冷却可用", 0.5, 0.3),    ])    patrol = Behavior("巡逻", [        Consideration("安宁程度", 3.0, 0.5),        Consideration("路线长度", 4.0, 0.5),    ])     ai = UtilityAI([attack, flee, patrol])    context = {        "敌人距离": 8.0,        "弹药余量": 20.0,        "失血比例": 30.0,        "冷却可用": 1.0,        "安宁程度": 2.0,        "路线长度": 10.0,    }    ranked = ai.decide(context)    print("=== 决策结果 ===")    for name, score in ranked:        print("%s: %.3f" % (name, score))    print(">>> 最终执行: %s" % ranked[0][0])  if __name__ == "__main__":    main()
目的:用面向对象方式实现一个 Utility AI 决策器,NPC 根据血量、距离、弹药等情境给攻击、逃跑、巡逻三个行为打分,选最高分执行。
  1. return raw_value / (raw_value + self.curve_k)收益递减响应曲线,把任意非负输入映射到 0~1
  2. total += cons.weight * cons.score(context[cons.name])核心加权累加,把每个因素的分数乘以权重再累加
  3. Consideration("敌人距离", 5.0, 0.6)定义一个考虑因素:名称、曲线常数 k、权重 w
  4. scored.sort(key=lambda item: item[1], reverse=True)按效用值降序排列,方便观察各行为排名
  5. ranked[0][0]取排名第一的行为名,即 NPC 最终执行的动作

常见误区

误区“攻击行为的分数是 80,巡逻只有 30,那攻击就是全世界最好的行为,任何时候都该选攻击。”

正解效用值只在当前时刻、当前情境下有意义。换一个情境——血量见底、弹药耗尽——攻击分数会瞬间暴跌。它是相对排名,不是绝对好坏。就像你饿的时候红烧肉分很高,可刚吃饱时它可能直接垫底。分数是情境的函数,脱离情境谈分数毫无意义。

为何常见:人的直觉容易把“分数”当成物体的固有属性,忽略它其实是随环境动态变化的输出值。

误区“把攻击的权重调高,NPC 就会一直攻击,所以权重越大行为越强。”

正解权重只放大某个因素对总分的影响,并不直接提升行为本身的执行概率。把“距离”权重调高,攻击分数会随距离剧烈波动,但敌人远在天边时攻击分照样很低。调权重是在调“敏感度”,不是调“强度”。

为何常见:把权重错误类比成“音量旋钮”,以为拧大就能一直响,实际上它是“放大镜”,放大的只是某一因素的波动幅度。

误区“距离和血量数值差不多大,直接拿原始数值加权就行,归一化多此一举。”

正解不同因素的量纲可能天差地别:距离是 0~100 格,血量是 0~1,弹药是 0~30。不归一化,量纲大的因素会淹没其他因素,“攻击”最终只由距离决定。归一化是让所有因素站在同一擂台上公平比试的前提,在原型阶段就是必做项。

为何常见:原型阶段的数值恰好处于同一量级,掩盖了量纲隐患,一旦换一组参数立刻失衡。

误区“Utility AI 是新东西,比老掉牙的状态机高级,所有 NPC 都用它准没错。”

正解工具没有高下之分,只有合不合适。状态机确定性强、易调试,适合判定逻辑简单的敌人;Utility AI 行为连续自然,但需要更细致的调参,行为结果也难以精确预测。选型要看游戏需求,而不是看技术是否新潮。

为何常见:技术选型时容易“唯新是从”,忽略了确定性和调试成本这些更贴近项目生命力的因素。

面试题

Q1. Utility AI 选择行为的核心依据是什么?

单选题 · easy

  • A.预设的顺序列表
  • B.行为的效用评分← 正确答案
  • C.随机的概率抽取
  • D.玩家输入的实时控制

答案:B。Utility AI 的核心是给每个行为计算效用分,选分数最高的执行。预设顺序是行为树或优先级逻辑的思路,随机抽取和直接映射玩家输入都与“自主决策”无关。

Q2. 一个考虑因素的分数通常被归一化到哪个区间?

单选题 · easy

  • A.0 到 1 之间← 正确答案
  • B.0 到 100 之间
  • C.-1 到 1 之间
  • D.1 到 10 之间

答案:A。归一化把不同量纲的原始值压到 0~1,让距离、血量、弹药能在同一个加权公式里公平相加。0~100 仍是线性缩放但缺乏“百分比”直觉,负区间会带来语义混乱,1~10 区间则没有归一化的意义。

Q3. 攻击行为由距离与弹药加权,距离权重 0.7 且距离分 0.8,弹药权重 0.3 且弹药分 0.5,攻击效用是多少?

单选题 · mid

  • A.0.59
  • B.0.65
  • C.0.71← 正确答案
  • D.0.79

答案:C。加权求和 = 0.7 × 0.8 + 0.3 × 0.5 = 0.56 + 0.15 = 0.71。0.65 是把两个分数直接平均的结果,0.79 是误把权重相加当成总分,0.59 则是计算错误。

Q4. 下列哪些信息可以作为 Utility AI 的考虑因素?

多选题 · mid

  • A.敌人与 NPC 的距离← 正确答案
  • B.NPC 当前的血量← 正确答案
  • C.NPC 技能的冷却状态← 正确答案
  • D.玩家在键盘上的按键

答案:ABC。考虑因素必须来自游戏世界状态,距离、血量、冷却都是典型输入。玩家按键属于玩家的操作输入层,不归 NPC 决策层处理,所以 D 不能选。

Q5. Utility AI 决策流程的最后一步是什么?

单选题 · easy

  • A.对分数做归一化
  • B.比较各行为分数
  • C.选出分数最高的行为← 正确答案
  • D.动态调整权重值

答案:C。决策循环是“列出候选、逐项算分、比较、执行最高分”。“选出最高分并执行”是最后一步;归一化和调权发生在算分阶段或设计阶段,不属于决策循环的收尾。

Q6. 与有限状态机相比,Utility AI 最突出的优势是什么?

单选题 · mid

  • A.内存开销明显更小
  • B.行为过渡更平滑自然← 正确答案
  • C.完全不需要调参数
  • D.调试过程更加简单

答案:B。多个因素连续变化让效用值平滑涨落,行为不会在临界点瞬间跳变,这是 Utility AI 最直观的优势。它并不省内存,反而对调参要求更高,调试也因结果不确定性而更复杂。

Q7. 某 NPC 的攻击效用 U = 0.6 × d/(d+5) + 0.4 × a/(a+2),其中 d=10 为敌人距离,a=4 为弹药余量,则 U 约为?

单选题 · extreme

  • A.0.67← 正确答案
  • B.0.71
  • C.0.58
  • D.0.63

答案:A。先算距离项 0.6 × 10/15 = 0.4,再算弹药项 0.4 × 4/6 ≈ 0.2667,两项相加约 0.67。0.71 是把曲线均值误当成 0.71 的干扰项,0.58 和 0.63 分别是漏算或算错曲线分母的结果。

Q8. 效用曲线 v(x) = x/(x+1) 的典型特征是什么?

单选题 · mid

  • A.随 x 线性增长没有上限
  • B.递增但边际收益递减← 正确答案
  • C.先增后减呈抛物线
  • D.存在周期性的振荡行为

答案:B。x/(x+1) 恒小于 1 且单调递增,导数 1/(x+1)² 随 x 递减,正是“越往后越难涨”的收益递减曲线,符合“已经够近再近也没差”的游戏直觉。其余三项均与单调有界递增的性质矛盾。

Q9. 给攻击行为引入冷却因素并使冷却分数只能取 0 或 1,主要目的是什么?

单选题 · hard

  • A.让分数看起来更整洁
  • B.控制攻击行为的触发频率← 正确答案
  • C.提高每帧的运算性能
  • D.避免与其他因素互相干扰

答案:B。冷却作为“硬开关”能在冷却期间把攻击分数压成 0,从根源上阻止高频触发,这是设计目的。整洁与性能只是副产品,“避免干扰”也不是这个设计要解决的首要问题。

Q10. 以下哪些游戏场景更适合用 Utility AI 而非状态机?

多选题 · hard

  • A.开放世界中 NPC 的日常行为模拟← 正确答案
  • B.需要多因素权衡的战场决策← 正确答案
  • C.只有开和关两种模式的开关门
  • D.希望行为平滑过渡的敌人 AI← 正确答案

答案:ABD。开放世界 NPC 需要同时处理时间、天气、声望等多因素,战场单位需要权衡血量与兵力,平滑过渡更是 Utility AI 的天然优势。只有开/关两种状态的开关门用状态机最合适,杀鸡不必用牛刀。