ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity寻路系统实战:从零搭建NavMesh Demo与动态避障全解析

Unity寻路系统实战:从零搭建NavMesh Demo与动态避障全解析 1. 从零搭建一个Unity寻路Demo为什么我建议你用NavMesh如果你在Unity里做过任何带角色移动的项目迟早会遇到一个问题场景里有一堆障碍物角色怎么才能自己绕过去而不是一头撞墙上我刚接触Unity那会儿第一反应是写射线检测角色往目标走碰到东西就往旁边偏一点。看着挺聪明实际一跑全是毛病墙角卡住、围栏绕不过去、两个角色互相堵路调参数调到怀疑人生。后来接触了Navigation寻路系统才明白真正的“让角色自己找到路”根本不需要自己造轮子——引擎自带的NavMesh就是干这个的而且干得相当漂亮。这篇文章就是一套面向入门者的完整实战案例我会从创建一个空场景开始一步步手动搭建地形、烘焙导航网格、配置寻路Agent、写代码让角色自己走向目标点最后加入动态障碍物和简单的地形标记。全程不涉及复杂算法所有操作都在Unity编辑器界面和少量C#脚本里完成。适合刚学会基础操作、想让角色“活起来”的Unity新手也适合用过寻路但没系统梳理过完整流程的人。2. 整体设计思路这个Demo到底要覆盖哪些核心能力2.1 寻路系统的三个核心概念正式动手之前我习惯先把系统的三个基本构件讲清楚这样后面做每一步你都知道它在整个流程里扮演什么角色。第一个是NavMesh也就是导航网格。它本质上是一张覆盖在可行走表面上的“路网”Unity在场景里生成一大堆互相连接的三角形标记出“哪些地方能走、哪些地方不能走”。寻路算法实际是A*路径搜索就在这张网格上找从起点到终点的最短路径。你可以把它理解成地图App里的道路数据——没有路的地方导航再厉害也导不过去。第二个是NavMesh Agent中文常叫寻路代理。它的作用是把一个游戏对象变成“会走路的人”。你在Agent组件上配置移动速度、加速度、转身速度、避障半径这些参数然后只要给它一个目标坐标它就会自动在NavMesh上计算路径并朝目标移动。这个组件承担了路径跟随、平滑转向、避开动态障碍等大量工作你几乎不用写任何移动逻辑。第三个是NavMesh Obstacle寻路障碍物用来标记场景中会挡路的物体。烘焙导航网格时Unity会把静态障碍物直接刻进网格里动态变化的障碍物比如一扇会打开的门、一个巡逻的敌人则需要用Obstacle组件在运行期间实时影响路径。在实际开发中这三个组件合在一起构成了绝大多数NPC移动、玩家自动寻路、AI追击等功能的底层骨架。你不需要理解A*的实现细节但一定要理解这三个角色各自的分工否则后面出了问题你根本不知道去查谁。2.2 选择一个合适的实战场景雏形每次带新人做寻路我都不建议一开始就在正式项目里改。最好单独建一个空场景用一个简单的游戏规则来串联所有知识点。我这次选的实战目是这样地面上立了几堵墙当作障碍物一个绿色小球代表玩家角色一个红色方块当作目标点。玩家点一下鼠标左键绿色小球就自动绕过墙壁走向红方块。之后我还会加一个会左右移动的障碍物验证一下动态避障的效果。选择这个场景的原因有三点。第一它足够简单所有目光都聚焦在寻路系统本身不会被美术资源或者玩法逻辑干扰第二它覆盖了从静态烘焙到动态避障的完整链路是后续做复杂AI的基础模板第三它几乎不依赖任何外部资源Unity自带的Cube、Sphere和Plane就能搞定全部内容。除此之外我还会用到一个叫NavMesh Surface的组件。如果你用的是Unity 2018之后的版本官方推荐用这个组件来进行烘焙而不是老旧的Window - Navigation面板。它更灵活可以只烘焙场景的一部分运行期间也能动态生成新网格对入门学习者来说其实更好理解。2.3 操作面板或脚本先动哪个这块我多啰嗦一句。很多教程一上来就甩一堆脚本新手照着抄完了依然一脸懵因为不知道每一行代码是在干嘛。我推荐的路线是先在编辑器里把寻路流程完整走通再用代码替换其中的“手动操作”步骤。换句话说第一步先在Inspector面板里手动配置NavMesh Surface手动烘焙手动给小球添加NavMesh Agent然后直接给Agent指定一个目标坐标编辑器里就能拖先让小球动起来。这一步确认了“这套流程是通的”。第二步再考虑写脚本用代码来控制Agent的destination属性实现“鼠标点击哪里角色就走哪里”。这样每一段代码你都能对应到编辑器里你做过的事情学的不是一行行API而是一个完整功能的实现思路。3. 动手第一步创建场景、配置烘焙参数并生成NavMesh3.1 基础场景搭建打开Unity新建一个3D项目在场景里添加一个Plane作为地面。我习惯把地面大小调成10x10不要默认的10x10也够用但为了后面测试路径绕行效果宽一点更好。在场景里丢几个Cube当作墙壁摆成一个不完全封闭的迷宫造型。你不用刻意摆出复杂的迷宫只要确保从一个点到另一个点“直线过不去”就行了。例如在场景中央摆一堵长长的墙再在侧面留一个缺口小球想过去就必须绕墙走。目标点用一个Cube改成红色放在地面一角方便观察。再创建一个Sphere作为角色放在地面另一角。场景的层级结构保持干净之后加组件时不容易乱。这里有个我踩过的坑值得说出来所有作为地面的Plane、作为墙壁的Cube都要确保它们的碰撞体Collider存在。Unity创建Cube和Plane时会自动带Box Collider和Mesh Collider但如果你从外部导入模型常常是不带碰撞体的。没有碰撞体的物体会在烘焙阶段被当成“可以穿过”的空气角色会直接走过去看起来就像穿墙了一样。3.2 用NavMesh Surface生成导航网格选中地面在Inspector面板底部点击Add Component搜索NavMeshSurface并添加。这个组件是Unity 2018后集成在AI/Navigation包里的如果你的项目里搜不到先去Package Manager窗口找到AI Navigation包点Install安装然后回到Unity编辑器就能搜到了。添加完成后在NavMeshSurface组件的UI里首先是Agent Type默认是Humanoid。这个类型定义了导航网格的代理属性包括步高、可爬坡度、净空高度等。入门阶段直接用默认值即可后面我会讲这些参数对实际游戏意味着什么。接着看Collect Objects选项有三个选择Everything、Volume、Children。这里务必手动选一下不要直接烘焙。如果选Everything它会把整个场景里所有带NavMeshModifier的物体纳入计算适合整张游戏地图的烘焙如果选Volume你可以在场景里放一个Box来限定烘焙范围适合大地图的局部区域如果选Children那么只会烘焙这个组件所在物体及其子物体所包含的场景部分。我们这个简单场景只有一个平面和几堵墙直接选Everything就行。烘焙前确认地面勾选了Navigation Area里的Walkable墙壁默认不在Walkable里但Unity会自动把有碰撞体且不是Walkable区域的部分挖空所以墙壁也无需你额外手动设置。最后点击组件底部一行小字里的Bake按钮。稍等片刻场景里会出现一层浅蓝色的半透明轮廓线那就是烘焙好的导航网格。网格只覆盖地面这一层墙壁所在位置和外部区域都不会生成网格。如果你烘焙后什么也没出现优先检查地面是否有Collider、墙壁是否真的挡在地面的上方而不是和地面完全重叠卡进地板里。注意烘焙完成后如果你又新加了墙体或者移动了墙体位置必须重新点击Bake之前生成的网格不会自动更新。新手最容易忽略这一点反复排查半天代码结果只是网格过期了。3.3 烘焙参数到底在控制什么NavMeshSurface的Advanced选项里有几个参数新手容易糊弄过去我挑最重要的解释一遍。Agent Radius是寻路Agent的半径。它决定了网格边缘离墙壁等障碍物的最小距离也就是角色“胖”到什么程度。如果你的游戏角色是胶囊体这个数值应该大约是胶囊体横截面半径。设得太小角色会贴墙走看起来像是蹭着墙飘过去的设得太大会导致一些比较窄的通道无法生成网格角色过不去明明看起来很宽敞的门缝。Agent Height是角色高度。低于这个高度的障碍物会被忽略——比如地上一个只有10厘米高的台阶大于Agent Height就可以直接迈上去而不用绕路。在烘焙时Unity会把“头会撞到”的障碍物区域从网格中挖掉角色自然就不会往矮洞口里冲了。Max Slope是最大爬坡角度。超过这个角度的斜坡不会生成网格角色不会去走。在做户外地形、山路时这个参数会非常明显地影响寻路质量。Step Height是台阶高度。它决定角色能直接“走上”多高的台阶而不需要把它当成墙绕过去。这个参数和Agent Height经常搞混简单理解Agent Height管的是上方空间比如桥底能不能过去Step Height管的是脚下比如台阶能不能直接迈脚。如果你理解不了这些参数的微妙差别没有关系记住一个最朴素的经验法则保持默认参数并跑通流程等到出现“角色钻不过去”“角色在门口逡巡不前”等现象时再来逐项调整。寻路的参数调试本质是在“走得舒服”和“空间利用”之间做平衡这需要结合你具体项目的角色体型去感受。4. 核心配置NavMesh Agent组件参数与小球自动寻路4.1 添加并理解Agent组件将场景里的Sphere选中Add Component添加NavMesh Agent。这个组件栏里同样有一排参数我按优先级逐个说明。Speed是角色移动的期望速度单位是米每秒。设成3.5基本符合一般游戏角色的走路速度跑步可以设成8到10。这里要注意这个速度是寻路系统内部“希望到达的速度”真实速度会受到Acceleration加速度的影响不会瞬间拉满。Angular Speed是角色每秒可以转多少角度的速度。设成360看起来转身很灵活设成120就比较迟缓。假如你的角色是一个人类建议120到180如果是无人机、悬浮物体360是合理的。Acceleration决定了角色从静止到满速所需时间。较大的值启动感很强较小则起步平滑。数值上8是我常用的默认值。如果你的角色经常像“漂移”一样滑着走优先降低Acceleration。Stopping Distance是角色停下时距离目标点的距离。设成0并不代表它真的零距离停在目标点上它会因为自身半径和数值精度在目标附近小范围徘徊。做交互类NPC时这个值建议设成1到2米和玩家对话时保持一个舒服的距离而不是直接怼到脸上。Auto Braking如果不勾选角色到达目标附近后不会自动减速它会试图直接冲过去。典型使用场景是巡逻点连线角色走到一个点后直接奔赴下一个点不悬停。Radius和Height这两个参数必须在Agent组件里也设置一遍。它们的作用和烘焙参数里的Radius、Height一致是运行时寻路避障计算使用的物理尺度。务必让它们和NavMeshSurface对应参数保持大致匹配——如果烘焙时用的半径是0.5Agent组件里换成2角色在网格边缘的避障判定就会乱套。加入组件后你可以先在Scene视图里找到Agent组件的轮廓线——一个绿色圆圈代表避障用的虚拟体。如果不希望角色自身碰撞体Sphere Collider和这个虚拟体打架给角色添加的Collider半径和Agent的Radius保持接近即可。4.2 手动指定目标让小球跑起来给Sphere添加Agent组件后切到Game视图点击Play运行此时小球不会动因为我们还没给它指定目标。回到Scene视图选中Sphere在Inspector里的NavMesh Agent组件中你需要找到Destination属性。直接往这个字段里填一个坐标或者更方便的方式是在Scene视图里点击这个属性旁边的小圆点图标然后在场景里点选目标位置——一个白色小圆圈会出现在你点击的地方。如果你是在运行状态下尝试这个操作就会直观地看到效果运行后瞬间Sphere开始移动到指定的位置期间会自己绕开中间的墙。我可以非常负责任地告诉你这个“手动指定目标测试”步骤极其重要。它验证了网格、代理、障碍三者配合正常如果这一步都没跑通后面写再多代码都是白搭。常见的问题是点了目标但小球完全不动。排查优先级依次是——NavMeshAgent组件是否存在且Enabled目标点位置是否在烘焙网格的范围内Agent的Area Mask是否设置了允许寻路区域最后检查烘焙有没有过期。4.3 关键参数速查表为了查阅方便我把涉及到的核心参数整理成下表方便你调参时快速对照参数位置作用推荐起始值Agent RadiusNavMeshSurface / Agent代理转弯避让的横向尺度0.5可调Agent HeightNavMeshSurface / Agent代理可通行的最大高度2Max SlopeNavMeshSurface允许行走的最大坡度角度45Step HeightNavMeshSurface允许直接跨越的台阶高度0.4SpeedAgent移动速度3.5Angular SpeedAgent转身速度180AccelerationAgent加速度8Stopping DistanceAgent停止判定距离0依据需求调Auto BrakingAgent到达后是否自动停勾选4.4 用代码实现鼠标点击移动编辑器手动操作确认无误后我们进入编码阶段目标是做一个“点击地面角色走过去”的功能。选中Sphere添加一个C#脚本命名为PlayerClickMove。打开脚本编辑器清空模板内容写入以下代码using UnityEngine; using UnityEngine.AI; public class PlayerClickMove : MonoBehaviour { private NavMeshAgent agent; void Start() { agent GetComponentNavMeshAgent(); } void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { agent.SetDestination(hit.point); } } } }这段代码的核心就两行一行把鼠标点击位置转换成一条从摄像机出发的射线一行调用agent.SetDestination把目标坐标交给寻路系统。不需要自己写任何寻路或避障逻辑角色会自己计算路径并移动。这里我特意用了Camera.main所以你的主摄像机Tag必须是MainCamera。如果你用的Unity默认Main Camera那就没问题。但如果你在测试时自己新建了摄像机忘记改Tag代码会报错或者完全不触发移动这是一年级新手最容易卡住的地方之一。射线检测在HitTest时命中的物体需要包含Collider。我们在场景里点击地面Plane它自带Mesh Collider所以能正常命中。如果你点击的是UI界面上的按钮这里会出问题——UI元素被点击时不触发Physics.Raycast需要另用EventSystem处理入门阶段不用纠结这个。4.5 SetDestination方法的内部逻辑我稍微聊聊SetDestination方法做了什么事帮助你理解它和直接修改Transform.position的区别。当你调用SetDestination时Agent先把目标点映射到最近的NavMesh网格上这一步叫SamplePosition然后在网格上用A*算法从当前节点搜索一条到目标节点的路径得到一串路径点。之后Update循环里Agent沿着这些路径点走经过每个拐点会调用转向逻辑同时不停用当前速度、加速度和障碍物信息修正自己的位置。最终呈现出来的效果就是平滑转弯、自动绕障。如果你直接改Transform.position等于强行传送寻路系统完全感知不到这个过程路径计算和避障全都失效。很多初学者把两者混用角色看起来在走但总是一卡一卡地抽搐那就是因为每一帧都在被传送、被拉回互相打架。5. 进阶玩法动态障碍物、区域标记与多点巡逻5.1 NavMesh Obstacle让障碍物动起来静态场景跑通之后我们来体验寻路系统的一个关键升级动态避障。它在实际游戏里太常见了——巡逻的守卫、突然关闭的大门、缓慢推过来的箱子都会改变角色不能走的路。在场景里新建一个Cube把它放到地面中央然后给它添加NavMesh Obstacle组件。组件界面有几个选项需要留意Carve是核心开关。如果勾选运行时这个物体会动态地从NavMesh上“挖掉”一块区域Agent在寻路时会把这块障碍物当成本来就不存在的路如果不勾选它只是参与物理避障但路径计算时并不知道它的存在视觉上角色会“贴边走”而不是提前绕开。我强烈建议动态障碍全部勾选Carve。但是要注意一个性能坑——Carve对性能的消耗不是线性的运行期间频繁移动、旋转障碍物会导致NavMesh需要反复重新雕刻。如果你需要在场景里放几十个同时移动的动态障碍物且每帧都在改变位置请考虑用RVO避障Local Avoidance方案而不是全部依赖Carve。入门阶段一两个Carve物体完全没问题。添加组件后给这个Cube随便写一个简单的左右移动脚本让它运行后在两个点之间来回移动。此时运行游戏点击小球让它穿过这块区域观察效果。如果小球原本计划好的路径被障碍物挡住了它会先停下来等障碍物离开后再继续走——这是Carve的工作方式。如果小球直接穿过了障碍物检查一下Obstacle组件是否勾选了Carve以及它的Transform朝向是否和碰撞体一致。重要提示上述的Carve在“运行期间”生效但不会自动修改已烤好的静态NavMesh数据。如果你暂停游戏回到编辑器会发现蓝色网格层并没有被挖掉那块。这是正常的。5.2 用NavMeshModifier控制区域通行权刚才我们是以“物体类型”区分道路在实际项目中你还会遇到更复杂的场景同一块地面在某些玩法阶段允许通行某些阶段不允许。实现这个需求不需要改地形用NavMeshModifier就能实现。选中某个墙壁或某个地板区域添加NavMeshModifier组件。它有这些选项Override Area把这块区域指定成另一个Area类型Area选一个区域类型默认是Walkable还有Not Walkable、Jump、Open Door等预设。你可以把某个地面的Area改成Not Walkable重新烘焙NavMeshSurface后这块区域会从网格中被扣掉Agent路径不会经过这里。到了特定剧情阶段你再用代码把它改回Walkable重新烘焙道路就重新开放了。有一个精妙用法值得分享在大型游戏中你可能会把“森林”“沼泽”“雪地”“公路”分别烘焙成不同的Area然后在代码里用NavMeshAgent的areaMask属性控制当前角色能走哪些区域。比如一个陆战单位可以走公路和草地但不能走沼泽水上单位则只走沼泽和水面。同一张场景不同的Agent类型各有各的可行网络这比物理碰撞体方案灵活一万倍。5.3 给Agent做多点巡逻最后我们扩展一下寻路能力的应用方式不点击目标而是让Agent自动按一串坐标点巡逻。巡逻是NPC的常见行为也适合验证你的寻路系统在“无人干预”情况下的健壮性。新建一个脚本PatrolPath挂在小球身上并删掉之前的PlayerClickMove脚本。代码如下using UnityEngine; using UnityEngine.AI; public class PatrolPath : MonoBehaviour { public Transform[] waypoints; private NavMeshAgent agent; private int currentIndex 0; void Start() { agent GetComponentNavMeshAgent(); if (waypoints.Length 0) { agent.SetDestination(waypoints[currentIndex].position); } } void Update() { if (!agent.pathPending agent.remainingDistance 0.5f) { currentIndex (currentIndex 1) % waypoints.Length; agent.SetDestination(waypoints[currentIndex].position); } } }在场景里创建几个空物体依次摆放到不同位置把它们的Transform拖进脚本的Waypoints数组。运行时小球会在这些点之间循环移动。这段代码里值得关注的是agent.remainingDistance的判定方式。很多教程写的判断是remainingDistance 0.1f但实际运行时如果路径还没计算完remainingDistance可能是零导致角色一开局就跳转到下一个点永远卡在第一个点不动。用agent.pathPending排除路径计算中的状态这是老开发才会注意到的细节。6. 常见问题与排查技巧实录省下你三天的调试时间下面这些问题是我在实际带项目过程中反复遇到的大部分来自初学者反馈每一个都是真实踩出来的坑。6.1 角色不走代码没报错Debug.Log也没输出最常见的三大元凶主摄像机没有设置Tag为MainCamera目标点不在NavMesh合法区域Use在PlayerClickMove脚本里漏了GetComponent ()。还有一个隐藏很深的坑如果你把Agent的Area Mask设成了Nothing任何目标都会被视为不可达。我在自己项目里就曾误操作勾选了Nothing当时排查了半天没头绪最后看README才发现是这处设置。遇到“角色完全不响应”时先看Agent组件的Area Mask是否为Everything。6.2 角色走着走着突然跳了一下或瞬移到另一个点这是NavMesh发生了“断崖”式切换的典型表现。常见原因有两条。第一你的Agent Height和Step Height设置得太小导致角色在爬坡或者跨台阶时找不到连续网格系统图省事直接“跳”过一段第二烘焙的网格在狭窄转角处出现了非连通碎片角色走到边缘后无法继续只能跳到最近的网格上。针对这种情况我的建议是把Agent Height调大一点、Step Height调小一点后再重新烘焙尽量让网格保持一整块连贯的覆盖。更重要的是不要把Inspector里的网格预览当成绝对精确的地图它只是寻路的参考。6.3 角色绕障碍特别慢像在“挤”着走这种情况基本都出在Agent的Radius和Obstacle的Carve配合上。如果动态障碍物体积很大但Carve只挖了很小的一个洞Agent的避障半径一算根本钻不过去就一直贴着边来回打转。记住一个经验值Carve挖掉的尺寸应大于Agent Radius的两倍否则通道被认为是不可通行。如果实在无法调整障碍物尺寸可以给障碍物加一个Scale为1.2的透明辅助碰撞体扩大Carve范围。6.4 烘焙后场景变紫红色材质丢失这个不属于寻路系统但玩Unity的人真的很容易被这个问题打断节奏。紫红色材质通常是因为相关Shader在导入时丢失特别是你用了一些第三方资源或者手动改了管线。解决思路是确认项目有没有用URP或其他渲染管线再把材质里的Shader重新指定为匹配当前管线的版本。这跟寻路没关系但它会出现在你频繁烘焙和切换场景的过程中顺手提一下免得你误以为自己的NavMesh烘焙导致项目坏了。6.5 菜品测试时很流畅打包到手机上卡顿严重动态Obstacle的Carve在高频率更新时会产生很高的重建网格成本这在PC上感知不明显在手机上就原形毕露了。解决方案减少同时启用Carve的障碍物数量用静态烘焙代替动态Carve如果必须保留动态障碍适当降低Obstacle的更新频率比如给移动的障碍物加上每隔0.2秒才允许雕刻一次的限制。6.6 快速排查表现象首要排查点次要排查点角色完全不动Agent组件的Destination是否有效Area Mask是否为Everything角色瞬移跳动网格是否连续Agent Height/Step Height是否过小角色走错路烘焙是否过期障碍物上是否有Collider不避让动态障碍Carve是否勾选Obstacle是否挂在移动物体上运行流畅打包卡Carve物体数量过多更新频率是否过高7. 收尾前再分享两个我常用的扩展思路这个Demo跑通之后你对Unity寻路应该有了一个完整的功能认知。但如果你想把这套系统用在真实项目里我建议再往两个方向去接触。第一个方向是多层级地图寻路。如果你的游戏里有电梯、楼梯、传送门单纯依靠NavMesh很难覆盖多个楼层的垂直联通。这时需要用到Off-Mesh Link组件它可以在两个网格点之间手动连接一条“跳跃弧线”。例如一楼和二楼各烘焙一块NavMesh在一、二楼的入口处放两个Off-Mesh Link的端点Agent就能在寻路时自动判断“要从一楼网格跳到二楼网格”。这算是一个看起来简单但实际很实用的进阶扩展。第二个方向是多角色协作避障。你在场里放几个小球全都挂上NavMesh Agent同时给每个小球不同的Speed或Radius运行一下就会发现它们互相之间也会避让。Unity内置的本地避障在中低数量NPC时表现还不错但如果你要做几十个甚至上百个单位同时移动的策略游戏还是得考虑把Agent的Radius调小并开启High quality避障模式或者放弃NavMesh Agent改用RVO算法库。总之先跑通这个小Demo你其实已经站在通往很多高级功能的路口了。我个人在实际操作中的体会是寻路系统调试最大的敌人是“参数太多出了问题不知道改哪一个”。所以我的最后一条建议特别简单——一次只改一个参数。烘焙网格出问题先调NavMeshSurface这一边运行时运动出问题先调Agent组件这一边两者之间的问题优先怀疑Radius不匹配。把这套“二分定位”的思路带进项目里你以后遇到任何寻路系统的疑难杂症都能很快找到病灶所在。
返回列表