
1. 项目概述为什么你的游戏角色总在原地发呆做游戏开发尤其是涉及到角色扮演、策略或者任何需要角色自主移动的游戏时最让人头疼的问题之一就是你费尽心思设计好了关卡和敌人结果你的角色要么卡在墙角疯狂鬼畜要么对着一个明明能走过去的斜坡望洋兴叹要么就是几个角色挤成一团谁也不让谁。寻路这个看似基础的功能往往是游戏体验的“隐形杀手”。Unity 内置的 AI Navigation 系统特别是其核心的 NavMesh导航网格技术就是来解决这个问题的。它不是一个简单的“从A点直线走到B点”的脚本而是一套完整的、基于对游戏场景几何理解的空间推理系统。简单来说NavMesh 会先给你的游戏世界拍一张“地形通行证”标记出哪里能走、哪里是墙、哪里是楼梯。然后当你的角色需要移动时寻路算法如 A*会在这张“通行证”上快速计算出一条最优的、可通行的路径。最后角色再沿着这条路径平滑移动并智能地避开动态障碍和其他移动的物体。这个过程听起来复杂但 Unity 已经为我们封装好了绝大部分工作。今天我就带你从零开始彻底搞懂这套系统。我们不止要实现一个“点击地面角色走过去”的 Demo更要深入理解每一步背后的原理和那些官方手册里不会写的“坑”。目标是让你在 5 分钟内跑通核心流程并在接下来的时间里成为能驾驭这套系统、解决实际复杂问题的“老司机”。2. 核心原理拆解NavMesh 不是魔法是空间管理艺术在动手之前我们必须先搞清楚 NavMesh 和 AI Navigation 到底在背后做了什么。这能帮助你在遇到诡异问题时知道该从哪里入手排查而不是盲目地调整参数。2.1 导航网格世界的可通行地图想象一下你要给一个机器人规划室内清扫路线。你不会让它去记忆每一寸地板和每一件家具的精确三维坐标那样计算量太大。更聪明的做法是给房间地板铺上一层虚拟的、由无数三角形拼接而成的“网格地毯”。机器人只需要知道它只能在这块“地毯”上活动碰到“地毯”边缘比如墙壁、家具脚就得绕开。NavMesh 就是这个“虚拟地毯”。它的生成过程烘焙本质上是场景的体素化与表面重建体素化Unity 会在你设定的导航区域上空投射一个三维的网格将空间切割成无数小立方体体素。可走面检测系统检测哪些体素的下表面与场景中标记为“可行走”的物体如地面、斜坡接触。表面生成将这些可行走体素的下表面连接起来生成一个连续的、由三角形构成的网格表面。这个网格就是 NavMesh。每个三角形都代表角色可以站立的一个位置。关键理解NavMesh 是一个二维表面在三维空间的表达。它紧贴着你场景中的地面、台阶等可站立面。角色寻路时是在这个二维表面上“行走”而不是在真正的三维空间中直线飞行。这就是为什么角色能理解“走楼梯”而不是“穿模上楼”。2.2 全局寻路与局部避障战略与战术的配合这是 AI Navigation 系统最精妙的设计也是很多新手混淆的地方。它把移动问题分成了两个层级全局寻路解决“怎么去”的问题。当你在远处点击一个目标点系统需要计算从起点到终点的完整路径。这通常使用 A* 算法在 NavMesh 的三角形网络上进行。这个过程计算量相对较大但不需要每帧都做只在目标改变或路径被阻断时才需要重新计算。它返回的是一条由一系列路径点拐点构成的“走廊”。局部避障解决“怎么走”的问题。当角色沿着“走廊”移动时它可能会遇到其他移动的角色动态障碍物或者需要微调路线以更平滑地拐弯。这就是局部避障的职责通常使用 RVO互逆速度障碍等算法。它计算频率很高每帧但只关心角色周围一小片区域内的其他物体通过轻微调整每个角色的移动速度和方向来避免碰撞实现自然的“擦肩而过”或“分流”效果。一个生动的比喻全局寻路好比手机地图给你规划了一条从家到公司的驾车路线战略。局部避障则是你开车时为了避开前方突然变道的车辆、或者绕过路面的一个小坑而进行的微操战术。两者协同才能既到达目的地又保证旅途安全顺畅。2.3 智能体的抽象角色是如何被理解的在 Navigation 系统中你的游戏角色无论是人、怪物还是汽车都被抽象为一个“NavMesh Agent”导航网格智能体。你可以把它想象成在 NavMesh 这张地图上行驶的一辆具备基础AI的“小车”。这个 Agent 组件有几个关键参数直接决定了角色的移动“手感”半径角色在 NavMesh 上所占用的“身体”大小。这决定了角色能通过多窄的通道。如果半径设置得比通道还宽寻路就会失败。高度角色能通过的最低空间。用于判断能否钻过门洞、隧道。步高角色能迈上去的最大台阶高度。比步高低的障碍如路边石、小门槛会被视为可行走地面的一部分而不是障碍。最大坡度角色能爬上的最大斜坡角度。超过这个角度的斜坡在烘焙时不会被计入 NavMesh。速度、角速度、加速度这些参数控制移动的动力学表现让移动看起来更自然而不是生硬的瞬移。理解这些参数是调优角色移动行为的基础。一个常见的坑是美术给了一个造型复杂的角色模型但你却用一个默认的、半径很小的圆柱体去代理它导致角色经常卡在视觉上很宽、但实际上 NavMesh 很窄的地方。3. 环境准备与场景搭建理论说得差不多了我们开始动手。首先确保你有一个 Unity 项目建议使用 2021 LTS 或更新版本系统更稳定。我们将创建一个最简单的场景来演示。3.1 创建基础场景新建一个 3D 项目Core 或 URP/HDRP 均可。在场景中创建一个 Plane 作为地面重置其 Transform。创建几个 Cube缩放并摆放在 Plane 上作为障碍物。比如摆成一个简单的迷宫格局。创建一个 Capsule胶囊体将其放置在 Plane 上。这将是我们的玩家角色。给它改个名比如 “Player”。现在你的场景应该有一个地面、几个障碍物和一个角色。这构成了我们测试寻路的基本环境。3.2 设置导航静态对象这是烘焙 NavMesh 前至关重要的一步。Unity 需要知道场景中哪些物体是构成“可行走区域”或“障碍物”的静态几何体。在 Hierarchy 中选中Plane地面和所有作为障碍物的Cube。在右侧 Inspector 面板的右上角找到Static下拉复选框。点击它。在弹出的菜单中确保Navigation Static被勾选。这意味着这些物体将参与导航网格的烘焙计算。重要提示对于大型、复杂的场景不要盲目地将所有物体都设为 Navigation Static。只标记那些确实影响行走的物体地面、墙壁、固定家具。对于装饰物如小石子、花瓶、特效等不影响通行的物体不要标记这可以显著减少烘焙数据量提高烘焙速度和运行时性能。3.3 配置导航区域我们还需要告诉 Unity不同的表面有什么样的行走属性。比如地面是正常行走沼泽是减速区域公路是加速区域。打开Window AI Navigation窗口。切换到Areas标签页。这里预定义了 “Walkable”默认可走、“Not Walkable”不可走和 “Jump”跳跃需要额外设置。我们可以创建一个新区域来实验。点击 “” 号新建一个名为 “Mud”泥沼的区域并给它分配一个 Cost花费为 5Walkable 的 Cost 是 1。Cost 越高角色越不愿意走这条路。寻路算法会寻找总 Cost 最低的路径而不仅仅是距离最短的路径。在场景中选中一个 Cube障碍物。在 Inspector 中你会看到一个Navigation Area组件如果没有可以添加。将其区域从默认的 “Walkable” 改为 “Not Walkable”。这样烘焙时这个 Cube 所占的位置就不会生成 NavMesh。我们也可以把地面 Plane 的 Navigation Area 设为 “Walkable”默认就是。4. NavMesh 烘焙生成你的游戏世界地图一切准备就绪现在来烘焙属于你这个场景的 NavMesh。保持在Navigation窗口切换到Bake标签页。这里有一堆参数但新手重点关注这几个Agent Radius上文提到的智能体半径。先保持默认0.5。Agent Height智能体高度。默认2.0。Max Slope最大坡度。默认45度。Step Height步高。默认0.3。Drop Height和Jump Distance用于处理跳跃连接的我们先不管。点击右下角蓝色的Bake按钮。Unity 会开始处理场景状态栏会显示进度。烘焙完成后你会看到场景视图中的地面和可走区域被覆盖上了一层蓝色的网格这就是 NavMesh而障碍物Cube所在的位置则是空的。如果没看到确保在场景视图的Gizmos下拉菜单中Navigation是开启状态。烘焙后的现场检查绕场景飞一圈检查蓝色网格是否完全覆盖了所有你希望角色能走到的地方。检查障碍物周围是否有足够的空间让角色根据你设置的半径通过。尝试调整 Bake 面板中的Agent Radius为一个更大的值比如 1.0然后再次点击Bake。你会发现蓝色网格区域缩小了因为系统认为“更胖”的角色需要更宽的通道。这就是为什么角色会卡住的原因之一——烘焙用的参数和实际角色 Agent 的参数不匹配。5. 实现角色点击移动地图有了现在让我们的角色动起来。5.1 为角色添加 NavMesh Agent选中场景中的 “Player” 胶囊体。点击 Inspector 底部的Add Component搜索并添加NavMesh Agent组件。保持其默认参数即可。注意这里的Radius、Height、Speed等参数应该与烘焙 NavMesh 时使用的参数设置相匹配或更宽松。例如烘焙时 Agent Radius 是 0.5那么角色的 NavMesh Agent 半径最好 ≤ 0.5否则它可能无法通过一些烘焙时认为“刚好能过”的狭窄通道。5.2 编写点击移动脚本我们需要一个脚本来处理鼠标点击并将目标位置传递给 NavMesh Agent。在 Project 窗口中右键创建一个新的 C# 脚本命名为ClickToMove。将脚本挂载到 “Player” 游戏对象上。双击打开脚本编写如下代码using UnityEngine; using UnityEngine.AI; // 引入 AI 命名空间 public class ClickToMove : MonoBehaviour { private NavMeshAgent agent; // 引用 NavMeshAgent 组件 private Camera mainCamera; // 主摄像机引用 void Start() { // 获取挂载在同一个游戏对象上的 NavMeshAgent 组件 agent GetComponentNavMeshAgent(); if (agent null) { Debug.LogError(NavMeshAgent component not found on gameObject.name); } // 获取主摄像机用于屏幕点到世界的射线转换 mainCamera Camera.main; } void Update() { // 检测鼠标左键是否在当帧按下 if (Input.GetMouseButtonDown(0)) { // 从鼠标在屏幕上的位置发射一条射线 Ray ray mainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 存储射线击中的信息 // 发射射线并检查是否击中了任何碰撞体 if (Physics.Raycast(ray, out hit)) { // 调用 NavMeshAgent 的 SetDestination 方法设置目标点 // hit.point 是射线击中世界空间中的点 agent.SetDestination(hit.point); // 可选在目标点创建一个临时视觉标记如一个小球方便调试 // DebugSphere(hit.point); } } } // 一个简单的调试方法在目标位置生成一个临时小球 void DebugSphere(Vector3 position) { GameObject sphere GameObject.CreatePrimitive(PrimitiveType.Sphere); sphere.transform.position position; sphere.transform.localScale Vector3.one * 0.2f; Destroy(sphere, 2.0f); // 2秒后销毁 } }代码解析NavMeshAgent.SetDestination(Vector3 target)是核心方法。你给它一个世界坐标它就会自动计算路径并控制角色移动过去。所有复杂的寻路、转向逻辑都被封装在里面。我们使用Physics.Raycast来将鼠标的屏幕坐标转换为游戏世界中的三维坐标。射线击中的第一个碰撞体的点就是我们的目标点。Camera.main获取场景中标记为 “MainCamera” 的摄像机这是通用做法。5.3 测试与验证运行游戏。在 Game 视图中用鼠标左键点击地面避开蓝色的障碍物 Cube。你应该能看到角色自动寻路走向点击的位置。尝试点击障碍物后面、或者需要绕路的位置。观察角色是否会智能地绕过障碍物。点击一个不可到达的位置比如障碍物顶上。角色会移动到离该点最近的可达 NavMesh 位置。至此一个基础的点击移动寻路系统就完成了整个过程的核心就是烘焙地图 (NavMesh) 添加司机 (NavMesh Agent) 设定目的地 (SetDestination)。6. 高级功能与深度优化基础功能跑通了但要做一款精致的游戏这还远远不够。下面我们来探讨几个提升体验和解决实际问题的进阶技巧。6.1 处理动态障碍物让世界活起来游戏中的障碍物不全是静态的。比如一扇突然关上的门一个被推开的箱子或者一个正在移动的敌人。我们需要让 NavMesh Agent 能应对这些情况。方法一NavMesh Obstacle 组件这是处理动态障碍最直接的方式。创建一个新的 Cube命名为 “DynamicObstacle”。为其添加NavMesh Obstacle组件。在组件中你可以选择障碍物的形状立方体/圆柱体并设置其大小。勾选Carve选项。这是关键它意味着这个障碍物会实时地在 NavMesh 上“挖洞”阻止 Agent 穿过。运行游戏让你的角色尝试走向障碍物另一侧。当障碍物移动时NavMesh 上对应的区域会被实时更新角色会重新寻路。注意事项Carve功能虽然强大但计算开销较大。对于大量快速移动的障碍物要谨慎使用。通常对于门、可移动的箱子等低频变化的物体使用 Carve对于大量同屏移动的单位如一群小怪更适合用下一节的避障功能。方法二Agent 之间的避障这是通过 NavMesh Agent 组件自身的Avoidance避障属性实现的。复制几个 “Player” 对象让它们都拥有 NavMesh Agent 组件。为每个 Agent 设置不同的Avoidance Priority避障优先级0最高99最低。当两个 Agent 路径冲突时优先级低的会让路给优先级高的。调整Radius和Height确保它们有合理的物理体积。运行游戏分别点击目标点观察它们是否会互相穿插还是优雅地避让。这背后就是 RVO 算法在起作用。6.2 使用 Off-Mesh Link实现跳跃与下落有时角色需要去到 NavMesh 无法直接连接的地方比如跳过一个沟壑或者从高台跳下。这时就需要Off-Mesh Link网格外链接。创建 Off-Mesh Link在场景中创建两个小方块如 Cube分别放在沟壑的两边或者高台和地面。确保它们都在 NavMesh 表面上。选中其中一个方块在 Navigation 窗口的Object标签页勾选Navigation Static是的它需要是静态的并在Navigation Area下拉菜单中将其区域设置为Jump或者你自定义的链接区域。对另一个方块做同样操作。在 Hierarchy 中创建一个空游戏对象命名为 “JumpLink”。选中 “JumpLink”添加Off Mesh Link组件。将该组件的Start和End字段分别拖拽赋值给刚才创建的两个小方块。在组件上你可以设置链接的Cost Override覆盖花费让角色更倾向于或更不倾向于使用此链接和Bi-Directional是否双向通行。工作原理当 Agent 寻路时如果发现路径的起点和终点分属两个被标记为可链接的 NavMesh 区域并且它们之间有一个 Off-Mesh Link 连接它就会将“使用这个链接”作为路径的一部分。当 Agent 走到链接起点时你可以通过脚本例如播放跳跃动画或使用 Agent 的自动移动来完成跨越动作。6.3 分层导航与区域成本创建复杂的移动逻辑回到我们之前创建的 “Mud” 区域。如何让角色真的走在上面会减速首先你需要将场景中的某些面比如另一个 Plane标记为 “Mud” 区域。在 Navigation 窗口的Areas页选中 Mud然后使用场景视图上方的Paint工具需要切换到 Object 标签页下的 Area 子标签像刷地形一样把 Mud 区域刷到特定的地面上。重新烘焙 NavMesh。在角色的 NavMesh Agent 组件上有一个Area Mask。这决定了角色可以行走在哪些类型的区域上。默认是所有 Walkable 区域。确保它包含了 “Mud”。关键点来了NavMesh Agent 在计算路径时会累加路径经过的所有三角形的区域成本。Mud 区域的 Cost 是 5是普通地面Cost 1的 5 倍。因此即使一条穿过 Mud 的路径直线距离更短系统也可能选择一条更长但全是普通地面的路径因为总成本更低。要让角色在 Mud 上减速你需要在脚本中动态修改 Agent 的speed属性。可以通过NavMeshAgent.SetAreaCost来设置某个区域对特定 Agent 的花费但这主要影响寻路选择。要影响移动速度通常的做法是在Update中检测 Agent 当前所在的 NavMesh 区域通过NavMesh.SamplePosition或NavMeshAgent.currentNavMeshHit如果是在 Mud 上就将agent.speed设置为一个较低的值。7. 性能优化与常见问题排查当你的游戏场景变得庞大角色数量增多时寻路系统可能成为性能瓶颈。以下是一些优化和排查技巧。7.1 性能优化要点精简 NavMesh 数据代理尺寸使用尽可能大的 Agent Radius 和 Height 进行烘焙。这能生成更简单、三角形数量更少的 NavMesh提升寻路效率。只要你的角色都能满足这个最小通过尺寸即可。烘焙分辨率在 Navigation Bake 面板中降低Voxel Size体素大小。体素越大生成的 NavMesh 越粗糙但烘焙和运行时计算越快。在保证角色移动不“踩空”或卡住的前提下尽量用大的体素。分块烘焙对于超大型的开放世界不要烘焙一整张巨大的 NavMesh。可以将世界分成多个区域Tile分别烘焙。运行时通过NavMesh Links连接它们。Unity 的 NavMesh Surface 组件支持此功能。控制寻路调用频率不要每帧都为所有 AI 调用SetDestination。对于巡逻的敌人可以在到达一个点后等待几秒再设置下一个点。对于大量需要追踪玩家的敌人可以考虑使用“扇区更新”或“距离阈值”只有离玩家较近的敌人才频繁更新路径。善用代理缓存对于大量使用相同 NavMesh Agent 预设的敌人使用对象池技术来复用 Agent 组件避免频繁的 Instantiate 和 Destroy 带来的开销。7.2 常见问题与解决方案速查表问题现象可能原因解决方案角色卡在角落或门口不动1. Agent Radius 大于通道实际宽度。2. 烘焙时 Agent 参数与运行时 Agent 参数不一致。3. 障碍物未正确设置为 Navigation Static 或 Not Walkable。1. 减小 Agent Radius或重新烘焙更大通道的 NavMesh。2. 确保烘焙设置与角色 Agent 组件设置匹配。3. 检查障碍物 Static 标记和 Area 设置。角色“穿墙”或走到不该去的地方1. 墙壁等障碍物未参与烘焙未标记为 Static 或 Area 为 Walkable。2. NavMesh 烘焙到了墙壁表面如薄墙两面都生成了网格。1. 将障碍物标记为 Navigation Static 且 Area 为 Not Walkable。2. 检查模型碰撞体确保是实心的。对于薄墙可以手动放置一个 Cube 碰撞体并标记为障碍。点击后角色不移动1. 角色未添加 NavMeshAgent 组件。2. 点击的位置不在 NavMesh 上如点击了空中或障碍物。3. 脚本中射线检测未命中或 SetDestination 未被调用。1. 为角色添加组件。2. 确保点击在蓝色 NavMesh 区域。使用NavMesh.SamplePosition将任意点投影到最近 NavMesh 点。3. 添加 Debug.Log 检查射线命中点和 SetDestination 调用。移动路径不自然频繁拐弯1. NavMesh 烘焙得太精细Voxel Size 太小生成了过多三角形。2. 路径拐点Corners过多。1. 增大烘焙的 Voxel Size。2. 调整 NavMeshAgent 的Steering下的Path Corners相关参数或使用NavMeshAgent.SetPath()传入简化后的路径点。多个角色挤在一起不避让1. Agent 的 Avoidance Priority 设置相同或不合理。2. 避障质量Quality设置过低。3. Agent 的 Speed/角速度过高避障算法来不及反应。1. 设置不同的优先级。2. 在 Navigation 窗口的 Agents 页提高避障的Quality和Iterations迭代次数但会消耗更多 CPU。3. 适当降低移动速度或增大 Agent 之间的物理间隔。烘焙时间过长或文件巨大1. 场景过大或 Navigation Static 物体过多过复杂。2. Voxel Size 设置过小。3. 包含了太多不需要导航的细节物体。1. 考虑分块烘焙。2. 适当增大 Voxel Size。3. 仔细检查只将必要的物体标记为 Navigation Static。7.3 调试与可视化工具Unity 提供了强大的内置工具来调试 NavMesh场景视图显示在 Scene 视图的 Gizmos 菜单中可以开启/关闭 NavMesh、NavMesh Agent 路径、障碍物等的可视化。NavMesh Agent 路径绘制在游戏运行时选中一个带有 NavMesh Agent 的游戏对象在 Scene 视图中可以看到它计算出的路径一条绿线和路径拐点。Console 日志NavMeshAgent.isPathStale、NavMeshAgent.pathStatus等属性可以帮助判断寻路状态路径是否失效、是否完成等。手动查询在脚本中可以使用NavMesh.CalculatePath手动计算路径并检查其有效性这在 AI 决策时非常有用。掌握这些调试方法就像给了你一副“透视眼镜”能让你清晰地看到寻路系统内部的工作状态快速定位问题根源。从一张静态的场景几何图到角色智能、流畅、互不干扰的移动Unity 的 AI Navigation 系统提供了一套从数据生成烘焙到实时决策寻路避障的完整解决方案。它绝不是简单的“直线移动”而是一个融合了空间分析、图论搜索和局部动力学模拟的复杂系统。理解其分层架构——全局寻路制定战略局部避障执行战术——是灵活运用和深度定制的关键。在实际项目中我最大的体会是烘焙参数是基石。花时间在场景搭建初期就确定好角色的基本尺寸半径、高度、步高并用这些参数去烘焙 NavMesh能避免后期大量的移动诡异问题。其次性能与效果的平衡需要反复测试。对于大量单位可以适当降低避障质量采用更粗糙的 NavMesh对于关键的主角或Boss则可以开启更高的精度。最后不要害怕去阅读和修改 Unity 提供的那些示例脚本它们是你理解 API 最佳实践和高级用法的宝贵资料。