对于单个单位的寻路可以使用A*算法。但是在实际应用中往往出现多个单位同时移动的场面,而且它们会互相影响,阻碍对方的移动。所以一旦冲突,之前为每个单位计算出的路径就会失效。
一种流行的解决方法是发现冲突的时候重新计算路径。还有定期重新计算的等等。这些都是动态调整的方案,最后形成的路径并非是最优的。虽然这些次优解可以满足大部分场景的需要,但是有没有办法计算出最优解呢?毕竟很多动态调整的算法会有失败的可能。
我们可以把一个最小时间单位(回合)中每个单位的可能移动的组合列出来作为节点,放入搜索树中,然后再用A*算法进行搜索。但是在方格地图中一个单位可以有最多5或者9(带斜线)种不同的移动方法,其中包括静止不动。2个单位就有5x5或9x9种组合。随着要考虑的单位数的增加,组合数会爆炸。这时我们可以把一个回合再分解到一系列单个单位的移动,把单个移动作为节点。这样一旦发现某些条件不满足,比如两个单位有冲突,就可以关闭当前节点,不继续生成组合,从而实现对搜索树进行剪枝。由于一个回合中各单位的移动实际并没有优先次序,而我们现在的处理是一个单位一个单位进行,所以先被处理的单位不需要检查和后面单位的冲突,只有后面的单位需要检查和前面单位的冲突。当然为了成功剪枝,优先处理哪些单位也是值得考虑的。可以按某种优先级排序,比如越接近目标的可以优先处理。
举个例子,假定一个2x2的地图,有两个单位A在坐标1,0处,B在坐标0,1处,都想要移动到坐标1,1处。那么我们按先处理A后处理B的次序生成节点。假定[]表示计划的移动列表,{}表示未计划的单位列表。那么搜索树可以表示如下:
[]{A10,B01} //初始状态
/ \ \
[A向下]{B01} [A不动]{B01}未展开 [A向左]{B01}未展开
/ | \
[A向下B向右]{}冲突,剪枝 [A向下B不动]{} [A向下B向上]{} 未展开
|
[]{A11,B01} -> []{B01} 回合1结束,结算位置,A到达目的地,被移走,然后进行下一轮。
/ \ \
[B向右]{} [B不动]{}未展开 [B向上]{}未展开
|
[]{} 回合2结束,结算位置,B到达目的地,被移走。列表为空,达成目标状态。
另一个优化是,虽然我们可以生成一个单位的所有移动方法,但是我们并不急于把所有的移动都作为节点放入open list中,而只选1~2个最有可能成功的。当前节点并不关闭,并且维护一个列表,记录有哪些移动已经展开过了。这样可以有效减少open list中节点的数量。假定<>为未展开的移动,上面的例子可以用如下表示:
[]{A10,B01} //初始状态 -> []<A不动, A向左>{}
/
[A向下]{B01} -> [A向下]<B向上>{}
/ |
[A向下B向右]{}冲突,剪枝 [A向下B不动]{}
|
[]{A11,B01} -> []{B01} -> []<B不动,B向上> 回合1结束,结算位置,A到达目的地,被移走,然后进行下一轮。
|
[B向右]{}
|
[]{} 回合2结束,结算位置,B到达目的地,被移走。列表为空,达成目标状态。
关于A*搜索的启发函数,我们当然可以求出每个单位的manhattan距离、diagonal距离或者duclidean距离,然后求和作为每个节点的启发值。更好的启发值可以用RRA*(Reverse Resumable A*)算法来获得。也就对目标点到当前点执行一次普通A*搜索,来获得(不计移动单位的障碍)的实际距离。这个距离比前面提到的估算算法要准确得多。RRA*搜索生成的open list是可以重用的,实际上开销并不大。当然对多个目标点,我们需要维护各自的open list。
具体实现可以参考github.com/silmerusse/fudge_pathfinding中的例子。对于相对简单的地图和较少的单位,最优解是可以很快求出的。但是对复杂度高的地图和较多单位,则需要花费较多时间和内存空间。