“敌人"是否聪明,直接决定一款游戏的体验上限。但游戏 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 复杂度增长而爆发:
- 状态爆炸:每个新行为都要新增状态 + 转移,交叉条件让代码难以维护。
- 无法复用:
追击逻辑写死在状态里,别的 AI 想用要复制粘贴。 - 调试困难:复杂 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²)。优化:
- 空间哈希/网格:只取周围邻域邻居,而非全部。
- 每帧只采样部分:Boid 每 0.2-0.5s 重算一次力,中间用插值。
- GPU 加速:大群(上万)用 Compute Shader 并行计算。
群集 + 寻路结合:宏观用 NavMesh 引导整个群(leader 寻路),微观用 Flocking 让个体避让、保持队形——这是 RTS 单位移动的标准做法。
5. 最佳实践与总结
游戏 AI 系统设计检查单:
- 先做感知再决策:行为树只读"感知结论"(黑板),不要直接在行为树里做 Raycast。
- 分层决策:策略层(行为树)→ 动作层(状态机/动画)→ 运动层(寻路+避障),各层解耦。
- 性能预算化:给 AI 分配
ms/帧预算,超预算时按距离/可见性降级(LOD AI)。 - 确定性考量:回滚/帧同步游戏里,AI 逻辑必须放在固定步长且结果确定(见 游戏网络同步:延迟补偿与回滚)。
- 可调试优先:可视化感知范围、行为树当前节点、寻路路径——不可见的 AI 无法调好。
调试工具:
- Unity:
AI Debugger、NavMesh Debugger、Gizmos 画 FOV 与路径。 - Godot:
NavigationRegion3D debug_enabled、行为树插件内置可视化。 - 自研:日志树(每帧打印当前树节点栈)是最廉价但最有效的调试手段。
游戏 AI 的秘诀不是复杂算法,而是清晰的决策分层 + 受控的感知 + 预算化的性能。行为树解决"怎么决策",A*/NavMesh 解决"怎么走",感知解决"知道什么",Flocking 解决"怎么成群"——四者组合,就是绝大多数游戏需要的全部智能。
相关阅读:游戏引擎架构:ECS 与资源管理 讲解 AI 组件与系统在 ECS 中的组织;游戏物理与碰撞 讲解 AI 移动落地的物理层;游戏渲染管线基础 讲解 AI 调试可视化如何合批渲染。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。