资讯动态

MMORPG 服务端寻路:基于 Unity 原生 NavMesh 数据的架构设计

发布时间:2026/10/21 10:39:49 来源:尧图企业网站定制
核心思路Unity 烘焙出的NavMeshData是私有二进制格式服务端无法直接读取。可行的路线是编辑器侧用公开 APINavMesh.CalculateTriangulation()取出可行走三角网经过清洗、补充信息后导出为自定义的服务端导航文件。服务端侧加载这份文件构建多边形邻接图 空间索引提供 A* 漏斗寻路、射线检测、贴地、随机点等查询。工程侧解决 MMO 特有的问题包括多实例共享、并发调度、客户端与服务端一致性、动态阻挡和超大地图。一句话概括Unity 负责烘焙工具链负责翻译服务端负责查询。一、总体分层┌──────────────────── ① 离线工具链Unity 编辑器 ────────────────────┐ │ 烘焙 NavMesh → 导出三角网 → 清洗修补 → 补充连接点/区域 → 切 Tile │ │ → 写入 .nav 文件版本号 哈希 → 一致性校验 │ └──────────────────────────────┬───────────────────────────────────┘ │ 随服务端配置一起发布 ┌──────────────────── ② 导航数据层只读、共享 ──────────────────────┐ │ NavMeshData顶点 / 多边形 / 邻接关系 / 连接点 / 区域 / 空间索引 │ │ 每张地图只加载一份所有分线、副本共享 │ └──────────────────────────────┬───────────────────────────────────┘ ┌──────────────────── ③ 查询核心层每线程一份状态 ──────────────────┐ │ NavQueryFindPath / Raycast / FindNearest / GetHeight / │ │ RandomPoint / MoveAlongSurface │ │ QueryFilter区域代价、可通行标志门、桥、阵营通道 │ └──────────────────────────────┬───────────────────────────────────┘ ┌──────────────────── ④ 寻路服务层调度 ─────────────────────────┐ │ 请求队列 工作线程池 优先级 每帧预算 结果缓存 超时取消 │ └──────────────────────────────┬───────────────────────────────────┘ ┌──────────────────── ⑤ 业务接入层 ─────────────────────────────────┐ │ 怪物 AI 追击/巡逻/回归 · 玩家移动校验 · 技能位移/击退 │ │ 自动寻路含跨场景 · NPC 护送 · GM 工具 │ └────────────────────────────────────────────────────────────────┘二、离线工具链把 Unity 数据翻译成服务端数据这是整个方案中最容易被低估、也最决定成败的一环。服务端算法写得再好数据有问题也会出现明明连着却走不过去的情况。步骤要做什么为什么① 按 Agent 类型导出每种 Agent人形、大型坐骑、巨型 BOSS只加载对应的NavMeshSurface分别导出CalculateTriangulation会把当前所有网格混在一起② 焊接顶点按 1cm 精度量化去重原始数据中存在重复顶点会导致邻接关系断开③ 删除退化三角形删除面积接近 0 的三角形避免 A* 和漏斗算法出现数值问题④ 修补 T 形接缝顶点落在相邻三角形边上时拆分那条边Tile 边界处常见不修补会断路⑤ 合并凸多边形可选把三角形合并成凸多边形节点数减少 24 倍A* 更快路径更平滑⑥ 导出连接点遍历NavMeshLink/OffMeshLink起点、终点、是否双向、区域三角网中不包含这部分信息⑦ 导出区域代价区域索引 Navigation 窗口中配置的 Cost三角网只有区域编号没有代价⑧ 标记动态区域门、吊桥、阵营通道所在的多边形打上可切换标志服务端运行时按标志开关不需要重新烘焙⑨ 切 Tile大地图按固定大小如 64m × 64m切分并裁剪三角形按需加载、局部更新、控制单块规模⑩ 写文件 校验写入版本号、源场景哈希、Agent 参数随机抽取点对对比 Unity 与服务端的寻路结果防止客户端与服务端数据版本不一致建议把导出接入构建流水线场景或 NavMesh 一改自动重新导出并跑一致性校验不依赖人工点菜单。三、导航数据格式Header magic / version / sceneId / agentTypeId / contentHash agentRadius / agentHeight / maxClimb / maxSlope bounds / tileSize / tileCount Tile[] Vertex[] float3或以 tile 原点为基准的量化整数 Polygon[] 顶点索引≤ 6、邻居索引、区域、标志 Link[] 跨 tile 的边连接 OffMeshLink[] 起点、终点、半径、方向、区域 BVTree / Grid 空间索引用于点定位 AreaCost[] 区域 → 代价设计要点只读、连续内存加载后不再修改可以用内存映射多个进程甚至可以共享同一份物理内存用索引代替指针便于序列化、按 tile 加载和热更新预计算邻接关系离线完成服务端启动时不需要再遍历计算空间索引必不可少寻路的第一步是这个坐标在哪个多边形上没有索引就要遍历全部多边形。四、查询核心层4.1 数据与状态分离NavMeshData每张地图 1 份只读线程安全 ▲ 引用 NavQuery每个工作线程 1 份 ├─ 开放列表二叉堆 ├─ 节点池按多边形编号索引用代数标记避免每次清空 └─ 临时缓冲区 QueryFilter每次查询传入 ├─ 区域代价 └─ 包含/排除标志 实例级标志覆盖某副本的门已打开4.2 需要提供的查询查询算法要点主要使用者FindNearestPoly空间索引定位 投影到最近多边形所有查询的入口FindPath多边形图上的 A*边中点或入口点估算代价→ 多边形走廊怪物追击、自动寻路FindStraightPath漏斗算法Simple Stupid Funnel把走廊拉直成路点生成最终路径Raycast沿多边形逐个穿过遇到无邻居的边即为碰墙视线判断、直线移动可行性、技能位移MoveAlongSurface从当前多边形出发沿表面移动碰墙滑动玩家移动校验、击退GetHeight多边形内插值高度服务端贴地、多层地形RandomPointInCircle按面积加权随机选多边形再在多边形内随机取点怪物巡逻、刷怪点4.3 两条实现路线路线做法适合自研按上表自己实现代码量可控核心几千行服务端为 Java / Go / C# 等或需求简单转为 Detour把导出的多边形填入dtNavMeshCreateParams生成dtNavMeshC 服务端希望直接复用成熟的查询库转为 Detour 时需要注意顶点要量化到 tile 坐标系单个 tile 的多边形和顶点数有上限所以前面的切 Tile 步骤必须做。五、寻路服务层MMO 的并发问题单次 A* 只需要几十到几百微秒但一张地图同时有上千个怪物、几百个玩家问题在于请求量。5.1 异步化逻辑线程 寻路线程池 │ 提交 PathRequest ──────► 请求队列按优先级 │ 继续执行其他逻辑 │ │ Worker 取请求用自己的 NavQuery 计算 │ ◄────── 回调 / 结果队列 ──────┘ │ 下一帧取结果驱动实体移动逻辑线程从不同步等待寻路结果请求携带实体 ID 和序号实体已死亡或目标已变化时直接丢弃旧结果。5.2 降低请求量的手段手段说明优先级玩家请求 战斗中的怪物 巡逻怪物 回归每帧预算A* 限制最大迭代节点数未完成的请求下一帧继续切片寻路先直线检测起点到终点Raycast通过就直接返回不跑 A*追击场景命中率很高重新寻路节流目标移动超过阈值或间隔超过一定时间才重新寻路否则沿旧路径修正末端群体共享同一怪物群追同一目标由领队寻路其余跟随或在偏移位置上复用路径结果缓存固定点之间的路径NPC 之间、传送点之间缓存复用无人区域休眠没有玩家的格子中怪物不寻路5.3 长距离寻路自动寻路跨越整张大地图时A* 展开的节点会很多。采用分层寻路场景连接图场景 A ─传送门→ 场景 B ← 跨场景 └─ 区域图离线把地图划成若干区域预计算区域间的连通和代价 ← 粗粒度 └─ 多边形 A*只在相邻区域内精细寻路边走边算下一段 ← 细粒度六、多实例与动态阻挡6.1 多实例地图 1001 的 NavMeshData1 份 ├─ 分线 1 → 实例状态门 A 开、门 B 关 ├─ 分线 2 → 实例状态全部默认 └─ 副本 #8823 → 实例状态BOSS 房门关闭实例只保存一个多边形标志覆盖表位图或稀疏表查询时通过QueryFilter合并。内存开销极小。6.2 动态阻挡的处理层次类型处理方式门、吊桥、机关离线标记多边形运行时切换标志推荐零成本区域封锁技能结界、BOSS 战圈运行时按范围批量修改实例标志其他实体玩家、怪物不进入导航网格由移动层的局部避让简单分离力或 RVO处理真正的地形变化可破坏建筑需要局部重新生成 tile。从 Unity 导出的数据很难做到必须用方案 C见第九节七、客户端与服务端一致性这是 MMO 寻路中最影响体验的问题两端结果不一致就会出现拉扯、卡墙、怪物穿墙。原则做法数据同源服务端数据由客户端场景导出文件中记录哈希客户端登录或进入场景时上报哈希不一致则告警服务端权威服务端的结果为准客户端 NavMesh 只用于本地预测和表现怪物路径下发路点服务端算出路点列表后下发客户端只做插值不自己寻路玩家移动带容差校验客户端上报的位置服务端用Raycast/MoveAlongSurface检查上一位置到当前位置是否可达在容差内则接受否则拉回坐标量化网络传输和校验统一使用固定精度如厘米避免浮点误差累积高度由服务端决定服务端用GetHeight贴地多层地形桥上、桥下根据上一帧所在的多边形判断层级八、超大地图与内存按 Tile 组织地图切成固定大小的块按需加载只读数据进程内共享上百个副本共享同一份数据按 Agent 类型分文件大型坐骑等使用独立网格不需要时不加载热更新新版本数据加载完成后原子替换引用旧数据等待正在进行的查询结束后再释放。九、演进路线阶段目标内容1. 可用跑通单张地图导出三角网焊接、去退化服务端三角形邻接图 A* 漏斗同步调用2. 正确数据可信T 形接缝修补、连接点、区域代价、一致性校验工具、可视化调试3. 可扩展支持 MMO 负载线程池异步调度、切片寻路、节流与缓存、多实例标志覆盖、Tile 化4. 完善全场景覆盖分层/跨场景寻路、多 Agent 类型、热更新、监控耗时、失败率、队列长度5. 视需要彻底统一改为 Recast 重新烘焙客户端和服务端都使用同一份 Detour 数据支持 TileCache 动态阻挡十、需要提前想清楚的问题服务端用什么语言C 可直接复用 Detour其他语言建议自研或通过原生库调用。地图有多大、多少层决定是否必须切 Tile、是否需要处理多层高度。是否有可破坏地形如果有直接评估方案 CUnity 导出数据难以支持。玩家移动是客户端驱动还是服务端驱动决定移动校验的严格程度和容差设计。同时寻路的实体峰值多少决定线程数、每帧预算和缓存策略。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑