本文内容:介绍路径寻找算法的主力——A* 路径寻找方法。
A* 路径寻找算法
路径寻找是游戏软件AI 最基本的问题之一。A* 算法是当今游戏软件开中,相当常用的一种。A* 算法之所以会如此吸引人,是因为它可以保证在任何起点及任何终点间找到最佳的路径。
我们可以尽量使用 A* 算法,除了某些特殊情况的场景。例如,如果起点和终点之间没有障碍物,有明确的视线,那么视线移动算法即快速又有效,就没有必要使用 A* 算法了。如果CPU的功能不强,A* 算法会耗用不少CPU运算能力,尤其是,需要同时为许多游戏角色寻找路径的时候,A* 算法可能不是最佳的选择。
本章会循序渐进地讨论 A* 算法的内部运作过程,了解 A* 算法如何在起点和终点间建立路径。
定义搜寻区域
路径寻找的第一步是定义搜寻区域,我们需要以某种方式表示游戏世界,让搜寻算法能借此予以搜寻,并找出最佳路径。
A* 算法并不适合连续环境,因为连续环境可能是由大量的像素点构成的,因为点的数量太多了,会大大增加计算量和CPU的负担。
连续环境的搜索域简化:在游戏环境中放置节点,来简化搜索域。节点数量越少,路径寻找速度越快。不过这会牵涉到节点的置放还需要建立节点间的连接关系的数据清单。如图7-1 所示。
另一方面,砖块环境的游戏就是 A* 算法的合适对象。因为砖块环境本质上已经分成节点了,每个砖块就是搜寻区域里的节点。而且我们也不需要建立节点间的连接关系的数据清单,因为这些节点在游戏世界中都是彼此相邻的。·
砖块环境的搜索域优化:可以使用一个节点代替若干个砖块。搜索路径时,只搜索环境中的几个或一个大砖块,而不是搜索整个环境,如果在一部分范围内都找不到合理的路径,就认为没有合理的路径。如图7-2 所示。
总结:无论是连续环境还是砖块环境,如果节点数量太多,那么 A* 算法不切实际。但是通过简化搜索区域,A* 算法会变得可行。
开始搜寻
我们简化了搜索区域,使其由适当数量的节点构成时,就可以开始搜寻了。我们会以 A* 算法找出任何两节点间的最短距离。
此例中,我们用一个小型的砖块环境,每个砖块都是搜寻区域里的一个节点,而某些节点含有障碍物。我们运用 A* 算法找出最短路径,同时避开障碍物。例7-1 是 A* 算法的伪代码。
A* 算法的伪代码
//例7-1:A* 伪代码
把起始节点加进open list
while (open list 不空)
{
当前节点 = open list中成本最低的节点
if (当前节点 == 目标节点)
{
路径完成
从目标节点开始寻找其母节点,直到母节点是起始节点位置,得到路径
}
else
{
把当前节点移入到close list
检视当前节点的每个相邻节点
for (每个相邻节点)
{
if (该节点不在open list中 and 该节点不在closed list中 and 该节点不是障碍物) {
把该节点移进open list
计算其成本
记录该节点的母节点为当前节点
}
}
}
}
if (还没有找到路径)
{
无法从起始点到达目的地
}
例7-1 已经把 A* 伪代码 先给出了,暂时不理解不要紧。后面我们会循循渐进的讲解,直至理解。
图7-3 是我们要用的砖块搜寻区域。起点是蜘蛛,目的地是人类角色。黑白色代表墙体障碍物,白方块代表蜘蛛能走的地方。
A* 算法的思路
首先从起始结点开始搜寻,然后再分别去搜寻周围节点(扩散搜索,类似于从一点开始向外辐射)。
就此例而言,会从其实砖块开始,再扩散到相邻砖块,直到抵达目的地结点。但是我们需要某种方式,记录已经被搜寻过的砖块。使用 A* 算法时,通常把这种记录方式叫做 open list 。开始时,open list 只有一个节点,即开始节点,接着会陆续把其他节点加入进 open list 中。
建好 open list 后,接着检查 open list 内每个砖块相邻的砖块(相邻的是8个),然后把每个有效(可以行走的砖块)的砖块加进 open list 中。如果某砖块含有障碍物(即无效的砖块),我们会忽略,不会加进 open list。图7-4 显示了最初位置的相邻砖块,它们都需要做检查。
除了 open list 之外,A* 算法也要拥有一份 closed list。closed list 里的砖块就是已经被检查过的砖块,不需要再做检视了。基本上,当某砖块的相邻砖块都已检查过后,就会将该砖块加入 closed list。如图7-5所示,我们已经检查过起始砖块的每个相邻砖块,所以,起始砖块会被加入到 closed list 中。
如图7-5 所示,最后的结果是我们现在有八个新的砖块加入 open list,而有一个砖块(起始砖块)从 open list 中移除,加入到 closed list 中。
上述的基本上就是 A* 算法主循环的基本检查运算。然而我们还需要记录其他的某些信息。open list 中有相邻的砖块清单,它能供角色行走,但是我们需要知道这些砖块是怎么连接的。我们的做法是记录 open list 中每个砖块的母砖块(即该角色走到当前位置前的那个砖块)。如图7-6 所示,运行第一次循环时,每个砖块都会以起始砖块作为其母砖块。
最后,当我们抵达最终目的地时,就会利用此母砖块的连接关系,沿路径往回退到起始砖块。不过,抵达目的地之前,我们仍然需要多次反复运行循环。
此时,我们需要再次执行整个过程。现在,我们需要从 open list 中选出新的砖块进行检查。第一轮循环时,open list 上只有一个砖块,但现在清单上有八个砖块。终点就是要找出要检查的 open list 的某一成员。我们用来确认的方式就是替每个砖块打分。
记分
我们用路径得分,找出起始砖块和目的地砖块间的最佳路径。
我们替任何指定砖块记分公式是: f(n)=g(n)+h(n)
其中各个参数的意义:
- f(n) 是从初始状态经由状态n到目标状态的移动成本的估计函数
- g(n) 是在从初始状态到状态n的实际移动成本
- h(n) 是从状态n到目标状态的的移动成本的估计
(对于本文的砖块环境中,移动成本即是移动的步数,状态即节点或者说砖块。)
分析一下,g(n) 即 从初始砖块到目标砖块的移动成本,很好计算,因为我们从初始砖块移动到目的砖块的过程中(A* 算法是以一个节点向外辐射,因此会移动到相邻的八个节点,是实际移动),即可记下移动数,因此这个移动成本就是实际的移动成本。
但,如何求出 h(n) 呢?因为此时目的地砖块是最终目标,还没有走到。如果此时一个个的实际求,即按照路径走过去求实际成本,就丧失了 A* 算法的意义。因此,我们使用启发法(heuristic)来猜测 h(n) 的值。
基本上,我们分析游戏环境后,根据所有信息,就可以得到一个猜测的算法用以估算此值。举个例子:对于几何路网来说,可以取两节点间曼哈顿距离做为距离估计,即f=g(n) + (abs(dx - nx) + abs(dy - ny))
。
这里要说明的是:保证找到最短路径(最优解的)条件,关键在于估价函数 f(n) 的选取(或者说 h(n) 的选取,因为g(n) 是确定的)。
h(n)的选取
我们以 d(n) 表达状态 n 到目标状态的实际距离,那么 h(n) 的选取大致有如下三种情况:
- 如果
h(n)< d(n)
到目标状态的实际距离,这种情况下,搜索的点数多,搜索范围大,效率低。但能得到最优解。 - 如果
h(n)=d(n)
,即距离估计 h(n )等于最短距离,那么搜索将严格沿着最短路径进行, 此时的搜索效率是最高的。 - 如果
h(n)>d(n)
,搜索的点数少,搜索范围小,效率高,但不能保证得到最优解。
h(n) 选取不好的 A* 算法,举一个例子。广度优先算法就是 A* 算法的特例。其中 g(n) 是节点所在的层数,h(n)=0,这种 h(n) 肯定小于 d(n),所以由前述可知广度优先算法也是一种可采纳的 A* 算法。不过它的效率就很不高,也是一种比较差的 A* 算法。
这里我们继续分析上面我们说过的砖块环境,以例子理解 A 算法。下图所有的箭头,指向母节点。open 符号则代表在 open list 中,closed 符号则代表在 closed list 中。g(n) 和 h(n) 观察地图可得知数据(注意:这里的 h(n) 没有用启发法猜测而是直接观察得知,因为我们这里只是想讲解 A 的实际应用例子。这里的 h(n) 也没有考虑障碍物,因为障碍物我们后面在“搜寻思路”小节中再谈到)。
下图是 起始砖块的路径得分,即我们已经检测了起始砖块相邻的八个砖块,并把它们加入了 open list 中,已经检测过的起始砖块,则被加入到了 closed list 中。
观察上图可知,h(n) 的值分别为 3,4,5 。我们已经把初始节点加入到了 closed list 中。在 open list 中,下一步选取 h(n) 的值最小的节点(即 h(n)= 3 ),即移动成本最低的节点,即(5,4)。继续循环,开始检测(5,4)八个相邻方向的砖块,计算相邻砖块的成本,将非障碍物的砖块放入open list 中,并把检测过的砖块(5,4)放入 closed list 中。再从(5,4)的相邻砖块中选一个移动成本最低的开始循环,以此类推,循环几轮,直至找到目的地节点,则 A* 算法的主循环结束,输出结果。
下图是 最终建立实际路径的节点。
等到目的地节点也被放入 open list 中时,我们就知道路径寻找已经完成。从目标位置开始依次寻找其母节点,按照母节点连接关系走回起始砖块,这些母位置构成的路径就是移动成本最小的路径,由(2,7),(2,6),(2,5),(3,4),(4,3),(5,4)以及(6,5)这几个点所构成。g(n)的值,可以告诉我们目标节点与起始结点之间,隔了几个母节点。
搜寻死路
任何两个指定点之间的有效路径,也可能根本就不存在。那么我们如何得知,是不是死路?
最简单的方法就是监控 open list。如果 open list 中再也没有任何成员,就是遇到死路了。如下图所示:
上图中,A* 算法已经扩散到每个可能的相邻方块。每个砖块均被检视过,都已经被移到 closed list 中。到最后,open list 中的每个砖块都被检视过,没有新的砖块可以加进来。我们就可以断定这是死路的情况了,不能建立从起点到目的地的路径。
地形成本
之前我们讲过的路径积分,即比较长的路径视为成本比较高,是 标准 A* 算法的最基本形式,但是有的时候我们还需要考虑其他因素。
最短的路径不一定是最快的,比如不同地形的移动成本是不同的,只要在计算总的移动成本时,考虑到地形因素就可以了。即:f(n)=g(n)+h(n)+地形成本
。基本算法还是和上面说的一致。
当然对于金钱,燃料或其他类型的资源时,问题就会变得更加复杂一些。
影响力对应
地形成本往往是可以事先知道的,但是有些因素是无法事先知道的。例如:通过任何敌人的视线的节点,有较高的成本。这种成本无法在设计游戏软件阶段时建立,因为游戏角色的位置是会改变的。
影响力对应(influence mapping)是一种改变 A* 节点成本的方法,根据游戏里发生的情节而定。
例如,如果玩家在特定的节点处,不断杀害计算机控制的角色,则可以在这个节点处增加成本。然后计算机控制的角色就可以改用其他的路径。对玩家而言,这可以让计算机控制的角色看起来很聪明,好像他们能从错误中吸取经验。这种技巧如图7-27 所示。
图7-27 展示了影响力对应,记录的玩家在每个节点杀害蜘蛛的数量。每次玩家杀一只,该节点就会在成本上增加值。