JPSPlusGoalBounding寻路算法整理

算法简介

算法链接:https://github.com/SteveRabin/JPSPlusWithGoalBounding

使用场景:2D格子寻路

优点:比A*快上百倍,对于没有障碍的直线可以由起点直接跳到终点(高效的原因)

缺点:需要预处理,不支持动态改地形碰撞(无法通过的格子)

潜规则:斜方向可以走的前提是,斜方向两个分量上的格子都要能走(即要走左上角,左边和上边都得是非碰撞)

题外话:理解这个算法前,最好先学会A*,因为JPS相当于是优化后的A*

跳点的定义

要理解JPS,首先得清楚该算法中跳点(也可以叫跳转点或者转折点)的含义。在代码中对跳点的定义是这样的

bool PrecomputeMap::IsJumpPoint(int r, int c, int rowDir, int colDir)
{
	return
		IsEmpty(r - rowDir, c - colDir) &&						// Parent not a wall (not necessary)
		((IsEmpty(r + colDir, c + rowDir) &&					// 1st forced neighbor
		IsWall(r - rowDir + colDir, c - colDir + rowDir)) ||	// 1st forced neighbor (continued)
		((IsEmpty(r - colDir, c - rowDir) &&					// 2nd forced neighbor
		IsWall(r - rowDir - colDir, c - colDir - rowDir))));	// 2nd forced neighbor (continued)
}

如何理解这段代码呢,其实无外乎两种情况,首先跳点是一个可非碰撞格子,假设我往上走,那么我正上方那一格是跳点,仅当我当前点和正上方可以走,并且(我右边是碰撞,右上角是可以走)或者(我左边是碰撞,左上角可以走),那么我正上方就是跳点,并且跳点的方向是往上。没错,跳点是带方向的,你可以把刚说的情形画下来,转几个方向,模拟一下从左往右,从右往左,从上往下,就能知道其他几种情况。如下第一张图,星星格子的跳点方向有4个,可以认为它是4个方向的跳点。而第二张图,它只有上和左两个方向。

预处理

首先需要有一个数据结构存储地图上每个格子8个方向距离碰撞或跳点的距离。这里我们只讨论两个方向,一个直的和一个斜的,其他方向同理。

假设我们要计算每个格子距离左边碰撞或跳点的距离时。首先碰撞是不需要存距离的,所以值直接是0即可。碰撞右边第一个非碰撞点值是0,第二个非碰撞点值是-1,第三个非碰撞点值是-2,以此类推用负数记录离碰撞的距离,至于为什么第一个非碰撞点是0不是1,是为了后面的非碰撞点找到这第一个非碰撞点,而不是直接找到碰撞,所以严格意义上是距离碰撞格子第一个非碰撞格子的距离。当遇到左方向的跳点时,跳点右边第一个非碰撞格子值是1,第二个是2,第三个是3,以此类推用正数记录距离跳点的距离

假设要计算左上角的值,如果我当前点处在左边或上边边界,或者左上角点是碰撞,值为0。意味着左上角是碰撞,我是碰撞后的第一个非碰撞格子,假设右下角也是非碰撞格子,那么它就是第二个非碰撞格子,值为-1,第三个是-2,同上面一样用负数记录离碰撞的距离。当左边和上边是非碰撞时并且左上角是上或左的跳点时,值为1,即左上角为跳点,假设右下角也是非碰撞格子,那么它就是第二个非碰撞格子,值为2,第三个是3,以此类推用正数记录距离跳点的距离

代码见PrecomputeMap.cpp的CalculateDistantJumpPointMap方法

 

GoalBounding

它是预处理的一部分,用一个数据结构存储每个格子8个方向最远到哪,目的是当检索到当前格子时,如果想从该格子往某个方向走,可以拿到往这个方向最远能走到的矩形区域,可以看看目标格子在不在这里面。不在的话,这个方向可以直接丢弃。但是,这个预处理特别费时间。记得当时跑200*200的地图跑了半个小时这个预处理,复杂度乎是8(n^2),n是地图地图格子数量。幸运的是,即使没有这个处理也不碍事,少了一些剪枝而已,可以看懂整个算法后自行去掉这个部分

运行时预处理

上面两部分说的都是离线预处理,运行时会先预处理每个格子8个方向是否碰撞

 

寻路部分

也就是代码JPSPlus.cpp部分,SearchLoop就是典型的BFS搜索,就是先找一遍8个方向,然后优先队列(优先级越高越可能到达终点,启发式搜索)遍历队列里每个格子,直到找到终点或队列为空的套路。唯一不一样的就是,搜素规则了。

一开始先引入了cases.h,先说说它的作用,它记录了2048个函数名(丧心病狂地撸了2048个函数),2048也就是2的11次方,也就是总共有11个比特位可以用,高八位用来记录当前格子8个方向是否碰撞,低3位用来记录当前格子是上一个格子从哪个方向过来。这样就可以为当前格子制定最优的方向策略,例如不用走回头的方向或者是碰撞的方向。

寻路一开始先对起始格子8个方向搜一遍,之后的格子就用上述的方式剪枝

然后往各个方向的移动策略,这里只套路直走和斜着走两种方式的其中一个方向,其他的以此类推。假设是直着往下走,会直接跳到下方的跳点或者下方离墙最近的点(用到离线预处理的数据),将这个点加到队列,假设终点在这条路径上,则直接跳到终点,将终点加入队列。假设是往右下方走,如果终点不在右下方,则仅当右下角还有跳点才将右下角跳点加入队列。如果终点在右下方,则判断能不能斜着跳到和终点同行或同列的位置,如果这条路线上有其他跳点,则认为不能需要将跳点加入队列,如果能的话,将跳到的位置加入队列(这样下次就能直着到达终点)。

加入队列时,需要记录上一个源点(即从哪边过来),需要根据到终点的距离用启发函数算出一个值用于优先队列排序的优先级,并且计算这条路径开销,上一个源点的路径开销累加上这两个点之间距离,就是起始点到这个格子的开销了,下一次如果这个格子又被加入队列,如果比较下来路径开销更优的话,则用新的路径

 

结尾

寻路逻辑部分到这里就说完了,然后讲一些当时事后一些工作。

虽然这个支持8方向,但斜着的限制太大,改一改能支持不用两个分量都是非碰撞,一个非碰撞或者直接斜着走都行。这么一改的话,那2048个函数就得改一改策略,总结下规律之后,自己写个生成器生成就好了,似乎他也提供了生成器。

平滑处理,这个算法最终生成的路径,其实大部分是一堆跳点,然后它又把这堆跳点补了中间点。但事实上那些跳点舍弃掉一些点后,依然是可行解并且更加平滑,不会出现折来折去的情况。所以采取的策略是这段路线两端一起平滑,先拿第一个点也就是起点作为固定点,然后在第二个点和终点间二分,直到找到一个即跟固定点间无碰撞并且尽可能靠后的点,将两点间的其他点舍弃,这样这个找到的点就成为第二个点了。然后拿终点作为固定点,在第二个点和倒数第二个点中找一个即靠前又跟终点是通路的点,舍弃掉找到的点与终点间的其他点,作为倒数第二个点。接着第二个点作为固定点,在第三个点与倒数第二个点中找。。以此类推,最终平滑的效果还不错,不过比较麻烦的是找出两点间经过的格子。

后面又支持了运行时动态维护,可以动态加减碰撞,改动量和脑洞略大,作为公司知识财产,不予细说。只能说需要运行时动态改跳点距离,一边寻路一边维护和计算真实的跳点距离,而不是加完碰撞,立马都计算出正确跳点距离。 然后还有连通性问题要考虑,因为离线处理,可以人为保证整个地图是连通的,也就不用急着去考虑连通性。当时连通性直接用的最粗暴的并查集去搞的,并查集的复杂度等价于地图大小,好在需求上增减碰撞不是特别频繁,所以还能接受

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值