预运算地图寻路的一种方法

云风 2021-01-05 15:57

上次谈到服务器上寻路算法的优化。文末提到,其实,针对具体需求,我们实际上可以预运算所有的路径,把结果持久化在文件中,运行时用 O(1) 的时间就可以查询到任意路径。

对于半径为 N 的地图网格,选取任意两点作为起点和终点,一共有 N^4 数量级的路径,直接按起点和终点来缓存路径显然不现实。

但实际上,除非整张地图是个复杂的迷宫,大部分路径是重复的。这和人实际上行路是一样的。如果你从家中的卧室去到办公室的工位上,整条路径和你从客厅出发是高度重叠的;仅有出家门前的那一小段略有不同。

在服务器上寻路,通常解决的是 NPC 的小范围内移动绕开障碍(从卧室到家门)的问题,远距离移动的路线(从家到公司)应当在另一个层面去解决。

基于网格寻路无论是用 A star 还是基于图的 dijkstra 算法都非常简单,这里不谈算法,只谈结果的持久化问题。

为了解决起点和终点有 N^2 种可能性,这个数量级太大的问题,我们可以适当的将地图划分成若干区域。如果每个区域都是一个内部没有任何障碍物的凸多边形,那么,当我们的起点和终点都落在同一个区域时,连接两点的直线就是最短路径。

如何划分区域,不是本文的重点。这里只简单说两句。如果障碍物基本上是轴对齐的,那么简单的贴着障碍线对分切割就好了。就是对地图不断的做二分的过程,空间划分用一个 KD tree 划分即可;如果障碍线不是轴对齐的,那么可以用 BSP 树做空间凸集的划分。

现在,我们假设地图已经划分为多个凸多边形了。如果我们的划分不算太畸形(避免出现细细长长的区域),那么基于这个划分方案,我们可以得到一个较优的路径。方法是这样的:

当起点和终点不在同一区域时,首先,我们找到起点到终点所属区域的轮廓任意一点的最短路径。即,最快靠近目标区域;然后,从目标区域的轮廓上的那一点直线通达目的地。

只要区域划分得当,这条路几乎是最优的。即使不算最短路径,也是非常好的结果。

如何找到一点到一个面(轮廓线)的最短路径?我曾经写过一篇 blog 谈我们实际使用过的一个算法

简单说就是,我们在网格图上先把目标区域的格子都涂上 0 ,然后把所有上色的格子的邻近格都加 1 ,逐步把整张图都填上数字。最终,没有数字的格子就是无法抵达的,而每个格子上的数字就是距离那个区域的最短路径的距离。

这张图可以视为一个流图,数字看作高度,当我们处于某个非 0 的格子时,只要向临接的格子中数字最小的走,就能最快的抵达最低点 0 ,整个过程就是最短路径。

任意一个区域中的任意一点到另一个区域的最短路径就可以储存在这么一张图中。图上每个格子都只需要记录一个方向。如果是四边形网格,就是 8 个方向加一个不可抵达标志,一共是 9 个状态,用 4bit 表示即可。当然也可以利用 8bit 记录更多的方向信息。

如果我们只处理划分好的区域中,每个区域的附近(直线距离)的有限个区域,那么就可以用一个非常紧凑的数据结构来保存所有的信息 。注意,临近区域未必是紧挨着的区域,只要空间直线距离不远都算。

首先,我们记录下地图上每个格子自己数据几号区域。然后在区域结构中保存下每个区域附近有那些区域。每个格子中额外记录到附近每个区域最短路径的方向(或不可抵达)。

运行时,我们只要查到目的地所归属的区域,然后在出发格上查询到对应区域的行动方向就可以移动了;或者在同一区域时直接直线移动。每来到一个新的格子时候就重新做一次查询,这个查询是 O(1) 的,且可以共享个服务器所有单位同时使用。这非常适合障碍是静态的,而目的地在不断运动(比如追逐一个移动的对象)的场合。

[返回] [原文链接]