 转自网上: 译序 这篇文章很适合A*算法的初学者,可惜网上没找到翻译版的。本着好东西不敢独享的想法,也为了锻炼一下英文,本人译了这篇文章。由于本人英文水平非常有限,六级考了两次加一块不超过370分,因此本译文难免存在问题。不过也算是抛砖引玉,希望看到有更多的游戏开发方面的优秀译作出现,毕竟中文的优秀资料太少了,中国的游戏开发者的路不好走。 本人能力有限,译文中有小部分词句实在难以翻译,因此暂时保留英文原文放在译文中。对于不敢确定翻译是否准确的词句,本人用圆括号保留了英文原文,读者可以对照着加以理解。A*算法本身是很简单的,因此原文中并没有过多地讨论A*算法本身,而是花了较大的篇幅讨论了用于保存OPEN和CLOSED集的数据结构,以及A*算法的变种和扩展。编程实现A*是简单的,读者可以用STL对本文中的伪代码加以实现(本人已花一天时间实验过基本的A*搜索)。但是最重要的还是对A*本身的理解,这样才可以在自己的游戏中处理各种千变万化的情况。翻译本文的想法产生于2006年5月,实际完成于2007年4月到6月,非常惭愧。最后,本译文仅供交流和参考,对于因本译文放到网上而产生的任何问题,本人不负任何责任。 蔡鸿于南开大学软件学院 2007年6月9日 原文地址: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)将会得到最好的效果。
1 导言... 5 1.1 算法... 6 1.2 Dijkstra算法与最佳优先搜索... 6 1.3 A*算法... 9 2 启发式算法... 11 2.1 A*对启发式函数的使用... 11 2.2 速度还是精确度?... 11 2.3 衡量单位... 12 2.4 精确的启发式函数... 12 2.4.1 预计算的精确启发式函数... 12 2.4.2 线性精确启发式算法... 13 2.5 网格地图中的启发式算法... 13 2.5.1 曼哈顿距离... 13 2.5.2 对角线距离... 14 2.5.3 欧几里得距离... 14 2.5.4 平方后的欧几里得距离... 15 2.5.5 Breaking ties 15 2.5.6 区域搜索... 19 3 Implementation notes 19 3.1 概略... 19 3.2 源代码... 20 3.3 集合的表示... 20 3.3.1 未排序数组或链表... 21 3.3.2 排序数组... 21 3.3.3 排序链表... 21 3.3.4 排序跳表... 21 3.3.5 索引数组... 21 3.3.6 哈希表... 22 3.3.7 二元堆... 22 3.3.8 伸展树... 22 3.3.9 HOT队列... 23 3.3.10 比较... 23 3.3.11 混合实现... 24 3.4 与游戏循环的交互... 24 3.4.1 提前退出... 24 3.4.2 中断算法... 24 3.4.3 组运动... 25 3.4.4 细化... 25 4 A*算法的变种... 25 4.1 beam search. 25 4.2 迭代深化... 25 4.3 动态衡量... 26 4.4 带宽搜索... 26 4.5 双向搜索... 26 4.6 动态A*与终身计划A*. 27 5 处理运动障碍物... 27 5.1 重新计算路径... 27 5.2 路径拼接... 28 5.3 监视地图变化... 29 5.4 预测障碍物的运动... 29 6 预计算路径的空间代价... 29 6.1 位置VS方向... 29 6.2 路径压缩... 30 6.2.1 位置存储... 30 6.2.2 方向存储... 30 6.3 计算导航点... 31 6.4 极限路径长度... 31 6.5 总结... 31
1 导言 移动一个简单的物体(object)看起来是容易的。而路径搜索是复杂的。为什么涉及到路径搜索就产生麻烦了?考虑以下情况:
 物体(unit)最初位于地图的底端并且尝试向顶部移动。物体扫描的区域中(粉红色部分)没有任何东西显示它不能向上移动,因此它持续向上移动。在靠近顶部时,它探测到一个障碍物然后改变移动方向。然后它沿着U形障碍物找到它的红色的路径。相反的,一个路径搜索器(pathfinder)将会扫描一个更大的区域(淡蓝色部分),但是它能做到不让物体(unit)走向凹形障碍物而找到一条更短的路径(蓝色路径)。 然而你可以扩展一个运动算法,用于对付上图所示的障碍物。或者避免制造凹形障碍,或者把凹形出口标识为危险的(只有当目的地在里面时才进去):
比起一直等到最后一刻才发现问题,路径搜索器让你提前作出计划。不带路径搜索的运动(movement)可以在很多种情形下工作,同时可以扩展到更多的情形,但是路径搜索是一种更常用的解决更多问题的方法。
 1.1 算法 计算机科学教材中的路径搜索算法在数学视角的图上工作——由边联结起来的结点的集合。一个基于图块(tile)拼接的游戏地图可以看成是一个图,每个图块(tile)是一个结点,并在每个图块之间画一条边:
 目前,我会假设我们使用二维网格(grid)。稍后我将讨论如何在你的游戏之外建立其他类型的图。 许多AI领域或算法研究领域中的路径搜索算法是基于任意(arbitrary)的图设计的,而不是基于网格(grid-based)的图。我们可以找到一些能使用网格地图的特性的东西。有一些我们认为是常识,而算法并不理解。例如,我们知道一些和方向有关的东西:一般而言,如果两个物体距离越远,那么把其中一个物体向另一个移动将花越多的时间;并且我们知道地图中没有任何秘密通道可以从一个地点通向另一个地点。(我假设没有,如果有的话,将会很难找到一条好的路径,因为你并不知道要从何处开始。) 1.2 Dijkstra算法与最佳优先搜索 Dijkstra算法从物体所在的初始点开始,访问图中的结点。它迭代检查待检查结点集中的结点,并把和该结点最靠近的尚未检查的结点加入待检查结点集。该结点集从初始结点向外扩展,直到到达目标结点。Dijkstra算法保证能找到一条从初始点到目标点的最短路径,只要所有的边都有一个非负的代价值。(我说“最短路径”是因为经常会出现许多差不多短的路径。)在下图中,粉红色的结点是初始结点,蓝色的是目标点,而类菱形的有色区域(注:原文是teal areas)则是Dijkstra算法扫描过的区域。颜色最淡的区域是那些离初始点最远的,因而形成探测过程(exploration)的边境(frontier):
 最佳优先搜索(BFS)算法按照类似的流程运行,不同的是它能够评估(称为启发式的)任意结点到目标点的代价。与选择离初始结点最近的结点不同的是,它选择离目标最近的结点。BFS不能保证找到一条最短路径。然而,它比Dijkstra算法快的多,因为它用了一个启发式函数(heuristic function)快速地导向目标结点。例如,如果目标位于出发点的南方,BFS将趋向于导向南方的路径。在下面的图中,越黄的结点代表越高的启发式值(移动到目标的代价高),而越黑的结点代表越低的启发式值(移动到目标的代价低)。这表明了与Dijkstra 算法相比,BFS运行得更快。
 然而,这两个例子都仅仅是最简单的情况——地图中没有障碍物,最短路径是直线的。现在我们来考虑前边描述的凹型障碍物。Dijkstra算法运行得较慢,但确实能保证找到一条最短路径:
 另一方面,BFS运行得较快,但是它找到的路径明显不是一条好的路径:
 问题在于BFS是基于贪心策略的,它试图向目标移动尽管这不是正确的路径。由于它仅仅考虑到达目标的代价,而忽略了当前已花费的代价,于是尽管路径变得很长,它仍然继续走下去。 结合两者的优点不是更好吗?1968年发明的A*算法就是把启发式方法(heuristic approaches)如BFS,和常规方法如Dijsktra算法结合在一起的算法。有点不同的是,类似BFS的启发式方法经常给出一个近似解而不是保证最佳解。然而,尽管A*基于无法保证最佳解的启发式方法,A*却能保证找到一条最短路径。
|