---
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')

漏斗算法能够让最终路径在绕过障碍物的同时，保证路径最短。从上面的文章中，可以总结一个核心逻辑，每次生成的路径如果超过左边界或右边界，就会增加一个节点，并从此处构造新的漏斗。直到到达终点。
