牛刀小试 - A*寻路算法简介
eidiot挂帅出征,携令牌一枚,率人马若干,编制如下:
寻路元帅
为协调行动,特颁军令如下:
“预备士兵”只能由起点或“父将”所在格横、竖或斜向移动一格,直向(横、竖)移动一格走10步,斜向一格14步(斜向是直向1.414倍,取整数),抵达后不得再移动。
看看战图更容易理解,从红色方格出发越过黄色障碍到达蓝色方格:
图例:
由图可形象看出何谓“开启士兵”、“关闭将军”:外围的绿色方格为“开启士兵”,“前线待命”,随时可向外继续探索。内围的紫色方格是“关闭将军”,从终点开始沿箭头寻其“父将”直至起点即得最终路径。
战前会议结束,拔营出征。
首先派出编号为0的“预备士兵”侦查起点,然后升其为“开启士兵”,列入“开启士兵名录”。
此过程结果如下(方格右上角数字是人员编号,左下角是G,右下角是H,左上角是F):
第一轮探索任务完成,元帅开始检查“开启士兵名录”。此时名录中有8名人员,其中1号F值最低为40(起点右移一格,G值为10,到终点平移3格,H值为30,F = G + H = 40),向其发出“行动令牌”。
1号“开启士兵”接到“行动令牌”,晋为“预备将军”,探索周围格子。
此过程结果如下:
第二轮结束,元帅再次检查“开启士兵名录”。此时还有7名“开启士兵”,5号和8号的F值都为最低的54,选择不同寻路的结果也将不同。元帅选择了最后加入的8号“开启士兵”发出“行动令牌”,过程同上,不赘述,结果如下:
重复这个过程直到某位“关闭将军”站到了终点上(或者“开启士兵”探测到了终点,这样更快捷,但某些情况找到的路径不够短),亦即找到了路径;或是“开启士兵名录”已空,无法到达终点。
下面整理一下全过程并翻译成“标准语言”,首先是各名词:
“开启士兵名录” - 开启列表 - open list
寻路过程:
1, 将起点放入开启列表
再翻译成“编程语言”?请看第三部分,锋芒毕露 - AS3代码和示例。
如虎添翼 - 使用二叉堆优化
如何让A*寻路更快?元帅三顾茅庐,请来南阳二叉堆先生帮忙优化寻找“开启士兵名录”中最低F值的过程,将寻路速度提高了2到3倍,而且越大的地图效果越明显。下面隆重介绍二叉堆先生:
下图是一个二叉堆的例子,形式上看,它从顶点开始,每个节点有两个子节点,每个子节点又各自有自己的两个子节点;数值上看,每个节点的两个子节点都比它大或和它相等。
在二叉堆里我们要求:
最小的元素在顶端
只要满足这两个条件,其他的元素怎么排都行。如上面的例子,最小的元素10在最顶端,第二小的元素20在10的下面,但是第三小的元素24在20的下面,也就是第三层,更大的30反而在第二层。
这样一“堆”东西我们在程序中怎么用呢?幸运的是,二叉堆可以用一个简单的一维数组来存储,如下图所示。
假设一个元素的位置是n(第一个元素的位置为1,而不是通常数组的第一个索引0),那么它两个子节点分别是 n × 2 和 n × 2 + 1 ,父节点是n除以2取整。比如第3个元素(例中是20)的两个子节点位置是6和7,父节点位置是1。
对于二叉堆我们通常有三种操作:添加、删除和修改元素:
添加元素
可以看出,使用二叉堆只需很少的几步就可以完成排序,很大程度上提高了寻路速度。
关于二叉堆先生需要了解的就是这么多了,下面来看看他怎么帮助元帅工作:
每次派出的“预备士兵”都会获得一个唯一的编号(ID),一直到寻路结束,它所有的数据包括位置、F值、G值、“父将”编号都将按这个ID存储。
注意,“开启士兵名录”里存的只是人员的编号,数据全都另外存储。不太明白?没关系,元帅将在 第三部分 来次真刀实枪的大演兵。
锋芒毕露 - AS3代码和示例
地形数据不属于A*寻路的范围,这里定义一个 IMapTileModel 接口,由其它(模型)类来实现地图通路的判断。其它比如寻路超时的判断这里也不介绍,具体参考 AStar类及其测试代码。这里只介绍三部分主要内容:
数据存储
首先看看三个关键变量:
private var m_openCount : int ; //当前开放列表中节点数量
private var m_openId : int ; //节点加入开放列表时分配的唯一ID(从0开始)
开放列表 m_openList 是个二叉堆(一维数组),F值最小的节点始终排在最前。为加快排序,开放列表中只存放节点ID ,其它数据放在各自的一维数组中:
private var m_yList : Array ; //节点y坐标
private var m_pathScoreList : Array ; //节点路径评分F值
private var m_movementCostList : Array ; //(从起点移动到)节点的移动耗费G值
private var m_fatherList : Array ; //节点的父节点(ID)
这些数据列表都以节点ID为索引顺序存储。看看代码如何工作:
currId = this . m_openList [ 0 ] ;
//读取当前节点坐标
currNoteX = this . m_xList [ currId ] ;
currNoteY = this . m_yList [ currId ] ;
还有一个很关键的变量:
使用 m_noteMap 可以方便的存取任何位置节点的开启关闭状态,并可取其ID进而存取其它数据。m_noteMap 是个三维数组,第一维y坐标(第几行),第二维x坐标(第几列),第三维节点状态和ID。判断点(p_x, p_y)是否在开启列表中:
寻路过程
AStar类 寻路的方法是 find() :
* 开始寻路
* @param p_startX 起点X坐标
* @param p_startY 起点Y坐标
* @param p_endX 终点X坐标
* @param p_endY 终点Y坐标
* @return 找到的路径(二维数组 : [p_startX, p_startY], ... , [p_endX, p_endY])
*/
public function find ( p_startX : int , p_startY : int , p_endX : int , p_endY : int ) : Array { /* 寻路 */ }
注意这里返回数据的形式:从起点到终点的节点数组,其中每个节点为一维数组[x, y]的形式。为了加快速度,类里没有使用Object或是Point,节点坐标全部以数组形式存储。如节点note的x坐标为note[0],y坐标为note[1]。
下面开始寻路,第一步将起点添加到开启列表:
openNote() 方法将节点加入开放列表的同时分配一个唯一的ID、按此ID存储数据、对开启列表排序。接下来是寻路过程:
{
//每次取出开放列表最前面的ID
currId = this . m_openList [ 0 ] ;
//将编码为此ID的元素列入关闭列表
this . closeNote ( currId ) ;
//如果终点被放入关闭列表寻路结束,返回路径
if ( currNoteX == p_endX && currNoteY == p_endY )
return this . getPath ( p_startX , p_startY , currId ) ;
//...每轮寻路过程
}
//开放列表已空,找不到路径
return null ;
每轮的寻路:
aroundNotes = this . getArounds ( currNoteX , currNoteY ) ;
//对于周围每个节点
for each ( var note : Array in aroundNotes )
{
//计算F和G值
cost = this . m_movementCostList [ currId ] + (( note [ 0 ] == currNoteX || note [ 1 ] == currNoteY ) ? COST_STRAIGHT : COST_DIAGONAL ) ;
score = cost + ( Math . abs ( p_endX - note [ 0 ]) + Math . abs ( p_endY - note [ 1 ])) * COST_STRAIGHT ;
if ( this . isOpen ( note [ 0 ] , note [ 1 ])) //如果节点已在开启列表中
{
//测试节点的ID
checkingId = this . m_noteMap [ note [ 1 ]][ note [ 0 ]][ NOTE_ID ] ;
//如果新的G值比节点原来的G值小,修改F,G值,换父节点
if ( cost < this . m_movementCostList [ checkingId ])
{
this . m_movementCostList [ checkingId ] = cost ;
this . m_pathScoreList [ checkingId ] = score ;
this . m_fatherList [ checkingId ] = currId ;
//对开启列表重新排序
this . aheadNote ( this . getIndex ( checkingId )) ;
}
} else //如果节点不在开放列表中
{
//将节点放入开放列表
this . openNote ( note [ 0 ] , note [ 1 ] , score , cost , currId ) ;
}
}
从终点开始依次沿父节点回到到起点,返回找到的路径:
* 获取路径
* @param p_startX 起始点X坐标
* @param p_startY 起始点Y坐标
* @param p_id 终点的ID
* @return 路径坐标数组
*/
private function getPath ( p_startX : int , p_startY : int , p_id : int ) : Array
{
var arr : Array = [] ;
var noteX : int = this . m_xList [ p_id ] ;
var noteY : int = this . m_yList [ p_id ] ;
while ( noteX != p_startX || noteY != p_startY )
{
arr . unshift ([ noteX , noteY ]) ;
p_id = this . m_fatherList [ p_id ] ;
noteX = this . m_xList [ p_id ] ;
noteY = this . m_yList [ p_id ] ;
}
arr . unshift ([ p_startX , p_startY ]) ;
this . destroyLists () ;
return arr ;
}
列表排序
这部分看代码和注释就可以了,不多说:
private function aheadNote ( p_index : int ) : void
{
var father : int ;
var change : int ;
//如果节点不在列表最前
while ( p_index > 1 )
{
//父节点的位置
father = Math . floor ( p_index / 2 ) ;
//如果该节点的F值小于父节点的F值则和父节点交换
if ( this . getScore ( p_index ) < this . getScore ( father ))
{
change = this . m_openList [ p_index - 1 ] ;
this . m_openList [ p_index - 1 ] = this . m_openList [ father - 1 ] ;
this . m_openList [ father - 1 ] = change ;
p_index = father ;
} else
{
break ;
}
}
}
/** 将(取出开启列表中路径评分最低的节点后从队尾移到最前的)节点向后移动 */
private function backNote () : void
{
//尾部的节点被移到最前面
var checkIndex : int = 1 ;
var tmp : int ;
var change : int ;
while ( true )
{
tmp = checkIndex ;
//如果有子节点
if ( 2 * tmp <= this . m_openCount )
{
//如果子节点的F值更小
if ( this . getScore ( checkIndex ) > this . getScore ( 2 * tmp ))
{
//记节点的新位置为子节点位置
checkIndex = 2 * tmp ;
}
//如果有两个子节点
if ( 2 * tmp + 1 <= this . m_openCount )
{
//如果第二个子节点F值更小
if ( this . getScore ( checkIndex ) > this . getScore ( 2 * tmp + 1 ))
{
//更新节点新位置为第二个子节点位置
checkIndex = 2 * tmp + 1 ;
}
}
}
//如果节点位置没有更新结束排序
if ( tmp == checkIndex )
{
break ;
}
//反之和新位置交换,继续和新位置的子节点比较F值
else
{
change = this . m_openList [ tmp - 1 ] ;
this . m_openList [ tmp - 1 ] = this . m_openList [ checkIndex - 1 ] ;
this . m_openList [ checkIndex - 1 ] = change ;
}
}
}
其中 getScore() 方法:
* 获取某节点的路径评分F值
* @param p_index 节点在开启列表中的索引(从1开始)
*/
private function getScore ( p_index : int ) : int
{
//开启列表索引从1开始,ID从0开始,数组索引从0开始
return this . m_pathScoreList [ this . m_openList [ p_index - 1 ]] ;
}