如此好贴,不能不转!原文地址:http://dev.gameres.com/Program/Abstract/Arithmetic/AmitAStar.mht
本文版权归原作者、译者所有,我只是转贴;如果侵害到您的权益,请联系我,我将删除本文。
基本上,这文章可以说是最佳A*算法文档。极力推荐!
Amit's A star Page中译文
译序
这篇文章很适合A*算法的初学者,可惜网上没找到翻译版的。本着好东西不敢独享的想法,也为了锻炼一下英文,本人译了这篇文章。
由于本人英文水平非常有限,六级考了两次加一块不超过370分,因此本译文难免存在问题。不过也算是抛砖引玉,希望看到有更多的游戏开发方面的优秀译作出现,毕竟中文的优秀资料太少了,中国的游戏开发者的路不好走。
本人能力有限,译文中有小部分词句实在难以翻译,因此暂时保留英文原文放在译文中。对于不敢确定翻译是否准确的词句,本人用圆括号保留了英文原文,读者可以对照着加以理解。
A*算法本身是很简单的,因此原文中并没有过多地讨论A*算法本身,而是花了较大的篇幅讨论了用于保存OPEN和CLOSED集的数据结构,以及A*算法的变种和扩展。
编程实现A*是简单的,读者可以用STL对本文中的伪代码加以实现(本人已花一天时间实验过基本的A*搜索)。但是最重要的还是对A*本身的理解,这样才可以在自己的游戏中处理各种千变万化的情况。
翻译本文的想法产生于2006年5月,实际完成于2007年4月到6月,非常惭愧。
最后,本译文仅供交流和参考,对于因本译文放到网上而产生的任何问题,本人不负任何责任。
蔡鸿于南开大学软件学院
<chsdate isrocdate="False" islunardate="False" day="9" month="6" year="2007" w:st="on">2007<span lang="EN-US" style="FONT-FAMILY: SimSun; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><span lang="EN-US">年</span></span>6<span lang="EN-US" style="FONT-FAMILY: SimSun; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><span lang="EN-US">月</span></span>9<span lang="EN-US" style="FONT-FAMILY: SimSun; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'"><span lang="EN-US">日</span></span></chsdate>
原文地址:http://theory.stanford.edu/~amitp/GameProgramming/
相关链接:http://www-cs-students.stanford.edu/%7Eamitp/gameprog.html#Paths
我们尝试解决的问题是把一个游戏对象(game object)从出发点移动到目的地。路径搜索(Pathfinding)的目标是找到一条好的路径——避免障碍物、敌人,并把代价(燃料,时间,距离,装备,金钱等)最小化。运动(Movement)的目标是找到一条路径并且沿着它行进。把关注的焦点仅集中于其中的一种方法是可能的。一种极端情况是,当游戏对象开始移动时,一个老练的路径搜索器(pathfinder)外加一个琐细的运动算法(movement algorithm)可以找到一条路径,游戏对象将会沿着该路径移动而忽略其它的一切。另一种极端情况是,一个单纯的运动系统(movement-only system)将不会搜索一条路径(最初的“路径”将被一条直线取代),取而代之的是在每一个结点处仅采取一个步骤,同时考虑周围的环境。同时使用路径搜索(Pathfinding)和运动算法(movement algorithm)将会得到最好的效果。
移动一个简单的物体(object)看起来是容易的。而路径搜索是复杂的。为什么涉及到路径搜索就产生麻烦了?考虑以下情况:
<shapetype id="_x0000_t75" coordsize="21600,21600" o:spt="75" o:preferrelative="t" path=" m@4@5 l@4@11@9@11@9@5 xe" filled="f" stroked="f"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0 "></f><f eqn="sum @0 1 0 "></f><f eqn="sum 0 0 @1 "></f><f eqn="prod @2 1 2 "></f><f eqn="prod @3 21600 pixelWidth "></f><f eqn="prod @3 21600 pixelHeight "></f><f eqn="sum @0 0 1 "></f><f eqn="prod @6 1 2 "></f><f eqn="prod @7 21600 pixelWidth "></f><f eqn="sum @8 21600 0 "></f><f eqn="prod @7 21600 pixelHeight "></f><f eqn="sum @10 21600 0 "></f></formulas><path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect"></path><lock v:ext="edit" aspectratio="t"></lock></shapetype><shape id="_x0000_i1025" style="WIDTH: 337.5pt; HEIGHT: 238.5pt" coordsize="21600,21600" alt="" type="#_x0000_t75"><imagedata o:href="http://theory.stanford.edu/~amitp/GameProgramming/concave1.png" src="AmitAStar.files/image001.png"></imagedata></shape>
物体(unit)最初位于地图的底端并且尝试向顶部移动。物体扫描的区域中(粉红色部分)没有任何东西显示它不能向上移动,因此它持续向上移动。在靠近顶部时,它探测到一个障碍物然后改变移动方向。然后它沿着U形障碍物找到它的红色的路径。相反的,一个路径搜索器(pathfinder)将会扫描一个更大的区域(淡蓝色部分),但是它能做到不让物体(unit)走向凹形障碍物而找到一条更短的路径(蓝色路径)。
然而你可以扩展一个运动算法,用于对付上图所示的障碍物。或者避免制造凹形障碍,或者把凹形出口标识为危险的(只有当目的地在里面时才进去):
<shape id="_x0000_i1026" style="WIDTH: 337.5pt; HEIGHT: 238.5pt" coordsize="21600,21600" alt="" type="#_x0000_t75"><imagedata o:href="http://theory.stanford.edu/~amitp/GameProgramming/concave2.png" src="AmitAStar.files/image003.png"></imagedata></shape>
比起一直等到最后一刻才发现问题,路径搜索器让你提前作出计划。不带路径搜索的运动(movement)可以在很多种情形下工作,同时可以扩展到更多的情形,但是路径搜索是一种更常用的解决更多问题的方法。
计算机科学教材中的路径搜索算法在数学视角的图上工作——由边联结起来的结点的集合。一个基于图块(tile)拼接的游戏地图可以看成是一个图,每个图块(tile)是一个结点,并在每个图块之间画一条边:
<shape id="_x0000_i1027" style="WIDTH: 184.5pt; HEIGHT: 184.5pt" coordsize="21600,21600" alt="" type="#_x0000_t75"><imagedata o:href="http://theory.stanford.edu/~amitp/game-programming/a-star/map-as-graph.png" src="AmitAStar.files/image005.png"></imagedata></shape>
目前,我会假设我们使用二维网格(grid)。稍后我将讨论如何在你的游戏之外建立其他类型的图。
许多AI领域或算法研究领域中的路径搜索算法是基于任意(arbitrary)的图设计的,而不是基于网格(grid-based)的图。我们可以找到一些能使用网格地图的特性的东西。有一些我们认为是常识,而算法并不理解。例如,我们知道一些和方向有关的东西:一般而言,如果两个物体距离越远,那么把其中一个物体向另一个移动将花越多的时间;并且我们知道地图中没有任何秘密通道可以从一个地点通向另一个地点。(我假设没有,如果有的话,将会很难找到一条好的路径,因为你并不知道要从何处开始。)
Dijkstra算法从物体所在的初始点开始,访问图中的结点。它迭代检查待检查结点集中的结点,并把和该结点最靠近的尚未检查的结点加入待检查结点集。该结点集从初始结点向外扩展,直到到达目标结点。Dijkstra算法保证能找到一条从初始点到目标点的最短路径,只要所有的边都有一个非负的代价值。(我说“最短路径”是因为经常会出现许多差不多短的路径。)在下图中,粉红色的结点是初始结点,蓝色的是目标点,而类菱形的有色区域(注:原文是teal areas)则是Dijkstra算法扫描过的区域。颜色最淡的区域是那些离初始点最远的,因而形成探测过程(exploration)的边境(frontier):
<shape id="_x0000_i1028" style="WIDTH: 394.5pt; HEIGHT: 282pt" coordsize="21600,21600" alt="" type="#_x0000_t75"><imagedata o:href="http://theory.stanford.edu/~amitp/game-programming/a-star/dijkstra.png" src="AmitAStar.files/image007.png"></imagedata></shape>
最佳优先搜索(BFS)算法按照类似的流程运行,不同的是它能够评估(称为启发式的)任意结点到目标点的代价。与选择离初始结点最近的结点不同的是,它选择离目标最近的结点。BFS不能保证找到一条最短路径。然而,它比Dijkstra算法快的多,因为它用了一个启发式函数(heuristic function)快速地导向目标结点。例如,如果目标位于出发点的南方,BFS将趋向于导向南方的路径。在下面的图中,越黄的结点代表越高的启发式值(移动到目标的代价高),而越黑的结点代表越低的启发式值(移动到目标的代价低)。这表明了与Dijkstra 算法相比,BFS运行得更快。
<shape id="_x0000_i1029" style="WIDTH: 394.5pt; HEIGHT: 282pt" coordsize="21600,21600" alt="" type="#_x0000_t75"><imagedata o:href="http://theory.stanford.edu/~amitp/game-programming/a-star/best-first-search.png" src="AmitAStar.files/image009.png"></imagedata></shape>
然而,这两个例子都仅仅是最简单的情况——地图中没有障碍物,最短路径是直线的。现在我们来考虑前边描述的凹型障碍物。Dijkstra算法运行得较慢,但确实能保证找到一条最短路径:
<shape id="_x0000_i1030" style="WIDTH: 394.5pt; HEIGHT: 282pt" coordsize="21600,21600" alt="" type="#_x0000_t75"><imagedata o:href="http://theory.stanford.edu/~amitp/game-programming/a-star/dijkstra-trap.png" src="AmitAStar.files/image011.png"></imagedata></shape>
另一方面,BFS运行得较快,但是它找到的路径明显不是一条好的路径:
<shape id="_x0000_i1031" style="WIDTH: 394.5pt; HEIGHT: 282pt" coordsize="21600,21600" alt="" type="#_x0000_t75"><imagedata o:href="http://theory.stanford.edu/~amitp/game-programming/a-star/best-first-search-trap.png" src="AmitAStar.files/image013.png"></imagedata></shape>
问题在于BFS是基于贪心策略的,它试图向目标移动尽管这不是正确的路径。由于它仅仅考虑到达目标的代价,而忽略了当前已花费的代价,于是尽管路径变得很长,它仍然继续走下去。
结合两者的优点不是更好吗?1968年发明的A*算法就是把启发式方法(heuristic approaches)如BFS,和常规方法如Dijsktra算法结合在一起的算法。有点不同的是,类似BFS的启发式方法经常给出一个近似解而不是保证最佳解。然而,尽管A*基于无法保证最佳解的启发式方法,A*却能保证找到一条最短路径。
我将集中讨论A*算法。A*是路径搜索中最受欢迎的选择,因为它相当灵活,并且能用于多种多样的情形之中。
和其它的图搜索算法一样,A*潜在地搜索图中一个很大的区域。和Dijkstra一样,A*能用于搜索最短路径。和BFS一样,A*能用启发式函数(注:原文为heuristic)引导它自己。在简单的情况中,它和BFS一样快。
<shape id="_x0000_i1032" style="WIDTH: 394.5pt; HEIGHT: 282pt" coordsize="21600,21600" alt="" type="#_x0000_t75"><imagedata o:href="http://theory.stanford.edu/~amitp/game-programming/a-star/a-star.png" src="AmitAStar.files/image015.png"></imagedata></shape>
在凹型障碍物的例子中,A*找到一条和Dijkstra算法一样好的路径:
<shape id="_x0000_i1033" style="WIDTH: 394.5pt; HEIGHT: 282pt" coordsize="21600,21600" alt="" type="#_x0000_t75"><imagedata o:href="http://theory.stanford.edu/~amitp/game-programming/a-star/a-star-trap.png" src="AmitAStar.files/image017.png"></imagedata></shape>
成功的秘决在于,它把Dijkstra算法(靠近初始点的结点)和BFS算法(靠近目标点的结点)的信息块结合起来。在讨论A*的标准术语中,g(n)表示从初始结点到任意结点n的代价,h(n)表示从结点n到目标点的启发式评估代价(heuristic estimated cost)。在上图中,yellow(h)表示远离目标的结点而teal(g)表示远离初始点的结点。当从初始点向目标点移动时,A*权衡这两者。每次进行主循环时,它检查f(n)最小的结点n,其中f(n) = g(n) + h(n)。
启发式函数h(n)告诉A*从任意结点n到目标结点的最小代价评估值。选择一个好的启发式函数是重要的。
启发式函数可以控制A*的行为:
所以我们得到一个很有趣的情况,那就是我们可以决定我们想要从A*中获得什么。理想情况下(注:原文为At exactly the right point),我们想最快地得到最短路径。如果我们的目标太低,我们仍会得到最短路径,不过速度变慢了;如果我们的目标太高,那我们就放弃了最短路径,但A*运行得更快。
在游戏中,A*的这个特性非常有用。例如,你会发现在某些情况下,你希望得到一条好的路径("good" path)而不是一条完美的路径("perfect" path)。为了权衡g(n)和h(n),你可以修改任意一个。
注:在学术上,如果启发式函数值是对实际代价的低估,A*算法被称为简单的A算法(原文为simply A)。然而,我继续称之为A*,因为在实现上是一样的,并且在游戏编程领域并不区别A和A*。
A*改变它自己行为的能力基于启发式代价函数,启发式函数在游戏中非常有用。在速度和精确度之间取得折衷将会让你的游戏运行得更快。在很多游戏中,你并不真正需要得到最好的路径,仅需要近似的就足够了。而你需要什么则取决于游戏中发生着什么,或者运行游戏的机器有多快。
假设你的游戏有两种地形,平原和山地,在平原中的移动代价是1而在山地则是3。A* is going to search three times as far along flat land as it does along mountainous land. 这是因为有可能有一条沿着平原到山地的路径。把两个邻接点之间的评估距离设为1.5可以加速A*的搜索过程。然后A*会将3和1.5比较,这并不比把3和1比较差。It is not as dissatisfied with mountainous terrain, so it won't spend as much time trying to find a way around it. Alternatively, you can speed up up A*'s search by decreasing the amount it searches for paths around mountains―just tell A* that the movement cost on mountains is 2 instead of 3. Now it will search only twice as far along the flat terrain as along mountainous terrain. Either approach gives up ideal paths to get something quicker.
速度和精确度之间的选择前不是静态的。你可以基于CPU的速度、用于路径搜索的时间片数、地图上物体(units)的数量、物体的重要性、组(group)的大小、难度或者其他任何因素来进行动态的选择。取得动态的折衷的一个方法是,建立一个启发式函数用于假定通过一个网格空间的最小代价是1,然后建立一个代价函数(cost function)用于测量(scales):
g’(n) = 1 + alpha * ( g(n) – 1 )
如果alpha是0,则改进后的代价函数的值总是1。这种情况下,地形代价被完全