「敌人会追我、会绕开障碍、会包抄」——这些看似智能的行为,底层是两套系统的配合:感知(AI 怎么知道玩家在哪)与寻路(AI 怎么走到玩家那)。很多开发者上来就套 A*,结果小地图能跑、大地图卡死,或敌人「看得见墙也绕不过去」。本文剥开 AI 感知与寻路外壳,聚焦四个核心模块:感知系统(AI 的「眼睛和耳朵」)、A 寻路原理与优化*、NavMesh 导航网格与动态障碍、避障与行为衔接,并用 Unity NavMesh、Godot Navigation 与自研 A* 三重视角对照。
建议先读 游戏 AI:行为树与寻路 建立 AI 决策层视角,本文深入其「感知 + 移动」一层。
1. 感知系统:AI 的「眼睛和耳朵」
1.1 感知模型
AI 感知 = 视野 + 听觉 + 记忆(三层)
├── 视野:扇形检测(角度 + 距离 + 遮挡)
├── 听觉:球形检测(声音源 + 音量衰减 + 距离)
└── 记忆:感知结果存在「黑盒」里(目标位置、上次见到的时间)
flowchart LR
A["玩家位置"] --> B["视野检测(扇形+遮挡)"]
A --> C["听觉检测(球形+衰减)"]
B --> D["感知记忆"]
C --> D
D --> E["行为决策"]
1.2 视野检测
// 视野:角度内 + 距离内 + 无遮挡
bool CanSee(Vector3 playerPos, Vector3 myPos, Vector3 forward, float fov, float range) {
Vector3 toPlayer = playerPos - myPos;
if (toPlayer.magnitude > range) return false;
if (Vector3.Angle(forward, toPlayer) > fov / 2f) return false;
if (Physics.Raycast(myPos, toPlayer, out hit, range)) {
if (hit.collider.tag != "Player") return false; // 被墙挡了
}
return true;
}
1.3 感知的代价控制
感知检测不能每帧对所有 AI 全做(会爆):
├── 频率控制:每 0.2~0.5s 检测一次(不是每帧)
├── 分级检测:近的每帧、远的低频
├── 遮挡用射线:预算控制在几条射线内
└── 静态遮挡:可预计算遮挡关系,不用实时射线
记忆:感知是「视觉扇形 + 听觉球形 + 记忆黑盒」,关键在频率控制——别让 AI 的「眼睛」比玩家的 GPU 还贵。
2. A* 寻路原理与优化
2.1 A* 的核心
A* = 把地图抽象成图,找从起点到终点的最短路径
├── 节点(Node):格子/顶点
├── 代价(g):从起点到这的开销
├── 启发式(h):到这终点的估计开销
└── f = g + h,每次扩展 f 最小的节点
A* 伪码:
openList = [start]; cameFrom = {}; gScore[start] = 0
while openList 非空:
当前 = openList 中 f 最小的
if 当前 == 终点: 回溯路径
for 邻居 in 当前的邻居:
g2 = gScore[当前] + 移动代价
if g2 < gScore[邻居]:
更新 gScore、记录 cameFrom、加入 openList
2.2 启发式与优化
| 优化 | 做法 | 效果 |
|---|---|---|
| 良好启发式 | 曼哈顿/欧氏距离 | 少扩展节点 |
| 二叉堆 | openList 用优先队列 | 取最小 O(logn) |
| 跳点搜索 | JPS(格子类地图) | 大幅减少节点 |
| 分层寻路 | 上层粗路径 + 下层细走 | 大地图 |
启发式是否准决定 A* 快慢:
├── h 低估 → 扩展多但路径准
├── h 高估(可接受)→ 扩展少但可能非最优
└── 网格地图常用曼哈顿距离(四方向)/ 欧氏(任意方向)
记忆:A 的引擎是「f = g + h」*,工程优化在「堆 + 启发式 + 分层」,别小看 h 的估算——它决定你是扩展 100 个节点还是 10000 个。
3. NavMesh 导航网格与动态障碍
3.1 NavMesh 是什么
A* 在「格子图」上跑;NavMesh 把可行走区域变成「多边形网格」
├── 优点:贴合真实地形(斜坡、窄缝、台阶)
├── 生成:烘焙(离线)或运行时生成
└── 寻路:在网格上做 A*,再平滑路径
NavMesh 生成流程(离线烘焙):
场景几何 → 标记可行走区域 → 生成多边形网格 → 保存 .asset
运行时寻路:
Agent 请求路径 → NavMesh 上 A* → 得到路径点 → Agent 沿路径移动
3.2 动态障碍处理
障碍不是固定的(门开合、柱子倒塌、玩家放置物):
├── 动态障碍:运行时把障碍区域「切出」NavMesh
├── 避障:Agent 之间互相避让(RVO/ORCA)
└── 局部重寻:遇到新障碍 → 只重算局部路径
| 维度 | Unity NavMesh | Godot Navigation |
|---|---|---|
| 生成 | 离线烘焙 / NavMeshSurface | 运行时 NavigationMesh |
| Agent | NavMeshAgent | NavigationAgent3D/2D |
| 动态障碍 | NavMeshObstacle | 动态烘焙 |
| 避障 | 内置 | 需配置 |
记忆:NavMesh 是「给 A 一张贴地形的图」*。动态场景的关键是「障碍切图 + Agent 避让 + 局部重寻」,三者配合才能应付会变的世界。
4. 避障与寻路衔接
4.1 寻路 vs 避障
寻路(全局):从 A 到 B 的大方向(NavMesh/A*,低频)
避障(局部):眼前这步怎么走(RVO/ORCA,高频)
├── 全局寻路:每 0.5s 或到达节点时重算
├── 局部避障:每帧调整方向避免撞人/撞墙
└── 融合:沿路径点走 + 每帧避障修正
Agent 移动每帧:
1. 全局路径:当前目标节点(低频更新)
2. 局部避障:修正方向(高频)
3. 实际移动:朝修正后的方向走
4.2 与行为树衔接
寻路结果 → 行为树的「移动」动作:
├── 行为树决策「追玩家」→ 寻路到玩家位置 → 沿路径移动
├── 感知发现新目标 → 重新决策 → 重新寻路
└── 寻路失败(无路可走)→ 行为树切「放弃/巡逻」
记忆:寻路解决「往哪走」,避障解决「怎么走」,行为树决定「要不要走」。三者是 AI 移动的三层,接错了(用避障做全局)就会「原地打转」。
5. 寻路性能优化
大地图 + 多敌人的寻路性能:
├── 寻路频率:不是每帧,是「到节点才重算」
├── 路径缓存:同一目的地多人共享缓存
├── 分层:上层导航(区域级)→ 下层细走
└── 异步:寻路放后台线程,别卡主线程
| 优化 | 场景 | 收益 |
|---|---|---|
| 低频重寻 | 移动中的敌人 | 大 |
| 路径缓存 | 群组同目标 | 大 |
| 分层寻路 | 大型开放世界 | 大 |
| 异步寻路 | 大量敌人 | 防卡顿 |
心法:寻路性能的瓶颈在「次数」不在「单次」——把「每帧重算」改成「到点重算 + 缓存 + 分层」,百个敌人也能跑得动。
6. 最佳实践与总结
AI 感知与寻路决策清单:
- 感知控频:视野/听觉检测 0.2~0.5s 一次,别每帧全做。
- A 打好底*:堆 + 好启发式 + 分层,先快后准。
- NavMesh 贴地形:大地图别用格子,烘焙 NavMesh。
- 动态障碍三件套:切图 + 避让 + 局部重寻。
- 全局与局部分开:寻路定大方向、避障调小步,别混。
自研 AI 移动最小骨架推荐阅读顺序:感知检测 → A* 寻路 → 路径平滑 → 沿路径移动 → 局部避障。每完成一层,用一个「敌人追玩家绕障碍」的 demo 验证感知→寻路→移动的衔接。
AI 移动没有银弹:Unity NavMesh 现成、Godot Navigation 轻量、自研 A* 可控。但感知控频、全局/局部分离、路径缓存这三件事不分引擎必须做对——它们决定 100 个敌人时游戏卡不卡。
相关阅读:游戏 AI:行为树与寻路 讲解决策层;游戏性能剖析与优化 讲解寻路性能预算。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。