1. Unity寻路系统基础与项目概述
在游戏开发中,AI角色的自主移动是核心需求之一。Unity内置的Navigation系统提供了一套完整的解决方案,让开发者无需从头实现复杂的寻路算法。这个系统基于业界成熟的NavMesh(导航网格)技术,通过预计算场景中的可行走区域,为游戏中的AI角色提供高效的路径规划能力。
我最近在开发一个简单的生存射击游戏demo,需要实现敌人自动追踪玩家的功能。经过对比几种实现方案,最终选择了Unity原生Navigation系统,主要基于以下考虑:
- 性能优化:NavMesh烘焙后,寻路计算开销远低于实时物理检测
- 功能完整:自动处理障碍物规避、坡度检测、跳跃点等复杂地形
- 开发效率:可视化编辑工具链完善,API简单易用
这个案例将展示从零开始搭建寻路AI的完整流程,包含以下核心环节:
- 场景地形烘焙与导航网格生成
- NavMeshAgent组件配置与参数调优
- 目标追踪逻辑的代码实现
- 实际开发中的性能优化技巧
提示:虽然本文以"敌人追踪玩家"为例,但相同技术可应用于NPC巡逻、RTS单位移动等任何需要自动寻路的场景。
2. 场景准备与导航网格烘焙
2.1 场景基础设置
首先需要准备一个包含地形和障碍物的测试场景。关键注意事项:
- 地面必须使用带有碰撞体的Mesh(如Plane或Terrain)
- 障碍物需要添加Collider组件并设置为Static
- 复杂地形建议先简化碰撞体以提高烘焙效率
csharp复制// 示例:通过代码设置物体为Navigation Static
GameObject obstacle = GameObject.Find("Wall");
obstacle.isStatic = true;
2.2 Navigation面板详解
通过Window > AI > Navigation打开设置面板,包含四个关键选项卡:
-
Bake:核心烘焙参数
- Agent Radius:0.5(角色半径)
- Agent Height:2.0(角色高度)
- Max Slope:45°(最大可爬坡度)
- Step Height:0.3(可跨越台阶高度)
- Drop Height:1.0(允许下落高度)
-
Areas:区域成本设置
- 可定义不同地形类型(如草地、沼泽)
- 设置不同移动成本影响路径选择
-
Object:物体级烘焙设置
- 可覆盖全局设置针对特定物体
- 设置是否参与烘焙或作为障碍物
-
Agents:多代理类型配置
- 支持定义不同体型角色的参数预设
2.3 烘焙流程与常见问题
点击Bake按钮生成导航网格后,场景中会出现蓝色半透明区域表示可行走表面。常见问题处理:
-
烘焙后出现空洞:
- 检查地面碰撞体是否完整
- 调整Agent Height/Radius参数
- 确认所有Static物体正确标记
-
角色卡在细小缝隙:
- 适当增大Agent Radius
- 在Bake设置中减小Cell Size
-
斜坡无法行走:
- 检查Max Slope角度设置
- 确认斜坡碰撞体法线方向正确
实测发现:对于复杂室内场景,建议将Cell Size设为0.1以下,但会显著增加烘焙时间和内存占用。
3. NavMeshAgent组件配置
3.1 组件参数解析
为敌人角色添加NavMeshAgent组件,关键参数:
| 参数 | 推荐值 | 作用 |
|---|---|---|
| Speed | 3.5 | 移动速度(m/s) |
| Angular Speed | 120 | 转向速度(°/s) |
| Acceleration | 8 | 加速度(m/s²) |
| Stopping Distance | 1.5 | 停止距离 |
| Auto Braking | true | 接近目标时减速 |
| Obstacle Avoidance | Medium | 避障质量 |
csharp复制// 获取组件引用
NavMeshAgent agent = GetComponent<NavMeshAgent>();
3.2 动态避障实现
高质量避障需要额外配置:
- 在场景中添加NavMeshObstacle组件
- 设置Carve选项为动态切割导航网格
- 调整Move Threshold避免频繁重新计算
csharp复制// 动态障碍物示例代码
NavMeshObstacle obstacle = wall.AddComponent<NavMeshObstacle>();
obstacle.carveOnlyStationary = false;
obstacle.carving = true;
3.3 多层级地形处理
对于多层建筑场景的特殊处理:
- 在楼梯位置设置OffMeshLink
- 配置Jump和Drop类型的连接点
- 在Agent设置中启用Auto Traverse OffMeshLink
csharp复制// 手动添加OffMeshLink示例
OffMeshLink link = gameObject.AddComponent<OffMeshLink>();
link.startTransform = stairBottom;
link.endTransform = stairTop;
link.biDirectional = true;
4. 寻路逻辑代码实现
4.1 基础追踪脚本
创建EnemyAI.cs脚本实现核心功能:
csharp复制using UnityEngine;
using UnityEngine.AI;
public class EnemyAI : MonoBehaviour {
[SerializeField] Transform target;
[SerializeField] float updateInterval = 0.5f;
private NavMeshAgent agent;
private float lastUpdateTime;
void Start() {
agent = GetComponent<NavMeshAgent>();
lastUpdateTime = -updateInterval;
}
void Update() {
if (Time.time - lastUpdateTime >= updateInterval) {
agent.SetDestination(target.position);
lastUpdateTime = Time.time;
}
}
}
4.2 高级路径优化
改进版包含以下增强功能:
- 视线检测优化:优先使用直线移动
- 路径有效性检查:避免无效目标点
- 动态更新频率:根据距离调整
csharp复制bool HasDirectPathToTarget() {
RaycastHit hit;
Vector3 direction = target.position - transform.position;
if (!Physics.Raycast(transform.position, direction, out hit, direction.magnitude)) {
return true;
}
return hit.transform == target;
}
void UpdatePath() {
if (NavMesh.SamplePosition(target.position, out NavMeshHit hit, 1.0f, NavMesh.AllAreas)) {
agent.SetDestination(hit.position);
}
}
4.3 状态机集成
实际游戏通常需要更复杂的AI行为:
csharp复制enum AIState { Patrol, Chase, Attack }
AIState currentState = AIState.Patrol;
void Update() {
float distanceToTarget = Vector3.Distance(transform.position, target.position);
switch (currentState) {
case AIState.Patrol:
if (distanceToTarget < 10f) currentState = AIState.Chase;
break;
case AIState.Chase:
agent.SetDestination(target.position);
if (distanceToTarget < 2f) currentState = AIState.Attack;
if (distanceToTarget > 15f) currentState = AIState.Patrol;
break;
case AIState.Attack:
// 攻击逻辑
break;
}
}
5. 性能优化与调试技巧
5.1 性能关键指标
通过Profiler监测的关键数据:
| 指标 | 正常范围 | 优化方法 |
|---|---|---|
| NavMesh.CalculatePath | <1ms | 减少调用频率 |
| NavMeshAgent.SetDestination | <0.5ms | 使用Coroutine |
| NavMesh.FindClosestEdge | <0.3ms | 缓存结果 |
5.2 多线程路径计算
Unity 2021+支持Job System优化:
csharp复制using Unity.Jobs;
using UnityEngine.AI;
public class PathfindingJob : IJob {
public NavMeshQuery query;
public NativeArray<NavMeshLocation> result;
public void Execute() {
// 线程安全的路径计算
}
}
5.3 常见问题排查
-
角色原地旋转:
- 检查目标点是否可达
- 调整Agent Base Offset
-
路径抖动:
- 提高Obstacle Avoidance Quality
- 增加Pathfinding Update Interval
-
性能骤降:
- 限制同时活动的Agent数量
- 使用LOD分级寻路精度
csharp复制// 调试可视化
void OnDrawGizmos() {
if (agent != null && agent.hasPath) {
Gizmos.color = Color.red;
for (int i = 0; i < agent.path.corners.Length - 1; i++) {
Gizmos.DrawLine(agent.path.corners[i], agent.path.corners[i+1]);
}
}
}
6. 进阶功能扩展
6.1 动态地形处理
实时响应场景变化的方法:
- 使用NavMeshSurface组件
- 调用NavMesh.UpdateNavMeshData
- 局部更新技术实现
csharp复制// 动态障碍物示例
void OnDestroyedBuilding() {
NavMeshDataInstance dataInstance = NavMesh.AddNavMeshData(navMeshData);
StartCoroutine(UpdateNavMeshAsync());
}
IEnumerator UpdateNavMeshAsync() {
AsyncOperation operation = NavMeshSurface.UpdateNavMesh(navMeshData);
yield return operation;
}
6.2 多Agent协作
群体行为实现方案:
- 领导跟随模式:
csharp复制void FollowLeader() {
Vector3 offset = leader.forward * -2f;
agent.SetDestination(leader.position + offset);
}
- 群体避让算法:
csharp复制void AvoidCrowd() {
foreach (var other in nearbyAgents) {
Vector3 dir = transform.position - other.position;
agent.velocity += dir.normalized * (1f / dir.magnitude);
}
}
6.3 自定义Area成本
实现地形影响移动速度:
- 在Navigation面板定义Area类型
- 设置不同Area的Cost值
- 代码控制Area使用:
csharp复制// 设置Area掩码
agent.areaMask = 1 << NavMesh.GetAreaFromName("Mud");
// 动态修改Cost
NavMesh.SetAreaCost(NavMesh.GetAreaFromName("Water"), 5.0f);
在项目实际开发中,我发现合理设置Stopping Distance能显著改善AI的"粘滞"问题。当敌人接近玩家时,保持1-2米的缓冲距离不仅更符合真实行为,也能避免物理碰撞导致的抖动现象。另一个实用技巧是使用agent.isStopped临时禁用移动,在处理攻击动画时特别有用。