跳转至

NPC行为演进

概述

游戏NPC(Non-Player Character)的行为控制技术经历了从简单状态机到LLM驱动的革命性演进。本文梳理这一演进路径,比较各技术方案的优劣。

有限状态机 (FSM)

有限状态机是最早和最简单的NPC行为控制方法。

基本结构

\[\text{FSM} = (S, s_0, \Sigma, \delta, F)\]
  • \(S\): 状态集合(如 Idle, Patrol, Chase, Attack)
  • \(s_0\): 初始状态
  • \(\Sigma\): 输入事件集合(如 see_enemy, lose_sight, health_low)
  • \(\delta: S \times \Sigma \rightarrow S\): 状态转移函数
  • \(F\): 终止状态集合
stateDiagram-v2
    [*] --> Idle
    Idle --> Patrol: start_patrol
    Patrol --> Chase: see_enemy
    Patrol --> Idle: patrol_complete
    Chase --> Attack: in_range
    Chase --> Patrol: lose_sight
    Attack --> Chase: out_of_range
    Attack --> Flee: health_low
    Flee --> Idle: safe_distance

实现示例

class FSM_NPC:
    def __init__(self):
        self.state = "idle"
        self.health = 100

    def update(self, perception):
        if self.state == "idle":
            if perception.patrol_timer_expired:
                self.state = "patrol"

        elif self.state == "patrol":
            if perception.enemy_visible:
                self.state = "chase"
                self.target = perception.nearest_enemy
            elif perception.patrol_complete:
                self.state = "idle"

        elif self.state == "chase":
            if self.distance_to(self.target) < self.attack_range:
                self.state = "attack"
            elif not perception.enemy_visible:
                self.state = "patrol"

        elif self.state == "attack":
            if self.health < 20:
                self.state = "flee"
            elif self.distance_to(self.target) > self.attack_range:
                self.state = "chase"

FSM的局限性

问题 说明
状态爆炸 复杂行为需要大量状态,$
刚性转移 缺乏灵活性,行为模式固定
难以扩展 新增行为需修改大量转移逻辑
缺乏上下文 无法考虑历史和上下文信息

分层有限状态机(HFSM)通过状态嵌套部分缓解了状态爆炸问题,但根本局限仍在。

行为树 (Behavior Tree)

行为树是游戏AI的工业标准,被广泛用于AAA游戏(如Halo、Unreal Engine内置)。

核心节点类型

graph TD
    Root[Root<br/>→] --> Sel1[Selector<br/>?]

    Sel1 --> Seq1[Sequence<br/>→ Combat]
    Sel1 --> Seq2[Sequence<br/>→ Patrol]
    Sel1 --> Act1[Action<br/>Idle]

    Seq1 --> Cond1[Condition<br/>Enemy Visible?]
    Seq1 --> Sel2[Selector<br/>? Attack/Flee]

    Sel2 --> Seq3[Sequence<br/>→ Attack]
    Sel2 --> Act2[Action<br/>Flee]

    Seq3 --> Cond2[Condition<br/>Health > 20?]
    Seq3 --> Act3[Action<br/>Attack Enemy]

    Seq2 --> Act4[Action<br/>Move to Waypoint]
    Seq2 --> Cond3[Condition<br/>At Waypoint?]
    Seq2 --> Act5[Action<br/>Wait]

节点类型详解

节点类型 符号 行为 子节点失败时 子节点成功时
Sequence 依次执行所有子节点 立即返回失败 继续下一个
Selector ? 尝试直到一个成功 尝试下一个 立即返回成功
Decorator 修饰子节点行为 依装饰器而定 依装饰器而定
Condition 检查条件 - -
Action 执行动作 - -

Tick机制

行为树通过 tick 驱动,每帧从根节点开始遍历:

\[\text{tick}(node) \rightarrow \{\text{SUCCESS}, \text{FAILURE}, \text{RUNNING}\}\]
  • SUCCESS: 节点执行成功
  • FAILURE: 节点执行失败
  • RUNNING: 节点仍在执行中(如移动到目标点的过程中)
class BehaviorTree:
    def tick(self, node, context):
        if isinstance(node, Sequence):
            for child in node.children:
                result = self.tick(child, context)
                if result != Status.SUCCESS:
                    return result
            return Status.SUCCESS

        elif isinstance(node, Selector):
            for child in node.children:
                result = self.tick(child, context)
                if result != Status.FAILURE:
                    return result
            return Status.FAILURE

        elif isinstance(node, Decorator):
            child_result = self.tick(node.child, context)
            return node.modify(child_result)

        elif isinstance(node, Action):
            return node.execute(context)

        elif isinstance(node, Condition):
            return Status.SUCCESS if node.check(context) else Status.FAILURE

常用Decorator

  • Inverter: 反转子节点结果
  • Repeater: 重复执行子节点N次
  • Timeout: 超时返回失败
  • Cooldown: 冷却期内不执行
  • ForceSuccess: 始终返回成功

目标导向行动规划 (GOAP)

GOAP(Goal-Oriented Action Planning)受STRIPS规划器启发,让NPC自主规划达到目标的行动序列。

核心概念

\[\text{GOAP} = (\mathcal{S}, \mathcal{A}, \mathcal{G})\]
  • \(\mathcal{S}\): 世界状态(键值对集合)
  • \(\mathcal{A}\): 行动集合,每个行动包含前置条件和效果
  • \(\mathcal{G}\): 目标集合,每个目标包含期望的世界状态
class GOAPAction:
    def __init__(self, name, cost, preconditions, effects):
        self.name = name
        self.cost = cost                    # 行动代价
        self.preconditions = preconditions  # 前置条件 {key: value}
        self.effects = effects              # 效果 {key: value}

# 定义行动
actions = [
    GOAPAction("get_axe", cost=2, 
               preconditions={"has_axe": False}, 
               effects={"has_axe": True}),
    GOAPAction("chop_tree", cost=4, 
               preconditions={"has_axe": True}, 
               effects={"has_wood": True}),
    GOAPAction("build_fire", cost=3, 
               preconditions={"has_wood": True}, 
               effects={"is_warm": True}),
    GOAPAction("find_shelter", cost=6, 
               preconditions={}, 
               effects={"is_warm": True}),
]

# 目标
goal = {"is_warm": True}
# A*规划器会找到: get_axe -> chop_tree -> build_fire (cost=9)
# 而非: find_shelter (cost=6) — 取决于当前状态

A*规划

GOAP使用反向A*搜索从目标状态回溯到当前状态:

\[f(n) = g(n) + h(n)\]
  • \(g(n)\): 从起始到当前节点的实际代价
  • \(h(n)\): 启发式估计(通常为未满足条件数量)
graph RL
    G["目标: is_warm=True"] --> A3["build_fire<br/>cost=3"]
    A3 --> A2["chop_tree<br/>cost=4"]
    A2 --> A1["get_axe<br/>cost=2"]
    A1 --> S["当前状态<br/>has_axe=False"]

    G --> A4["find_shelter<br/>cost=6"]
    A4 --> S2["当前状态<br/>(无前置条件)"]

GOAP的优势与局限

优势:

  • 行动解耦:新增行动无需修改其他逻辑
  • 自动规划:NPC自主发现行动序列
  • 动态适应:环境变化时自动重新规划

局限:

  • 状态空间需预定义
  • 启发式函数设计影响性能
  • 不擅处理连续动作和模糊目标

效用AI (Utility AI)

效用AI通过评分函数为每个可能的行动计算效用值,选择效用最高的行动:

\[U(a) = \sum_{i=1}^{n} w_i \cdot f_i(a)\]

其中: - \(a\) 是候选行动 - \(f_i(a)\) 是第 \(i\) 个评估因子对行动 \(a\) 的评分 - \(w_i\) 是对应权重

评分曲线类型

常用的评分函数 \(f_i(a)\) 形式:

  • 线性: \(f(x) = mx + b\)
  • 二次: \(f(x) = ax^2 + bx + c\)
  • Logistic: \(f(x) = \frac{1}{1 + e^{-k(x - x_0)}}\)
  • 指数衰减: \(f(x) = e^{-\lambda x}\)

示例

class UtilityAI:
    def select_action(self, npc, context):
        actions = {
            "eat": self.score_eat(npc),
            "sleep": self.score_sleep(npc),
            "fight": self.score_fight(npc, context),
            "flee": self.score_flee(npc, context),
            "socialize": self.score_socialize(npc, context),
        }
        return max(actions, key=actions.get)

    def score_eat(self, npc):
        hunger = npc.hunger / 100.0  # 归一化到[0,1]
        # Logistic曲线:饥饿度高时急剧上升
        return 1.0 / (1.0 + math.exp(-10 * (hunger - 0.6)))

    def score_fight(self, npc, context):
        if not context.enemy_visible:
            return 0.0
        health_factor = npc.health / 100.0
        weapon_factor = 1.0 if npc.has_weapon else 0.3
        enemy_weakness = 1.0 - context.enemy_health / 100.0
        return 0.4 * health_factor + 0.3 * weapon_factor + 0.3 * enemy_weakness

    def score_flee(self, npc, context):
        if not context.enemy_visible:
            return 0.0
        danger = 1.0 - npc.health / 100.0
        return danger * 0.8  # 血量低时逃跑倾向高

LLM驱动的NPC

LLM的出现彻底改变了NPC设计范式,从预定义行为转向生成式行为

核心优势

  1. 自由对话: 不再局限于预设对话树
  2. 动态目标: 根据上下文生成合理的目标
  3. 个性化: 通过人设prompt实现独特人格
  4. 涌现行为: 多NPC交互产生意料之外的行为

架构

class LLM_NPC:
    def __init__(self, name, personality, backstory):
        self.name = name
        self.personality = personality
        self.backstory = backstory
        self.memory_stream = MemoryStream()
        self.current_plan = []

    def generate_response(self, player_input, context):
        """生成对话回复"""
        relevant_memories = self.memory_stream.retrieve(player_input)

        prompt = f"""You are {self.name}. {self.personality}

Backstory: {self.backstory}

Relevant memories:
{format_memories(relevant_memories)}

Current situation: {context}

{player_input}

Respond in character:"""

        response = call_llm(prompt)
        self.memory_stream.add(f"Player said: {player_input}")
        self.memory_stream.add(f"I responded: {response}")
        return response

    def decide_action(self, perception):
        """决定下一步行动"""
        memories = self.memory_stream.retrieve(str(perception))

        prompt = f"""You are {self.name}. Given your personality, 
memories, and current situation, what should you do next?

Available actions: {perception.available_actions}
Current observation: {perception.description}
Relevant memories: {format_memories(memories)}

Choose one action and explain briefly:"""

        return call_llm(prompt)

技术对比总表

特性 FSM 行为树 GOAP 效用AI LLM NPC
可预测性 极高
灵活性 极高
可扩展性
对话能力 原生
开发成本 低(调用)
运行成本 极低 高(API)
调试难度
涌现行为 有限 有限 丰富
适用场景 简单NPC AAA游戏 策略游戏 模拟游戏 开放世界
代表案例 早期游戏 Halo, UE F.E.A.R Sims Smallville

演进趋势

graph LR
    A[FSM<br/>1990s] --> B[行为树<br/>2000s]
    B --> C[GOAP<br/>2004+]
    B --> D[效用AI<br/>2010s]
    C --> E[混合架构<br/>2015+]
    D --> E
    E --> F[LLM驱动<br/>2023+]

    style F fill:#f9f,stroke:#333,stroke-width:2px

混合架构趋势

现代NPC系统越来越多地采用混合架构

  • LLM + 行为树: LLM处理对话和高层决策,行为树执行具体动作
  • LLM + GOAP: LLM生成目标,GOAP规划行动序列
  • LLM + 效用AI: LLM评估情境,效用AI选择行动
  • 分层架构: 上层LLM(战略),中层GOAP/效用AI(战术),底层FSM/行为树(执行)
\[\text{Decision} = \text{LLM}_{\text{strategic}}(\text{context}) \rightarrow \text{GOAP}_{\text{tactical}}(\text{goal}) \rightarrow \text{BT}_{\text{execution}}(\text{plan})\]

总结

NPC行为控制从确定性的FSM演进到生成式的LLM,每一代技术都解决了前一代的核心局限,同时引入新的挑战。未来的方向是混合架构——利用LLM的灵活性和传统方法的可控性,构建既智能又可靠的NPC系统。


评论 #