开放大世界的寻路一直是很困扰我的一个点,地图大、还是动态可变的,所以寻路会有很多要求。就我们项目来讲,有这几个要求:
- 能满足极大范围的地图;
- 地图寻路数据能实时构建,且重建代价很小;
- 寻路的效率要极高,客户端服务器都要大量计算;
- 寻路的路径客户端可预测;
根据上面的需求,像 NavMesh、JPS+ 虽然效率高,但是重建代价大;用 A* 重建代价小但是效率也太差了;而 FlowField 又不支持多目标寻路。最后,我根据项目本身的需求,设计了一套适用于航海主题的海洋+岛屿的寻路解决方案。
这个方案能够满足项目要求,且只有一个 O(1) 的复杂度,还可以分布寻路,地图的重建开销也很低。当然,这里对岛屿、地图设计做了一些限制,同时是针对航海特化的方案。
1、名词说明
地图数据上,采用格子来构建地图数据,可以参考 开放大世界的数据管理-CSDN博客 ,对寻路来讲,较为核心的有以下几个:
- Cell : 寻路的最小单位(也就是一艘船的占地)。
- Chunk:最小的岛屿,也是一个Block的最小单位。
- Block :寻路块,等价于一个岛屿,会包含岛上的阻挡、寻路等信息。
- AOI Block : AOI 块,用于服务器、客户端的数据同步(对寻路本身作用不大,但是会影响地图设计)。
在岛屿设计时,不允许任何一个岛屿跨越两个AOI Block,这样能客户端只需要加载有限的几个AOI块就能保证寻路正确。
2、寻路步骤
以下面的地图举例:
上图中最小的格子是一个Chunk (最小岛屿),其中橙黄色的部分是岛屿(一个寻路的Block),有大有小。玩家需要从A点(红色块)移动到B点(绿色块),其寻路部分如下:
2.1、两点直线并栅格化
起点和终点直接做直线,作为初始的两个点。
之后将直线进行栅格化,找出途经的所有Chunk点,然后将这些Chunk点所在的岛屿进行查询,看是否属于某个岛屿。
如上图灰色块即为栅格化的Chunk路径,黄色块即为各个岛屿的入口、出口。
之后计算直线与岛屿的交点,计算入口和出口。判断如果存在出口,即离开了这个岛屿,就可以采用流场寻路。
2.2、穿岛/出岛,流场寻路
航海大世界的岛屿有一个强制规则:
所有的岛屿最外面的一圈Cell都是可行走区域(海面)。
这种设计,保证不会出现绕过岛屿的情况,且所有岛屿都可以穿越。在穿越岛屿时,采用流场(FlowField)进行处理,如下图所示:
上图是从下方往上走时的流场 G 值表,表示到达目标点的代价。在寻路时,先往 G 值小的地方走,当走到 G 值为0的地方则表示已经到达边缘,然后将边缘出口点作为起点,继续向终点拉直线,寻找下一个穿过的岛屿。
在实际的数据处理中,不会保存G值,而是会预处理出每个Cell附近G值最小的格子。因此在 rumtime 寻路的时候直接根据预生成的数据,依次行走即可(不需要再进行计算)。
流场寻路输入出口方向以及当前起点,然后根据当前起点的父节点不断遍历,直到走到无父节点的边缘。
2.3、当终点在岛内时,JPS+
当终点在岛屿内部时,采用JPS+的方法进行计算。
由于A*寻路过程中会产生多余的节点被放入OpenList,导致OpenList过大,性能消耗过高。 JPS算法就是对A*的一个改进。JPS事先寻找到地图中可能的跳点,之后仅使用跳点进行A*运算。而JPS+算法则是先离线生成每个格子8方向到跳点的距离,运行时可以加速计算。
关于算法,可以参考文章:KillerAery_JPS/JPS+ 寻路算法
3、方案特点
此方案除了效率高之外,还有以下特点:
-
可以分段寻路,即不用建立所有的路径点仍然可以行走
-
迁城不影响大的路径:因为一个城只占用一个Chunk,玩家迁城不会影响栅格化的路径,服务器只需要部分重新计算而不需要全部重算。
-
即便不知道完整数据,客户端仍然可以预测出路线。(因为第一段路线一定是起点到终点的直线,直到遇到第一个岛)局部寻路与整体寻路结果相同。
-
容错率较高,只要服务器能正确同步动态岛屿(只需要目标船只附近的)的信息,即便是完全没有服务器的寻路数据,客户端依然可以模拟正确的完整路径。
4、通用开放世界寻路的思考
其实在开发过程中,就是把整个大世界进行了分块,且确保了各个分块之间能互相联通。这样,在最上层的寻路就可以直接走直线而不必关心每一个格子的阻挡。
抽象来看,即便是非航海主题的开放大世界,也可以用这个思路来设计寻路。
![](https://i-blog.csdnimg.cn/direct/d5d078e6fa494408941c0b599121312f.png)
现有主机游戏的开放世界 3A 大作,其地图可以明显看到设计过的痕迹。各个区块之间有明显的区分,这就给全局寻路创造了可能。只要在地图设计上,每个区域可以互相联通,且还有若干条大路贯穿整个地图,就能很方便地实现全局寻路。
对于区域而言,其邻接的区域是有限的几个,可以提前算好连接路线。而在地图内部,可以直接引导寻路至道路上,然后沿着道路前进就行了。