十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

从RecastDemo入门NavMesh:游戏寻路与导航网格生成全解析

从RecastDemo入门NavMesh:游戏寻路与导航网格生成全解析 1. 初识寻路与NavMesh为什么路点导航不够用了做游戏开发尤其是涉及角色AI、怪物移动、RTS单位控制的时候“寻路”是绕不开的一个坎。早些年大家做寻路最土的办法是路点Waypoint导航——美术或者策划在地图上摆一串坐标点角色朝下一个点走到了再朝下一个点走。这东西在小场景、路径固定的游戏里勉强够用但一旦地图稍微复杂一点问题就全冒出来了角色会直愣愣地穿过墙角、队伍会被一个窄门卡死、动态障碍物一变化路径就断掉。后来有了网格Grid导航把地图切成均匀的方格用A*搜索格子。这比路点强不少但问题也很明显——地图大时格子数量爆炸而且角色走出来的路径是折线非常僵硬还得额外做平滑处理。再后来就是标题里提到的NavMesh导航网格。它把可行走区域剖分成一组凸多边形角色在网格顶点之间移动路径看起来自然得多查询效率也远高于格子搜索。NavMesh现在几乎是商业游戏寻路的标配Unity的NavMesh、Unreal的NavMesh、甚至很多自研引擎都在用。而Recast就是目前业界最成熟的开源NavMesh生成库之一它的配套Demo——RecastDemo正是用来展示和调试这套流程的最好工具。这篇博客我就从RecastDemo入手聊聊NavMesh的核心原理、Recast生成导航网格的几个关键阶段、实际操作中怎么调参以及我在自己项目里踩过的那些坑。这篇内容适合谁看正在入门游戏AI寻路、想搞懂NavMesh工作方式的人或者已经用Unity/UE的NavMesh、但不知道它底层怎么来的开发者。读完你应该能看懂RecastDemo里每个按钮是干嘛的了解导航网格从无到有经历了什么以及在自己的项目中怎么把这套东西接进来。2. RecastDemo一个“打开就能玩”的导航网格实验室2.1 Recast和它的Demo到底是什么Recast是开源社区里最出名的自动生成NavMesh工具库作者是Mikko Mononen最初是为了给游戏《War Thunder》的寻路系统做技术储备而开发的后来开源出来。它由两个核心库组成Recast负责“生成”导航网格Detour负责“使用”导航网格——包括路径查询、路径跟随、动态障碍物的处理等。RecastDemo是官方配套的示例程序直接拉下来编译就能跑。它内置了几个测试场景从简单的房间到一个城堡废墟可以让你一边旋转摄像机一边看到NavMesh是怎么一层一层被生成出来的。打开Demo的第一感觉可能是“东西真多”左侧一堆参数面板、右侧是3D场景、顶部还有几组构建按钮。但别慌这些UI都对应着NavMesh生成流程的各个阶段一步步理解下来比读十篇文档都有用。之所以推荐从RecastDemo入手去研究寻路是因为它把“从几何数据到可用于路径查询的导航网格”这条完整链路用可视化的方式摊开摆在了你面前。你可以看到体素化之后的样子、可以单独显示每个阶段的结果、可以拖拽参数实时观察效果变化——这种直观性是看任何源码、读任何文档都替代不了的。2.2 导航网格跟A*、路点到底差在哪先理清一个概念NavMesh不是一种寻路算法它是一种“数据表示”。底层的路径搜索算法仍然是A*之类的启发式搜索但搜索的图从格子变成了“由多边形组成的连通图”。路点导航是点对点连线网格导航是格子搜索NavMesh则是把地图划分成多个凸多边形。凸多边形的好处是多边形内任意两点连线都在多边形内部这意味着角色可以直线走不会撞墙。搜索时把多边形作为节点找到一条从起点多边形到终点多边形的路径然后沿着多边形之间的边一步步走。对比起来优势很明显路径点数量少搜索速度快不少路径更平直自然不需要大量的后处理平滑可以天然表达“斜坡”“跳跃”“下落”等不同移动类型数据量比均匀网格小得多适合大地图代价就是生成过程比格子导航复杂。格子导航只需要遍历一遍高度图就行NavMesh要经历体素化、区域划分、轮廓提取、凸多边形化等一系列步骤。这也是为什么Recast源码读起来略费劲——每一步都有很多细节。3. 核心机制拆解Recast把地图变成导航网格的四个阶段3.1 第一阶段体素化把三角形网格变成“乐高块”Recast的输入是场景的三角形网格Triangle Mesh包括静态几何体和地形。第一步体素化Voxelization把这堆三角形转换成一堆小立方块体素Voxel。这一步的理解可以类比成给你一堆三角形组成的雕塑你要用乐高积木把它重新堆出来。三角形的面会被离散化成一个个小方块方块落在哪些格子里这个格子就是“有东西”的。这些体素并不需要很精细它们的主要目的是后续判断“哪些空间是可以站人的”。RecastDemo的Sample Solo Mesh这个例子里点击Build后首先生成的就是这层体素。在侧边栏勾选“Voxels”可以看到结果地面和墙体变成了密集的小方块虽然看起来“糙”但已经足够表达碰撞空间了。体素化阶段的几个关键参数Cell Size体素格子的水平边长默认0.3单位是场景单位。这个值越小导航网格越精细但计算量和内存也会成倍增加。0.3是性能和精度的不错平衡点。Cell Height体素格子的垂直高度默认0.2。它决定竖直方向的分辨率台阶这种小高度差能不能被识别就看这个值。这里有个容易踩的坑如果你把Cell Size调得很大比如1.0角色会“变胖”很多细窄通道会被直接忽略调到很小0.1则构建时间会瞬间暴涨。所以调整这两个参数时要同时考虑你的角色半径和地图尺寸而不是纯靠感觉。3.2 第二阶段生成区域识别哪些地方是“可走的地面”体素化完成之后Recast需要判断哪些体素是“可行走”的。它会对每个体素做检测这个体素上面到下面有没有足够的高度供角色站立这个体素的倾斜角度是否超过角色能爬的坡度满足条件的体素会被标记为“可行走体素”然后Recast会把它们连接成连通的区域Region。这一步相当关键——整个场景被切割成多个互不连通的大块比如“一楼大厅”“二楼走廊”“外面的院子”各自成为一个独立的区域。在Demo里勾选“Regions”可以看到不同颜色的区域同一颜色代表同一块连通的可行走区域。如果两个区域之间没有连接比如中间有一堵墙完全隔开那它们就会显示为不同颜色。这一步涉及两个重要参数Agent Radius角色半径决定角色能挤过的最小通道宽Agent Max Climb角色能攀爬的最大台阶高度Agent Max Slope角色能走的最大坡度单位是度这三个参数本质上定义了“谁会在这张地图上走”。你得根据你的实际单位尺寸来设比如你的角色半径是0.5那通道最小宽度至少得是2倍的0.5加上一点余量否则角色会被卡住。3.3 第三阶段生成轮廓与凸多边形把“乐高堆”变成“几何图”区域还是体素表示的不是多边形Recast还需要把它们转换成多边形数据。过程是先提取区域边界上的体素轮廓Contour得到一圈粗糙的折线然后把轮廓上的顶点简化Simplify减少顶点数再把轮廓内部的区域细分Triangulate成一个个凸多边形。这一步大约是Recast里最数学的部分。轮廓简化用了Douglas-Peucker算法目的是在保持整体形状的前提下大幅减少顶点数量。后面细分凸多边形时核心要求是每个多边形都是凸的因为只有凸多边形的内部路径才保证不越界。如果生成了凹多边形角色走“直线”就可能穿模。在Demo中勾选“Contours”观察轮廓轮廓边线是粗锯齿状的再勾选“Poly Mesh”观察细分后的多边形这时你会看到地面变成了整齐的蓝绿色块。如果某个区域多边形数量特别多、甚至出现重叠或破碎多半就是参数设置不对。3.4 第四阶段生成Detail Mesh把高度变化补回来到这里导航网格已经是一组平面的凸多边形了但它还缺少“高度信息”。之前的体素化已经丢掉了部分细节地面咖咖不平怎么办角色站在斜坡上怎么表示Recast的解决方案是生成Detail Mesh细节网格在每个凸多边形内部根据原始几何体的表面高度生成一些额外的顶点和三角面用来记录精细的地表起伏。导航网格本身用凸多边形做路径搜索路径寻优只需要平面坐标但角色在行进过程中脚下的高度需要引用Detail Mesh来插值计算。所以NavMesh的数据通常是两层结构低分辨率的凸多边形用于寻路高分辨率的细节网格用于精确的高度采样。明白了这一点你就知道为什么Recast要费两道工序来做这件事了——路径搜索和高精度定位的需求是分开的。通过Demo的“Detail Mesh”选项可以直观看到之前方方正正的凸多边形表面出现了细碎的小三角形用来贴合地面形状。这个阶段几乎不需要手动调参但理解了它你在上层写路径跟随逻辑时就知道该怎么取高度了。4. 实操演练从编译RecastDemo到跑通第一个Sample的完整流程4.1 编译RecastDemo没有IDE也没关系先交代一下环境。RecastDemo官方仓库在GitHub上用C写的依赖很少基本就是标准库加一个OpenGL渲染器。编译方式有两种用CMake生成工程文件后再build或者直接在IDE里打开。我的个人建议是走CMake。在项目根目录创建一个build文件夹然后执行git clone https://github.com/recastnavigation/recastnavigation.git cd recastnavigation mkdir build cd build cmake .. -G Visual Studio 17 2022 # Windows下macOS/Linux用默认生成器 cmake --build . --config Release如果你的机器是Mac或者Linux把-G那条去掉或换成对应的生成器参数就行。编译完成之后在build/bin目录下会生成RecastDemo的可执行文件双击运行即可。提示老版本RecastDemo对高DPI屏幕支持不太好如果你打开后界面显示模糊可以右键属性里改一下“高DPI设置”或者直接用4K缩放为200%的机器观感会好很多。4.2 第一次Build理解左侧参数面板的每一个关键项启动RecastDemo后左侧面板最上方有一个“Sample”下拉框默认是“Solo Mesh”。这个模式下可以设置参数、构建NavMesh还能手动添加角色和路径查询点。先选Solo Mesh然后点一下工具栏的Build按钮几秒钟后你就能看到地面和墙体上出现了蓝色半透明的导航网格。此时左侧的参数几乎都能实时调整每次改动后重新Build就能看到变化。最关键的几个我之前已经提到了这里再罗列一下方便对照着调参数默认值作用调大的影响调小的影响Cell Size0.3体素水平分辨率生成更快细节丢失更精细构建更慢Cell Height0.2体素垂直分辨率台阶识别粗糙能识别更小的台阶Agent Radius0.6角色半径通道变宽小缝隙忽略能过窄道容易卡墙角Agent Max Climb0.9最大攀爬高度能翻高台阶矮台阶也过不去Agent Max Slope45最大可走坡度能爬陡坡缓坡也判定不可走Tile Size0分块大小0表示不分块分块后能局部更新调完这些参数别忘了按Build。如果你发现某块区域始终生成不出NavMesh大概率是Agent Radius设太大、把通道判定成“人过不去”了。这是新手最容易困惑的地方明明地面是平的为什么生成不了网格答案往往就是角色半径把路堵死了。4.3 试试“人”怎么走用工具栏做路径查询构建完NavMesh工具栏上有一个带圆点的小图标用来添加角色Agent和设置目标点。操作方式很直观先点击放置角色的初始位置再点击放置目标位置RecastDemo会自动调用Detour的路径查询接口显示一条从起点到终点的路径。这一步实际走的就是游戏里寻路的完整流程dtNavMeshQuery::findNearestPoly()找到距离起终点最近的多边形dtNavMeshQuery::findPath()在导航网格上做A*搜索得到一系列多边形dtNavMeshQuery::moveAlongCorridor()角色沿路径走廊移动并做碰撞规避路径在场景中往往是一根白线仔细观察能发现它在多边形边界之间穿行而不是贴在所有格子的中心线上——这就是NavMesh相对格子导航的一大优势路径又直又近。Demo还支持多角色同时寻路、动态障碍物测试你可以试着把一个Agent放在几个连通区域里依次设置目标点看看它们能不能绕开障碍找到路径。4.4 从Demo回到自己的引擎Recast集成的最小代码骨架Demo看完总得落到自己代码里。Recast的生成接口简单来说就是“喂顶点和索引调几个参数拿结果”。下面是一段最简C伪码展示了Recast的SoloMesh生成主流程// 1. 输入场景三角形网格 // vertexList: 顶点数组, triList: 三角形索引数组 rcConfig cfg; memset(cfg, 0, sizeof(cfg)); cfg.cs 0.3f; // 体素水平尺寸 cfg.ch 0.2f; // 体素垂直尺寸 cfg.walkableRadius 0.6f; // 角色半径 cfg.walkableClimb 0.9f; // 可攀爬高度 cfg.walkableSlopeAngle 45.0f; // 最大坡度 cfg.minRegionArea 8; // 剔除小于该面积(体素数)的区域 cfg.mergeRegionArea 20; // 合并小区域阈值 // 2. 构建高度场 rcHeightfield* solid rcAllocHeightfield(); rcCreateHeightfield(ctx, *solid, cfg.width, cfg.height, cfg.bmin, cfg.bmax, cfg.cs, cfg.ch); rcRasterizeTriangles(ctx, vertexList, triCount, triList, triCount, *solid, cfg.walkableClimb); // 3. 过滤可行走区域 rcFilterLowHangingWalkableObstacles(ctx, cfg.walkableClimb, *solid); rcFilterLedgeSpans(ctx, cfg.walkableHeight, cfg.walkableClimb, *solid); rcFilterWalkableLowHeightSpans(ctx, cfg.walkableHeight, *solid); // 4. 生成区域、轮廓、凸多边形流程较长此处省略部分函数 // rcBuildCompactHeightfield // rcBuildDistanceField // rcBuildRegions // rcBuildContours // rcBuildPolyMesh // 5. 生成细节网格 rcPolyMeshDetail* detailMesh rcAllocPolyMeshDetail(); rcBuildPolyMeshDetail(ctx, *polyMesh, *solid, cfg, *detailMesh);大致流程就是这样。产出的polyMeshdetailMesh就是导航网格数据你可以把它们烘焙成二进制文件运行时用Detour加载。这在游戏开发中很常见编辑器里离线构建导航网格游戏运行时只负责查询。5. 常见问题与排查技巧实录我踩过的那些寻路坑5.1 NavMesh生成不出来或者大块缺失场景Build完之后某些区域没有导航网格这是最常遇到的问题。我总结下来九成情况出在下面几个原因Agent Radius太大通道被判定“不够宽”WalkableClimb太小台阶或门槛变成“不可逾越”的障碍WalkableSlopeAngle太小斜坡被判定“不可走”场景几何体本身有破面、悬浮三角形导致可走空间计算异常排查思路很简单把Agent Radius调到0.1、WalkableClimb调到1.5、Slope调到80如果这个区域能生成出大块网格说明是参数太严格了一点一点往回调如果降低参数后依然缺失再去查几何体本身有没有问题。5.2 角色路径会“绕远路”或者“穿墙”路径查询出来路径比较远、明显偏离直觉先别急着怀疑算法。检查一下是不是没加dtQueryFilter的可走标记或者NavMesh里有“空洞”——有些不该阻挡的区域比如一张水下地表被误判成不可走把路径给切断了。穿墙问题通常是Detail Mesh和Poly Mesh的顶点没对齐导致的。Recast的Poly Mesh顶点是体素化的结果Detail Mesh是原始几何的投影两边一比对如果偏差超过Agent Radius的一半就会出现“路径看起来贴在墙里”的情况。解决方法通常是调整Cell Size让体素更精细或者检查几何体自身有没有重叠面。5.3 寻路结果跳变同一位置不同时间走的路径不一样这种情况常见于动态障碍物的场景。Detour提供了动态Tile的机制Tile Cache它允许你在运行时局部更新某一块Tile的导航网格——比如一堵墙被炸毁了你重新生成那一块的Tile数据即可。跳变往往是因为局部更新和全局数据的Tile边界没有对齐。每张Tile必须有少量重叠边缘通常是一个体素以上的宽度更新时只提交改动的区域不要整张Tile全部重建。另外注意使用动态Tile时推荐把Tile Size设成非零值让NavMesh统一分块这是动态更新的前提条件。5.4 性能问题Build慢、查询慢Build慢一般是体素分辨率太高。大地图如果用Cell Size0.1那个计算量是成指数增长的。能用0.3就尽量别用0.2。对于超大地图强烈建议开启分块构建Tile Size设为32或64这样构建时可以多线程并行而且以后做动态更新也方便。查询慢多半是NavMesh数据没有做空间分区。Detour本身用BVH树加速多边形查找但如果你的场景是多个不连通区域合在一张Mesh里查询时会把不相关的多边形也扫一遍。多区域场景尽量分开构建NavMesh查询时用dtNavMeshQuery::findNearestPoly先定位再做路径搜索性能会好很多。6. 避坑清单与调参心得从“能用”到“好用”Agent Radius不是“角色物理碰撞半径”而是“寻路用的保守半径”。如果你把物理半径直接填进去角色在窄通道边界贴着走时很容易蹭墙。一般是物理半径的0.8~0.9倍给寻路留一点余量。WalkableHeight这个参数经常被忽略它决定头顶空间默认是Agent Height。如果地图里有低矮的天花板或桥洞这个值没设对角色会显示“能走”但实际上“站不起来”。不要一次性把所有参数调到“看起来完美”。先保证生成结果正确再看路径是否自然最后才去优化构建时间和内存。这个顺序反了会浪费大量时间。在Tiled模式下所有Tile的参数必须一致。你不能这块Tile用0.3的分辨率那块用0.2不然Tile接缝处会出现裂缝。真的想深入了解Recast的话建议把源码里的Recast.cpp、DetourNavMeshQuery.cpp读一遍重点是dtNavMeshQuery::findPath函数的实现。你会发现核心就是A*加上一些堆优化并没有太多玄学。官方Demo里的几个Sample——Solo Mesh、Tile Mesh、Temp Obstacles——建议都跑一遍。Temp Obstacles是动态障碍物的演示它展示了运行时如何往NavMesh上临时“压”一个障碍并且自动重新计算路径这个在很多动作游戏里都能用到。7. 从RecastDemo到Unity、Godot这套技术在哪都能落地如果你用过Unity的NavMesh你会发现它的参数面板上也有Agent Radius、Agent Height、Max Slope、Step Height这几个值跟Recast的几乎一模一样。没错Unity的内置导航网格底层从2018版之后改成了Honeycomb但核心思路和参数体系与Recast是一脉相承的。你在RecastDemo里调参的经验直接可以迁移到Unity里用。Godot引擎同样内置了基于Recast思想实现的NavigationServer它把导航网格的烘焙和查询分成两个层面运行时可以通过代码动态更新障碍物。Godot 4.x的NavigationMesh的cell_size、agent_radius这些属性熟悉吧换了层皮底层逻辑还是那套。至于UE5它的NavMesh是基于自研的Navigation System但如果你用过UE的RecastNavMesh会发现它的参数Cell Size、Agent Radius、Agent Max Slope跟Recast的如出一辙——官方文档直接承认就是受Recast启发的实现。所以说学会RecastDemo你几乎是同时掌握了三大引擎的导航网格调参思路。以后再遇到“角色爬不了斜坡”“NavMesh生成不出来”“动态阻挡没反应”这类问题你脑子里会有清晰的排查路径而不是一头雾水地各个论坛翻帖子。我个人的体会是寻路系统这种东西直接上手跑一遍Demo比单纯看文档有效十倍。RecastDemo的代码量并不大界面也不算精致但它是把一套复杂的工程系统“可视化”出来的典范。花一个周末的时间把每个按钮都点一遍、每个参数都调一遍、每个Sample都构建一遍你对NavMesh的理解会超过刷十篇博客。顺手把Detour的路径查询接口也跑通那么在Unity、Godot、UE里写寻路逻辑你都心里有底。
返回列表