游戏 AI:行为树、寻路与群集行为

深入游戏 AI 系统设计:行为树与有限状态机(Selector/Sequence/黑板)、A* 寻路与 NavMesh、感知系统(视野/听觉/记忆)、群集行为(分离/对齐/聚合),含 Unity NavMesh、Godot Navigation 与自研行为树代码实战。

“敌人"是否聪明,直接决定一款游戏的体验上限。但游戏 AI 的目标从来不是"真正的智能”,而是在性能预算内表现合理。本文聚焦游戏 AI 的四大支柱:行为决策(行为树/FSM)、寻路(A/NavMesh)*、感知系统、群集行为,用 Unity、Godot 与自研实现三重视角,教你搭出一套可维护、可调试、性能可控的 AI 系统。

相关:AI 的行为决策最终驱动物理与动画,可参考 游戏物理与碰撞 的移动模型;网络游戏里 AI 由服务端托管时的确定性要求见 游戏网络同步:延迟补偿与回滚。

1. 行为决策:FSM 与行为树

1.1 有限状态机(FSM)及其局限

FSM 是最直观的 AI 框架:敌人处于 巡逻 / 追击 / 攻击 / 死亡 等状态,状态间按条件转移。

// FSM 经典实现(状态模式)
enum EnemyState { Patrol, Chase, Attack, Dead }

class EnemyAI {
    EnemyState state = EnemyState.Patrol;

    void Update() {
        switch (state) {
            case EnemyState.Patrol:
                PatrolUpdate();
                if (CanSeePlayer()) state = EnemyState.Chase;
                break;
            case EnemyState.Chase:
                ChaseUpdate();
                if (ReachedPlayer()) state = EnemyState.Attack;
                break;
            case EnemyState.Attack:
                AttackUpdate();
                break;
        }
    }
}

FSM 的痛点随着 AI 复杂度增长而爆发:

  1. 状态爆炸:每个新行为都要新增状态 + 转移,交叉条件让代码难以维护。
  2. 无法复用:追击 逻辑写死在状态里,别的 AI 想用要复制粘贴。
  3. 调试困难:复杂 FSM 的转移图肉眼难以推演。

1.2 行为树:决策的树形抽象

行为树用树节点组织决策,核心是控制流节点(组合器)与叶子节点(行为/条件):

[Selector: 决定做什么]  ← 有子节点成功即成功(优先级)
 ├─ [Sequence: 追击并攻击]  ← 全成功才成功
 │   ├─ 能看见玩家?(Condition)
 │   ├─ 移动到玩家(行为)
 │   └─ 攻击(行为)
 ├─ [Sequence: 巡逻]
 │   ├─ 到达巡逻点?(Condition)
 │   └─ 走向巡逻点(行为)
 └─ [Wait(行为)]
节点类型行为类比
Selector(选择)从左到右试子节点,任一成功即返回成功或 逻辑,优先级选择
Sequence(序列)从左到右执行,任一失败即返回失败与 逻辑,任务顺序
Condition(条件)只返回成功/失败,不做事布尔判断
Action(行为)执行并返回 Running/Success/Fail具体动作
Decorator(装饰器)包装子节点,修改其行为直到成功、限时、反转

行为树的关键优势:返回 Running(进行中) 状态,让"移动到玩家"这种长时行为可以跨帧执行,配合**黑板(Blackboard)**共享数据,天然可复用、可热插拔。

1.3 自研行为树实现

// 简化版行为树骨架(C#)
public enum Status { Success, Failure, Running }

public abstract class BTNode {
    public abstract Status Tick(Blackboard bb, float dt);
}

public class Selector : BTNode {
    private BTNode[] children;
    public override Status Tick(Blackboard bb, float dt) {
        foreach (var c in children) {
            var s = c.Tick(bb, dt);
            if (s == Status.Success) return Status.Success;
            if (s == Status.Running) return Status.Running; // 保持进行
        }
        return Status.Failure;
    }
}

public class MoveToPlayer : BTNode {
    public override Status Tick(Blackboard bb, float dt) {
        var enemy = bb.Get<Enemy>("enemy");
        if (!enemy.ReachedPlayer()) { enemy.MoveTowardPlayer(dt); return Status.Running; }
        return Status.Success;
    }
}

// 组合:追击并攻击
var attackBrain = new Selector(
    new Sequence(
        new CanSeePlayer(),
        new MoveToPlayer(),
        new Attack()
    ),
    new Patrol()
);

1.4 行为树 vs FSM vs GOAP

框架学习成本可维护性动态性适用
FSM低低(状态多后崩溃)低简单敌人、UI 状态
行为树中高中绝大多数游戏
GOAP(目标规划)高中高开放世界、策略 AI

实践建议:行为树作为主干;状态机用于"单节点内部"的动画状态(如攻击动作的 前摇/命中/后摇);复杂多目标 AI 才考虑 GOAP。Unity 用 Behaviour Tree 资产(如 Behavior Designer)、Godot 用 BehaviorTree 插件或自研。

2. 寻路:A* 与 NavMesh

2.1 A* 算法核心

A* 在网格上找最短路径,是寻路的基础算法。核心是估值函数 f(n) = g(n) + h(n):

  • g(n):从起点到 n 的实际代价
  • h(n):从 n 到终点的启发式估计(通常欧氏距离/曼哈顿距离)
  • f(n):经过 n 的预估总代价,优先扩展 f 最小的节点
# A* 伪代码
def a_star(start, goal, graph):
    open_set = {start}                 # 待扩展
    came_from = {}
    g = {start: 0}
    f = {start: heuristic(start, goal)}

    while open_set:
        current = min(open_set, key=lambda n: f[n])
        if current == goal:
            return reconstruct_path(came_from, current)
        open_set.remove(current)
        for neighbor in graph.neighbors(current):
            tentative = g[current] + graph.cost(current, neighbor)
            if tentative < g.get(neighbor, inf):
                came_from[neighbor] = current
                g[neighbor] = tentative
                f[neighbor] = tentative + heuristic(neighbor, goal)
                open_set.add(neighbor)
    return None                        # 无路可达

A 的常见改进:*

  • 二叉堆/优先队列:open_set 用堆,O(log n) 取最小。
  • JPS(跳点搜索):网格对称性剪枝,路径规划快 10 倍(仅限均匀网格)。
  • 分层寻路(Hierarchical):先规划区块级粗路径,再在区块内细寻路。

2.2 NavMesh:从网格到导航网格

NavMesh 把可走区域烘焙成凸多边形集合,相比网格寻路有三大优势:

维度网格(Grid)NavMesh
节点数像素级密集多边形级稀疏
路径平滑需要后处理平滑天然多边直行
动态障碍简单(改网格)需要局部避障
存储大小

Unity 的 NavMesh 工作流:

1. 标记场景地面/障碍为 Navigation Static
2. Bake:设置 Agent 半径、高度、爬坡角度、跳跃高度
3. 运行时:NavMeshAgent.SetDestination() → 引擎处理寻路+避障
// Unity NavMeshAgent 用法
using UnityEngine.AI;

public class Enemy : MonoBehaviour {
    NavMeshAgent agent;
    Transform player;

    void Start() { agent = GetComponent<NavMeshAgent>(); }
    void Update() {
        agent.SetDestination(player.position);
        if (agent.remainingDistance < 0.5f) Attack();
    }
}
# Godot 4 NavigationServer
var map = get_world_3d().navigation_map
var path = NavigationServer3D.map_get_path(
    map, start_pos, end_pos, use_navigation_edge_connection)

2.3 动态避障(RVO / Local Avoidance)

NavMesh 管宏观路径,动态物体之间的微观避让靠 RVO(Reciprocal Velocity Obstacles) 或本地避障:每个 Agent 预测邻居的轨迹,调整自己的速度避免碰撞。Unity 的 NavMeshAgent 内置本地避障;Godot 可用 NavigationObstacle + RVO 节点。

常见坑:大量 Agent(>200)同时寻路会造成 CPU 峰值。解法:分帧寻路(每帧只给 N 个 Agent 重算)、路径缓存(目标不变不重算)、LOD AI(远处敌人降级为简单移动)。

3. 感知系统

3.1 视野、听觉与记忆

游戏 AI 不"看见"一切,而是通过感知系统获取世界信息。经典三通道:

感知实现关键参数
视觉视野角(FOV)+ 距离 + 遮挡测试半角、距离、遮挡 Layer
听觉声源传播 + 距离衰减音量、可闻距离
记忆最近感知到的位置、事件记忆时长、遗忘曲线
// 视野检测(视线 + 距离 + 遮挡)
bool CanSee(Transform self, Transform target, float fov, float range, LayerMask obstacles) {
    var dir = target.position - self.position;
    if (dir.magnitude > range) return false;              // 超距离
    if (Vector3.Angle(self.forward, dir) > fov / 2) return false; // 超出视野角
    return !Physics.Raycast(self.position, dir, dir.magnitude, obstacles); // 被遮挡
}

3.2 感知的工程化处理

  • 感知 ≠ 每帧全量测试:敌人只在**感知周期(如 0.5s 一次)**采样,降低 CPU 峰值。
  • 感知结果写入黑板:seenPlayerPosition 存入 Blackboard,行为树读取,实现"记忆 + 追查最后已知位置"。
  • 去重与优先级:多个感知源(视觉+听觉)合并为事件队列,行为树按优先级消费。

4. 群集行为:Flocking

4.1 三大基础规则

Craig Reynolds 的 Boids 模型用三条局部规则产生宏观群体行为:

规则行为公式倾向
分离(Separation)与邻居保持距离排斥向量
对齐(Alignment)与邻居同向同速平均速度方向
聚合(Cohesion)向群体中心靠拢指向中心向量
// Flocking 核心(伪代码)
Vector3 FlockingForce(Boid b, List<Boid> neighbors) {
    Vector3 sep = 0, ali = 0, coh = 0;
    int n = neighbors.Count;
    foreach (var nb in neighbors) {
        sep += (b.pos - nb.pos).normalized / (b.pos - nb.pos).magnitude; // 分离
        ali += nb.velocity;
        coh += nb.pos;
    }
    ali /= n;  coh = coh / n - b.pos;   // 对齐与聚合
    return sep * sepWeight + ali * aliWeight + coh * cohWeight;
}

4.2 性能:空间划分 + 每帧采样

群集行为最容易 O(n²)。优化:

  1. 空间哈希/网格:只取周围邻域邻居,而非全部。
  2. 每帧只采样部分:Boid 每 0.2-0.5s 重算一次力,中间用插值。
  3. GPU 加速:大群(上万)用 Compute Shader 并行计算。

群集 + 寻路结合:宏观用 NavMesh 引导整个群(leader 寻路),微观用 Flocking 让个体避让、保持队形——这是 RTS 单位移动的标准做法。

5. 最佳实践与总结

游戏 AI 系统设计检查单:

  1. 先做感知再决策:行为树只读"感知结论"(黑板),不要直接在行为树里做 Raycast。
  2. 分层决策:策略层(行为树)→ 动作层(状态机/动画)→ 运动层(寻路+避障),各层解耦。
  3. 性能预算化:给 AI 分配 ms/帧 预算,超预算时按距离/可见性降级(LOD AI)。
  4. 确定性考量:回滚/帧同步游戏里,AI 逻辑必须放在固定步长且结果确定(见 游戏网络同步:延迟补偿与回滚)。
  5. 可调试优先:可视化感知范围、行为树当前节点、寻路路径——不可见的 AI 无法调好。

调试工具:

  • Unity:AI Debugger、NavMesh Debugger、Gizmos 画 FOV 与路径。
  • Godot:NavigationRegion3D debug_enabled、行为树插件内置可视化。
  • 自研:日志树(每帧打印当前树节点栈)是最廉价但最有效的调试手段。

游戏 AI 的秘诀不是复杂算法,而是清晰的决策分层 + 受控的感知 + 预算化的性能。行为树解决"怎么决策",A*/NavMesh 解决"怎么走",感知解决"知道什么",Flocking 解决"怎么成群"——四者组合,就是绝大多数游戏需要的全部智能。

相关阅读:游戏引擎架构:ECS 与资源管理 讲解 AI 组件与系统在 ECS 中的组织;游戏物理与碰撞 讲解 AI 移动落地的物理层;游戏渲染管线基础 讲解 AI 调试可视化如何合批渲染。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「game」更多文章

  1. 游戏性能剖析与优化:从 Profiler 到平台适配
  2. 游戏网络同步:延迟补偿、客户端预测与回滚
  3. 游戏物理与碰撞:刚体、碰撞检测与响应