原文链接
专业术语
tile
我认为最好的翻译是区块,一般都翻译成瓦片,实际上指地图上的一小块方形区域。这里不做翻译
path distance
路径距离,标量,描述某个tile到达终点的路径长度。文中tile的左上角标示了这个值的大小
原文开始
这个教程中我会解释向量场寻路(vector field pathfinding)以及它对比其他传统的寻路的优点,比如Dijkstra's算法。对于 Dijkstra's算法和势场(potential fields)概念的理解可以帮助你更好的理解本文,但这不是必要条件。
简介
寻路问题有多种方案,每种方案都有其优缺点。很多寻路算法为每一个寻路个体执行路径计算,这就意味着寻路开销会随着寻路个体成倍增加而增加。在多数情况下是可以接受的,但如果有几百个寻路个体的情况下,那么就需要考虑更加高效的方案。
向量场寻路方法计算从寻路的终点到图中的每一个节点的路径。为了更充分地对向量场寻路的解释,我会用我个人的实现版本作为例子。
注:向量场寻路法可以一般化并运用在节点或者图上;这里我使用tile和grid并不意味这这个算法仅限于基于tile的世界!
视频预览
向量场算法由三个步骤组成。
1.生成热力图,用来确定地图中任意块/节点到终点的路径距离
2.为每一个节点生成指向到达终点方向的矢量域
3.每个正在搜索共享终点的粒子使用矢量域导航到终点
这个视频展示了最终的效果,为你展示完整教程的总体概念:
https://youtu.be/Bspb9g9nTto
生成热力图
热力图保存了从终点到地图上每一个点的路径距离。路径距离区别于欧式距离,它计算了2点之间能通过的地形的距离。这就好比是GPS总是只计算地图上能通过的道路的路径距离。
下图中你可以看出从终点(这里标记为红色)到任意一点(标记为粉色)的路径距离和直线距离的区别。不可通过的tile被染成绿色。你可以看到,路径距离(黄色)是9,直线距离(浅蓝)大约是4.12。
通过热力图生成算法,每一个Tile的左上角的数字显示了到达终点的路径距离。请注意2点之间有多条路径;本文中我们只关心最短路径。
热力图生成算法是一种波面算法(wavefront algorithm)。算法从终点开始,标记值为0,最后像波浪一样扩散,填满整个可以通过的区域。波面算法包含2个步骤:
1.算法从终点开始,用0标记终点的路径长度。
2.标记每一个未被标记的相邻tile,赋值为前一个tile的路径长度 + 1
3.继续算法之道地图中每一个可达的tile都被标记完成
注:波面算法是在grid上使用广度优先算法,并保存从终点到每个tile的路径需要花费多少步。这个算法有时也被称为森林火灾算法。
生成矢量场
既然每个tile到达终点的路径长度已经计算完毕了,我们可以很简单的确定如何接近终点的路径。虽然可以在运行时对每一个寻路个体每帧进行计算,但是更好的方式是对一个向量场仅计算一次,然后所有的寻路个体在运行时参考向量场。
向量场简单地在每一个tile中保存一个向量,这个向量指向了下一个低梯度的距离。这里是向量场的图示,包含了从tile中心沿最短距离指向终点(红色标识)的向量。
这个向量场是通过查看热力图逐个遍历tile生成的。向量的x和y的分量分别计算,下面是伪代码:
Vector.x = left_tile.distance - right_tile.distance Vector.y = up_tile.distance - down_tile.distance
注:每个tile保存了到达终点路径值的变量,该变量是通过上面描述的波面算法得到的。
如果任何一个被引用的tile是不可行走的,那么就不会在tile内保存一个可用的距离值。当前tile的距离值使用缺失值代替。
一旦路径向量大致计算完成后,向量需要被归一化以避免不一致性。
寻路个体移动
既然向量场已经计算完毕了,那么针对一个寻路个体的计算就相当简单了。假设函数vector_field(x,y)返回一个我们之前在tile(x,y)中已算好的向量值,并且期望速度矢量desired_velocity (寻路个体的到达目标需要的速度矢量)是个标量,那么对于一个粒子个体来说,计算速度矢量的伪代码大致是这样:
velocity_vector = vector_field(x, y) * desired_velocity
粒子仅需要朝着向量场提供的矢量方向开始运动即可。这是最简单的办法,但更复杂的移动系统可以使用flow fields来实现。
例如,Understanding Steering Behaviors文中的技术可以被应用到寻路个体移动上。这种情况下,我们上面计算得到的velocity_vector 可以用做期望速度矢量,并且转向行为(steering behaviors)可以在每个时间片中被用来计算实际的运动。
局部最优(Local Optima)
当计算运动时,有时会产生一个问题,这个问题被称作局部最优。给定某个tile,当存在2条最优(最短)路径时会出现此问题。
这个问题可以用以下图表示。墙壁中心最左边的那块tile(粉色标记)的路径向量x和y的分量值都是0
局部最优会导致寻路个体卡死;寻路个体用来参考的向量无法指明正确的方向。当这个情况发生时,除非找到修复问题的办法,非原则寻路个体将一直停留在同一个位置。
解决这个问题最优雅的方法(我目前所找到的)是同时对热力图和向量场进行一次细分操作。这样每一个热力图和向量场都被分成4个更小的区块。不过这个问题在细分的网格里依然存在;只是问题被稍微最小化了而已。
真正解决局部最优问题的技巧是:在初始阶段,加入4个终点节点,而不只是一个。为了实现这个目标我们只需要修改第一步的热力图生成算法。之前我们仅加入一个路径距离值为0的终点,现在我们加入最接近目标点的4个tile。
选出4个tile有好几种方法,他们是被如何选择的不是重点。只要这4个终点相互邻接(并且可达),这个技术就管用。
这里是经过修改的热力图伪代码:
1.算法从4个终点tile开始,并把这全部4个终点的路径距离标记为0。
2.然后,取每个标记过的tile的相邻节点,并把他们的路径距离值标记为 前一个tile的路径距离值 + 1.
3.继续这个过程,知道所有图中可达的节点都被标记完成
现在,我们得到了最后的结果,下图清晰的展示了局部最优问题被修复了:
虽然这个解法很优雅,不过任然不理想。使用这个方法意味着计算热力图和向量场的时间将会花费原来的4倍因为tile的数量增长了。
其他解决方法是基于不同种情况进行检查和确定需要行走的方向,这将会明显降低粒子运动的计算速度。对我而言,对地图的细分是比较好的办法。
结论
希望这个教程能教会你如何实现基于tile的地图的寻路。请记住这种寻路的核心思想很简单:让粒子跟随距离的梯度函数朝着终点前进。
实现起来更加复杂,但可以拆解为下面的3个可实现的步骤:
1.热力图生成
2.向量场生成
3.粒子移动
我希望各位可以扩展这个思想。像以前一样,如果你有任何问题,请毫无顾虑地在留言中提问!