--- title: 使用导航网格实现寻路 date: '2023-09-12' tags: ['Three.js', 'Navigation Mesh', '3D', 'Game', 'Path Finding'] draft: false summary: 本文结合 three-pathfinding 项目的源代码,简要分析如何使用导航网格实现寻路功能。 three-pathfinding 是一个 Three.js 导航网格寻路库。 images: ['https://s3.ivanli.cc/ivan-public/uPic/2023/mZ9HNo.jpeg'] --- 本文结合 [donmccurdy/three-pathfinding](https://github.com/donmccurdy/three-pathfinding) 项目的源代码,简要分析如何使用导航网格实现寻路功能。 three-pathfinding 是一个基于 [PatrolJS](https://github.com/nickjanssen/PatrolJS) 实现的 Three.js 导航网格寻路库。 在 Three.js 中使用导航网格实现寻路功能,主要用到以下三个部分: - 导航网格(Navmesh, Navigation Mesh):寻路用的地图 - A\*(A Star)搜索算法:用于寻路 - 漏斗算法(Funnel Algorithm): 用于在二维平面找到绕过障碍物的最短路径 ## 导航网格 导航网格由若干个可供角色行走的、相邻的凸多边形组成,在我们的用例中,是三角形。导航网格的作用是寻路算法提供所需的顶点数据的,本身并没有任何算法。导航网格可以通过 [UPBGE](https://tl.ivanli.cc/m/28) 生成,生成后就是一个 Mesh 物体,所以顶点数据可以直接通过 GLTF 等格式分发。 ## A Star 搜索 网络上有很多文章介绍这个算法: - [A Star Algorithm 总结与实现 | Cheng Wei's Blog](https://shiori.ivanli.cc/bookmark/47/archive/) 本文结合实际应用再简要地说明下。 首先,我们除了会给算法传入三角形的定点数据,还会传了起点和终点,这里分为三个情况: #### 起点和终点顶点都在同一个三角形之中 不需要执行 A Star 搜索算法,直接将两点用直线连接就是目标路径。 #### 起点或终点不在任意一个的三角形之中 要么通过其他算法将原始的起点和终点在三角形中找到最接近的点,要么放弃这次寻路。因为这是不可处理的意外情况。 #### 起点和终点在不同的三角形之中 这样就能正式执行 A Star 搜索算法了。 ### 数据准备 [Builder.\_buildNavigationMesh()](https://github.com/donmccurdy/three-pathfinding/blob/538b57b65e78b37fe033ff845a09659ebed426dd/src/Builder.js#L150) 方法将从 NavMesh 提取所有三角形的顶点,并通过三角形的三个顶点的 x 分量作为 ID,找到了每个三角形相邻的三角形数组。 [Build_buildPolygonGroups()](https://github.com/donmccurdy/three-pathfinding/blob/538b57b65e78b37fe033ff845a09659ebed426dd/src/Builder.js#L103) 方法将从上一个方法中返回三角形及其相邻的三角形作为一个参数,返回了若干组三角形,每组内的三角形都能相互联通,也就是在这组三角形上的任意两点都能找到连接的路径。 最后每个三角形会被结构化成 Node,长这样: ```javascript { id: number, // ID neighbours: nb[], // 相邻的三角形的 id 数组(Node.id 数组) vertexIds: vertexIds[], // 顶点 ID,使用三个顶点的 X 分量组成 centroid: Vector3, // 重心 portals: [number, number][] // 与其他三角形公用的边的顶点的索引数组, // 一般情况下是两个,即一个边与另一个三角形相邻,但是也不排除会三个边都相邻 // 如果三边都相邻,则是 [number, number, number] } ``` ### 开始使用 A Star 搜索路径 A Star 算法怎么跑的本文就不赘述了。 执行 [Pathfinding.findPath()](https://github.com/donmccurdy/three-pathfinding/blob/abc331195143d7ea1242debed4b52500bda8b7fe/src/Pathfinding.js#L106) 时,需要传入 zoneID 和 groupID。通过前面对数据准备的分析,我们知道同组的三角形是相通的,只要保证起点和终点都在这组三角形的面上,正常情况下 findPath 就能求出路径了。 A Star 算法本质上是在若干个点之间求出一组点,连接这些点就是导航路径。这里使用三角形的重心作为这个点。 通过 A Star 算法搜索出路径后, 会获得一组有序的 Node。 ## “拉绳” A Star 算法寻得的路径是比较粗粒度的路径,接下来使用漏斗算法来拉出一条最短路径。 [Channel.js](https://github.com/donmccurdy/three-pathfinding/blob/364fdc5e6c41c6f3835d881edd00565c45ab0401/src/Channel.js) 里便是使用漏斗算法来获取最短路径。值得注意的一点是,这个算法适合平面,并不适合有高度落差联通的导航网格。 漏斗算法参考这篇文章:[图解NavMesh寻路中的漏斗算法 - PointerSMQ - 博客园](https://shiori.ivanli.cc/bookmark/48/archive/) ![](https://s3.ivanli.cc/ivan-public/uPic/2023/JJjp9f.png ' =666x428') 漏斗算法能够让最终路径在绕过障碍物的同时,保证路径最短。从上面的文章中,可以总结一个核心逻辑,每次生成的路径如果超过左边界或右边界,就会增加一个节点,并从此处构造新的漏斗。直到到达终点。