原文链接
简介:搜索区域
假设有某个人想要从A点到达B点,假设有一堵墙隔离了两个点,如下图,绿点是起始点A,红点是终点B,中间蓝色的长方形是墙。
你需要注意的第一件事情是我们把搜索区域分割成正方形网格。就像我们已经做的,简化搜索区域是寻路的第一步。这个特殊的步骤将我们的搜索区域简化为二维矩阵。每个矩阵中的元素代表着网格中的一个正方形,并且它的状态被标记为walkable或者是unwalkable。通过找出从A到B我们需要经过哪些正方形,我们找到路径。一旦路径被发现,我们的玩家将会从一个正方形的中间移动到另一个正方形的中间,直到抵达目的地。
这些中心的点被称为”nodes”.当你在其他地方阅读寻路算法时,你经常会发现人们讨论nodes.为什么不直接叫它们正方形呢?因为我们可能把搜索区域分割成其他的形状,比如六边形,三角形等。并且nodes可以被放置在形状中的任何位置——在中间或者边界,或者任何地方。但我们把nodes放在中心,因为这是最简单的。
开始寻路
一旦我们简化了搜索区域,将其变为可以管理的一些节点,下一步是搜索并找出最短路径。我们可以通过从A点开始检查邻接正方形,并不断向外搜索来找到目标。
我们通过以下的方法开始搜索:
- 从A点开始,并把它加到一个正方形的open list中,这个open list有点类似于一个shopping list.现在在list中正有一个节点,接下来我们会拥有更多。open list包含了可能位于你最终路径上的正方形,也包含了可能不位于路径上的正方形。总之,这是一个需要被检查的正方形的list.
- 无视掉那些代表墙、水或者非法的地形的正方形,我们把可以到达的,可以行走的正方形加入到open list中,对于这次加入的每个正方形,我们把A点设为它们的父亲正方形。父亲正方形在我们追踪路径的时候会很有用。我们会在接下来讲解这个。
- 把A点从你的open list中扔掉,并把它加到你暂时不需要再看的closed list中。
现在你有了如下图所示的东西,在这个图示中,位于中心的暗绿色的正方形是你的起始正方形。它被添加了亮蓝色的轮廓线表示这个正方形已经被加到closed list中。所有的A点的邻接正方形此刻位于open list中等待检查,并且它们被添加了亮绿色的轮廓线。每个邻接正方形有一个灰色的指针指向它们的父亲,即开始的正方形。
接下来我们选择open list中的一个邻接正方形,重复上述过程。但是我们应该选择哪个正方形? 那个有着最低F代价的正方形。
路径得分
在寻路中决定使用哪个正方形的关键是决定下列等式:
F = G + H
其中
- G 代表从起始点A点,沿着生成的路径,到达一个指定的正方形的代价。
- H 代表从指定的正方形到达目的地B的预估的代价。这通常被称为启发式的,可能会给你带来一点疑惑。它被成为启发式的理由是因为这是一个猜测。在我们发现路径之前,我们并不知道实际的距离,因为各种各样的障碍物可能挡住我们的去路(墙啊,水啊,什么的..)。本次的教程中,你将使用一种方法来计算H,但你也可以在网上的文章中发现很多其他的方法。
我们通过不断地遍历open list并选择最小F分数的正方形来生成路径。本文会更详细的描述这个过程,现在我们先来看下怎么计算这个等式。
如前所述,G是从起始点,通过生成的路径,到达指定的正方形的移动代价。我们给每个水平或垂直移动的正方形赋予10分的代价,给对角线的移动赋予14分的代价。这是因为对角线的移动距离是2的开根号,大约是水平移动或垂直移动代价的1.414倍。我们为了方便,使用10和14。这个比例大致是对的,并且我们避免了小数,计算机运行得更快。
因为我们在一个具体路径上计算到达给定的正方形的G代价,计算指定的正方形的G代价也就是计算它的父亲正方形下的G代价加上10或者14(这取决于指定的节点到达其父节点是对角线的或者不是对角线的),在本例中,这个过程的重要性马上将会体现出来。
H可以通过各种各样的方式预估,我们使用的方法叫做曼哈顿方法。曼哈顿方法计算从指定的正方形到目的正方形所有水平、垂直的移动,无视对角线移动,无视任何可能的障碍。这个方法叫做曼哈顿方法(可能)是因为它像计算从一个城市街区到另一个城市街区的数量,其中你不能对角线穿过街区。
读了上面的描述,你可能猜测启发式只是从当前正方形到目的正方形剩余距离的粗略估计。实际情况不是这样。我们的确尝试估计路径上的剩余距离(实际情况通常更远)。我们的预测和实际剩余距离更接近,算法更快。然而,如果我们过多地估计了距离,我们可能不会得到最短距离。我们把这些例子叫做“inadmissible heuristic”.
严格来说,本次例子中,曼哈顿方法是不被接受的,因为它略微大于实际的剩余距离。但我们仍然使用它,因为这样更容易被理解,而且只是超过实际情况一点点。计算出来的路径大多情况都是最短路径,如果不是,那么它很接近最短路径。想要知道更多吗?你可以在这里发现等式和额外的注释。
F通过相加G和H计算得到。在我们搜索中第一步的结果如下图所示。F,G和H的分数写在每个正方形上。就像起始正方形右边的正方形所示,F写在左上角,G卸载左下角,H写在右下角。
让我们来看一下这些正方形。有些正方形中G=10,这是因为正方形只是起始正方形的一个水平移动或者是垂直移动。对角线的正方形中G的分数是14。
H分数通过预估到红色目标正方形的曼哈顿距离获得,只水平或者垂直移动而无视障碍物。使用这种方法,起始正方形右手边的正方形H的分数是30. 这个正方形上方的正方形是4个正方形远(记住,只水平或者垂直移动)。
F分数只是简单的G分数和H分数的相加。
继续搜索
继续搜索我们只需要选择open list中F分数最低的正方形。接下来我们对选择的正方形做下面这些处理:
- 把它从open list中丢弃,并且把它加到closed list 中。
- 检查所有的邻接正方形,无视位于closed list中的和unwalkable的正方形,把没有在open list中的正方形加到open list中。设置被选择的正方形做为新正方形的父亲。
- 如果一个邻接正方形已经在open list中,检查当前路径是否是更好的选择。换言之,检查G分数是否更低如果我们使用当前的正方形到达那里。如果不是,则不做任何事情。另一方面,如果新路径的G代价更低,那么设置这个邻接正方形的父节点为被选择的节点(在图示中,改变指针的方向指向被选择的正方形)。最后,重新计算那个正方形的F、G分数。如果听起来有点迷惑,你会在下面看到解释。
好了,让我们看看这是怎么工作的。在我们初始的9个正方形中,在起始正方形被移动到closed list后,我们有8个留在open list中。当然,最低F分数的正方形是起始正方形右手边的F分数是40的正方形。所以我们选择这个正方形作为我们的下一个正方形。在下图中它用蓝色高亮。
首先,我们把它从open list中丢弃,将其添加到我们的closed list中(这就是为什么它被蓝色高亮)。然后我们检查邻接正方形。好吧,这个正方形右手边的正方形是墙,所以我们无视它们。左右边的正方形是其实正方形,它位于closed list中,所以我们也无视它。
剩余4个正方形已经在open list中,所以我们需要使用G分数做为我们的参照点,检查到达这些正方形的路径是否比之前的更好。让我们看一下选择的正方形上方的正方形。它当前G分数是14,如果我们经过当前的正方形到达那里,G分数会等于20,很明显不是一个更好的路径。
当我们为所有已经位于open list中的4个邻接正方形重复这个过程,我们发现没有路径会因为经过当前的节点而变得更短,所以我们不改变任何东西。既然我们已经看过了所有的邻接正方形,我们就处理完了这个正方形,可以开始处理下一个正方形。
所以我们继续遍历open list,现在它只有7个正方形了。我们选择一个最低F分数的正方形。有意思的是,现在我们有两个分数是54的正方形。我们选哪一个?这不太重要。考虑速度的话,选择open list中的最后一个会更快。当你距离目标更近的时候,这会影响你接下来发现的正方形,但这不太重要。(不同的选择会导致不同版本的A*可能发现两个相同长度的不同路径。)
让我们选择下面的正方形,如下图所示。
这次,当我们检查邻接正方形,我们发现右边的是墙,所以我们无视它。我们也无视墙下面的正方形。为什么?因为你必须要绕过墙才能到达那个正方形——先往下走,再走到那个正方形上。(注释:这个绕过墙角落的规则是可选的,它取决于你的节点是怎么放置的。)
剩下来5个其他的正方形,当前正方形下方的两个正方形没有在open list上,所以我们需要把它们加到open list中,并设置父亲为当前的正方形。对于剩余的3个正方形,两个已经在closed list中,所以我们无视它们,对于最后一个正方形,当前正方形左边的正方形,检查其G分数是否从当前正方形过去比之前的更近。并没有,所以我们处理完了这个正方形,继续处理open list中的下一个正方形。
我们会重复这个过程直到我们把目标正方形加到closed list中,结果看起来像是下图这样。
注意距离起始正方形下方两个正方形位置的正方形,它的父亲正方形已经被改变。之前它的G分数是28,并且指向右上方的正方形,现在它的分数是20,并且指向它上方的正方形。这发生在我们搜索的过程中。虽然这个改变看起来在这个例子中不太重要,但有很多可能的情境下,这个检查会改变你的最优路径。
所以我们应该怎么得出路径?很简单,只需要从红色目标正方形开始,沿着箭头后退到它的父亲。这最终会带你回到起始正方形。这看起来像是下面的图示。从起始正方形移动到目标正方形只是简单的从一个正方形中心移动到另一个正方形中心,知道你到达目的地。
A*算法总结
好了,现在你已经看过了解释,让我们把每个过程都展示在下面:
- 把起始正方形添加到open list中。
重复下列过程:
- 在open list中寻找F代价最低的正方形。我们称这个正方形为当前正方形。
- 把当前正方形移动到closed list中。
遍历与当前正方形相邻的8个正方形:
a) 如果它不是walkable或者它已经位于closed list中,那么无视它。否则执行下面的步骤 b) 如果它不在open list上,那么把它添加到open list中。把当前正方形设置为它的父亲,记录下这个正方形的F,G和H的分数。 c) 如果它已经位于open list,以G代价为标准,检查到当前的到达那个正方形的路径是否更优。如果更优,改变那个正方形的父亲为当前的正方形,并重新计算那个正方形的G分数和F分数。如果你想要保持open list是按照F分数有序的,你可能需要重新排序open list。
然后在下面两种情况停止:
a) 将目标正方形添加进closed list,此时已经找到路径 b) 无法发现目标正方形,并且open list为空。这种情况下,没有可达路径。
保存路径。从目标正方形,沿着父亲正方形原路返回,找到起始正方形。这就是可达路径。
注意:在这篇文章早期的版本中,它建议在目标正方形被加入到open list的时候结束运行,而不是cloed list。这么做会更快并且它大多数情况下会给你最短路径,但不总是。我们需要考虑,当目标正方形被加入到open list之后,如果继续执行,它的父亲节点可能被改变(找到了更短的路径)。
吐槽
请原谅我的偏题,但我必须要指出,当你在各种论坛上看到各种关于A*寻路的讨论,你有时候会看到某些人把其它的算法当作A*算法。如果使用A*算法,那么你需要包含刚刚讨论的元素——特别是open list和closed list还有路径分数F,G和H。还有其他很多算法,但不是A*算法,而A*算法通常被认为是最好的。有时候其他的选择在特定的情形下会更好,但你需要知道你自己在做什么。好了..吐槽够了,让我们回到正题。