目前项目中寻路是通过A*来实现的,实际应用时候发现存在以下问题:
1.生成的Z型折线路径
用A*算法得到的路径,在起点和目标点间没有任何障碍物的情况下路径仍可能是
Z型折线型。
2.目标点无法到达情况
使用A*算法,选择一个不可移动点或者一个被障碍物围住的“岛屿”点作为目标点的时候A*寻路算法会返回false的寻路结果,通知你它没有找到一条通路,此时角色会停止不动,而不是移动到距离目标点最近的可移动点。
3.效率不高
A*算法会遍历全部的格子直至最终找到路径,遍历全局,效率较低。
1,更合理的行走方式
对于第一个问题,可以用下图来解释这一现象,当我们选择一个目标点后,即使目标点与起始点间无障碍物存在,A*寻路产生的路径依然是曲折的:
这是由于A*寻路算法是基于格子进行寻路的,(当前也有使用三角网格进行寻路的但是会引入新的问题如绕远路,绕大圈的情况)因此返回的寻路结果将是一个包含很多格子对象(假设格子对象的类名为Node)的数组,那么在行走的过程中 自然是根据“到达路径数组中一个Node对象的屏幕坐标所在处之后以数组中下一个Node对象的屏幕坐标所在处为下一个目的地”这样的行走过程进行行走 的.
那么如何避免这种情况的发生呢?我想只在必要的时候调用A*寻路行不行呢?当终点与起点间无任何障碍物的时候直线行走,有障碍物了再进行寻路行走,如下图所示:
首先要解决的问题是如何判断起点和终点间是否存在障碍物,先看到下图,我们把两点的中心点用直线连接起来,直线经过的格子都以黄色标示,当然,不包含这两个当事点
此时我们就可以依次检查这些绿色的点中是否有一个是障碍点,若有任意一个是障碍点,那么就表示着我这两个当事点之间走直线是行不通地!
,我首先想到的方案是以一个节点宽度为步长,从起点到终点横向遍历出它们之间连线(假设此连线叫 l )与每个节点左边缘的交点,见下图:
图上绿色的点就是我们第一步需要求得的线段 l 与起点终点间所有节点的交点了,从图上我们发觉,由于 l 是起点与终点节点中心点的连线,所以第一次遍历时取的步长是半个节点宽,之后遍历的步长则是一个节点宽了。那么求得这些点有什么用呢?从图上看到,只要正 确地得到了这些关键点,之后就可以求每个关键点所毗邻的全部节点以最终得到全部的大便节点,如上图中最左边这个绿色的关键点它所毗邻的两个节点是起点(红 色圆球所在点)以及起点右边那个(1,0)号点。由此,我们可以先把循环体给定下来了,如果假设我们用来计算两点间是否存在障碍物的方法名叫做 hasBarrier,那么它的代码雏形如下:避免在不必要的时候依然使用A*寻路
- /**
- * 判断两节点之间是否存在障碍物
- *
- */
- public function hasBarrier( startX:int, startY:int, endX:int, endY:int ):Boolean
- {
- //为了运算方便,以下运算全部假设格子尺寸为1,格子坐标就等于它们的行、列号
- /** 循环递增量 */
- var i:Number;
-
- /** 循环起始值 */
- var loopStart:Number;
-
- /** 循环终结值 */
- var loopEnd:Number;
-
- loopStart = Math.min( startX, endX );
- loopEnd = Math.max( startX, endX );
-
- //开始横向遍历起点与终点间的节点看是否存在障碍(不可移动点)
- for( i=loopStart; i<=loopEnd; i++ )
- {
- //由于线段方程是根据终起点中心点连线算出的,所以对于起始点来说需要根据其中心点
- //位置来算,而对于其他点则根据左上角来算
- if( i==loopStart )i += .5;
-
- ............
-
- if( i == loopStart + .5 )i -= .5;
- }
- }
但是这样根据x值横向遍历会不会漏掉一些节点呢?答案是肯定的,看下图这种情况
按上面我所说的横向遍历的规则,第一次遍历我求得了上图左边这个绿色点,第二次遍历求得了右边这个绿色点,在求得此二关键点后求出它们各自所毗邻的节点并在图上以屎黄色标示,发现遍历结果中漏掉了中间这块节点。
处理时,我们倾斜角大于45度角的情况(此时起点与终点间纵向距离大于横向距离)使用纵向遍历,而对倾斜角小于45度 的情况(此时起点与终点间横向距离大于纵向距离)使用横向遍历,这样就不会漏掉任何一个点了。
oh, yeah, perfect!
既然遍历的方向要根据情况而定,所以原先代码将更改为下面这样:
- //根据起点终点间横纵向距离的大小来判断遍历方向
- var distX:Number = Math.abs(endX - startX);
- var distY:Number = Math.abs(endY - startY);
-
- /**遍历方向,为true则为横向遍历,否则为纵向遍历*/
- var loopDirection:Boolean = distX > distY ? true : false;
-
- /** 循环递增量 */
- var i:Number;
-
- /** 循环起始值 */
- var loopStart:Number;
-
- /** 循环终结值 */
- var loopEnd:Number;
-
- //为了运算方便,以下运算全部假设格子尺寸为1,格子坐标就等于它们的行、列号
- if( loopDirection )
- {
-
- loopStart = Math.min( startX, endX );
- loopEnd = Math.max( startX, endX );
-
- //开始横向遍历起点与终点间的节点看是否存在障碍(不可移动点)
- for( i=loopStart; i<=loopEnd; i++ )
- {
- //由于线段方程是根据终起点中心点连线算出的,所以对于起始点来说需要根据其中心点
- //位置来算,而对于其他点则根据左上角来算
- if( i==loopStart )i += .5;
-
- …………
-
- if( i == loopStart + .5 )i -= .5;
- }
- }
- else
- {
- loopStart = Math.min( startY, endY );
- loopEnd = Math.max( startY, endY );
-
- //开始纵向遍历起点与终点间的节点看是否存在障碍(不可移动点)
- for( i=loopStart; i<=loopEnd; i++ )
- {
- if( i==loopStart )i += .5;
-
- …………
-
- if( i == loopStart + .5 )i -= .5;
- }
- }
好了,接下来该做的就是决定循环体中应该执行的逻辑了。
要求得绿点的 y 值,只需要将它们的 x 值代入线段 l 的直线方程(假设直线方程为 y = ax + b )中即可。所以接下来要做的事情就是先求出这个直线方程中的未知数 a 与 b 的值。
既然我们已知了该线段两段的端点坐标,把它们的坐标值代入方程即可求得未知数 a 与 b。我把这一数学求解方程的代码放在一个数学类MathUtil.as中,代码如下:
- package
- {
- import flash.geom.Point;
-
- /**
- * 寻路算法中使用到的数学方法
- *
- */
- public class MathUtil
- {
-
- /**
- * 根据两点确定这两点连线的二元一次方程 y = ax + b或者 x = ay + b
- * @param ponit1
- * @param point2
- * @param type 指定返回函数的形式。为0则根据x值得到y,为1则根据y得到x
- *
- * @return 由参数中两点确定的直线的二元一次函数
- */
- public static function getLineFunc(ponit1:Point, point2:Point, type:int=0):Function
- {
- var resultFuc:Function;
-
- // 先考虑两点在一条垂直于坐标轴直线的情况,此时直线方程为 y = a 或者 x = a 的形式
- if( ponit1.x == point2.x )
- {
- if( type == 0 )
- {
- throw new Error("两点所确定直线垂直于y轴,不能根据x值得到y值");
- }
- else if( type == 1 )
- {
- resultFuc = function( y:Number ):Number
- {
- return ponit1.x;
- }
-
- }
- return resultFuc;
- }
- else if( ponit1.y == point2.y )
- {
- if( type == 0 )
- {
- resultFuc = function( x:Number ):Number
- {
- return ponit1.y;
- }
- }
- else if( type == 1 )
- {
- throw new Error("两点所确定直线垂直于y轴,不能根据x值得到y值");
- }
- return resultFuc;
- }
-
- // 当两点确定直线不垂直于坐标轴时直线方程设为 y = ax + b
- var a:Number;
-
- // 根据
- // y1 = ax1 + b
- // y2 = ax2 + b
- // 上下两式相减消去b, 得到 a = ( y1 - y2 ) / ( x1 - x2 )
- a = (ponit1.y - point2.y) / (ponit1.x - point2.x);
-
- var b:Number;
-
- //将a的值代入任一方程式即可得到b
- b = ponit1.y - a * ponit1.x;
-
- //把a,b值代入即可得到结果函数
- if( type == 0 )
- {
- resultFuc = function( x:Number ):Number
- {
- return a * x + b;
- }
- }
- else if( type == 1 )
- {
- resultFuc = function( y:Number ):Number
- {
- return (y - b) / a;
- }
- }
-
- return resultFuc;
- }
- }
- }
这个方法将会根据两个参数点求得它们连线的直线方程并返回一个函数实例,如果你第三个参数type传入的是0,那么将会得到一个类似于 y = ax + b这样的函数实例,假设此实例名为fuc,那么你可以传一个 x 值作为 fuc 的参数,它会返回给你一个在直线 l 上横坐标等于此 x 值的点的 纵坐标 y = fuc( x ); 如果你第三个参数传入的是1,那么将会得到一个类似于 x = ay + b这样的函数实例,可以根据你传入的 y 值得到直线 l 上纵坐标为 y 的点的横坐标 x = fuc( y )。 设置type这样一个参数是因为我们可能横向遍历也可能纵向遍历,横向遍历时我需要根据 x 值来求 y,纵向遍历时则相反。
好了,有了这个方法以后我们要求出绿色的关键点应该是没有问题了,接下来要做的就是根据一个关键点求出它所毗邻的节点有几个,它们分别是哪些。一般来说,最多可能有4个节点共享一个关键点,最少则是一个节点拥有一个关键点:
如果假设一个节点的宽、高均为1,那么如果一个点的 x 、y 值都不是整数那就可以判定它只可能由一个节点拥有;如果 x 值为整数则表示此点会落在两个节点横向的临边上;如果 y 值为整数则表示此点会落在两个节点纵向的临边上。由此可得getNodesUnderPoint方法:
- /**
- * 得到一个点下的所有节点
- * @param xPos 点的横向位置
- * @param yPos 点的纵向位置
- * @param exception 例外格,若其值不为空,则在得到一个点下的所有节点后会排除这些例外格
- * @return 共享此点的所有节点
- *
- */
- public function getNodesUnderPoint( xPos:Number, yPos:Number, exception:Array=null ):Array
- {
- var result:Array = [];
- var xIsInt:Boolean = xPos % 1 == 0;
- var yIsInt:Boolean = yPos % 1 == 0;
-
- //点由四节点共享情况
- if( xIsInt && yIsInt )
- {
- result[0] = getNode( xPos - 1, yPos - 1);
- result[1] = getNode( xPos, yPos - 1);
- result[2] = getNode( xPos - 1, yPos);
- result[3] = getNode( xPos, yPos);
- }
- //点由2节点共享情况
- //点落在两节点左右临边上
- else if( xIsInt && !yIsInt )
- {
- result[0] = getNode( xPos - 1, int(yPos) );
- result[1] = getNode( xPos, int(yPos) );
- }
- //点落在两节点上下临边上
- else if( !xIsInt && yIsInt )
- {
- result[0] = getNode( int(xPos), yPos - 1 );
- result[1] = getNode( int(xPos), yPos );
- }
- //点由一节点独享情况
- else
- {
- result[0] = getNode( int(xPos), int(yPos) );
- }
-
- //在返回结果前检查结果中是否包含例外点,若包含则排除掉
- if( exception && exception.length > 0 )
- {
- for( var i:int=0; i<result.length; i++ )
- {
- if( exception.indexOf(result[i]) != -1 )
- {
- result.splice(i, 1);
- i--;
- }
- }
- }
-
- return result;
- }
万事具备,只欠东风了,最后把之前写的两个方法用到hasBarrier方法的循环体中去。下面是完整的hasBarrier方法代码:
- /**
- * 判断两节点之间是否存在障碍物
- *
- */
- public function hasBarrier( startX:int, startY:int, endX:int, endY:int ):Boolean
- {
- //如果起点终点是同一个点那傻子都知道它们间是没有障碍物的
- if( startX == endX && startY == endY )return false;
-
- //两节点中心位置
- var point1:Point = new Point( startX + 0.5, startY + 0.5 );
- var point2:Point = new Point( endX + 0.5, endY + 0.5 );
-
- //根据起点终点间横纵向距离的大小来判断遍历方向
- var distX:Number = Math.abs(endX - startX);
- var distY:Number = Math.abs(endY - startY);
-
- /**遍历方向,为true则为横向遍历,否则为纵向遍历*/
- var loopDirection:Boolean = distX > distY ? true : false;
-
- /**起始点与终点的连线方程*/
- var lineFuction:Function;
-
- /** 循环递增量 */
- var i:Number;
-
- /** 循环起始值 */
- var loopStart:Number;
-
- /** 循环终结值 */
- var loopEnd:Number;
-
- /** 起终点连线所经过的节点 */
- var passedNodeList:Array;
- var passedNode:Node;
-
- //为了运算方便,以下运算全部假设格子尺寸为1,格子坐标就等于它们的行、列号
- if( loopDirection )
- {
- lineFuction = MathUtil.getLineFunc(point1, point2, 0);
-
- loopStart = Math.min( startX, endX );
- loopEnd = Math.max( startX, endX );
-
- //开始横向遍历起点与终点间的节点看是否存在障碍(不可移动点)
- for( i=loopStart; i<=loopEnd; i++ )
- {
- //由于线段方程是根据终起点中心点连线算出的,所以对于起始点来说需要根据其中心点
- //位置来算,而对于其他点则根据左上角来算
- if( i==loopStart )i += .5;
- //根据x得到直线上的y值
- var yPos:Number = lineFuction(i);
-
- //检查经过的节点是否有障碍物,若有则返回true
- passedNodeList = getNodesUnderPoint( i, yPos );
- for each( passedNode in passedNodeList )
- {
- if( passedNode.walkable == false )return true;
- }
-
- if( i == loopStart + .5 )i -= .5;
- }
- }
- else
- {
- lineFuction = MathUtil.getLineFunc(point1, point2, 1);
-
- loopStart = Math.min( startY, endY );
- loopEnd = Math.max( startY, endY );
-
- //开始纵向遍历起点与终点间的节点看是否存在障碍(不可移动点)
- for( i=loopStart; i<=loopEnd; i++ )
- {
- if( i==loopStart )i += .5;
- //根据y得到直线上的x值
- var xPos:Number = lineFuction(i);
-
- passedNodeList = getNodesUnderPoint( xPos, i );
-
- for each( passedNode in passedNodeList )
- {
- if( passedNode.walkable == false )return true;
- }
-
- if( i == loopStart + .5 )i -= .5;
- }
- }
-
- return false;
- }
通常人物的行走是由鼠标点击触发的,那么在鼠标点击事件的处理函数中,需要根据点击的目的地来选择是否启用A*寻路算法来进行寻路。
- private function onGridClick(event:MouseEvent):void
- {
- //起点是玩家对象所在节点位置,终点是鼠标点击的节点
- var startPosX:int = Math.floor(_player.x / _cellSize);
- var startPosY:int = Math.floor(_player.y / _cellSize);
-
- var endPosX:int = Math.floor(mouseX / _cellSize);
- var endPosY:int = Math.floor(mouseY / _cellSize);
-
- //判断起终点间是否存在障碍物,若存在则调用A*算法进行寻路,通过A*寻路得到的路径是一个个所要经过的节点数组;否不存在障碍则直接把路径设置为只含有一个终点元素的数组
- var hasBarrier:Boolean = _grid.hasBarrier(startPosX, startPosY, endPosX, endPosY);
- if( hasBarrier )
- {
- _grid.setStartNode(startPosX, startPosY);
- _grid.setEndNode(endPosX, endPosY);
-
- findPath();
- }
- else
- {
- _path = [_grid.getNode(endPosX, endPosY)];
- _index = 0;
- addEventListener(Event.ENTER_FRAME, onEnterFrame);//开始行走
- }
-
- }
-
- private function findPath():void
- {
- var astar:AStar = new AStar();
- if(astar.findPath(_grid))
- {
- _path = astar.path;
- _index = 0;
- addEventListener(Event.ENTER_FRAME, onEnterFrame);//开始行走
- }
- }
另一种思路:
MMO游戏中角色行走的概念中其实是没有格子的。游戏中行走其实是根据给出的路径中的两点,判断朝向,播放相应的行走动画,移动(可以是地图动人不动,或人动地图不动)。
为了保证精度,我们使用的格子很小,得到的路径上点也就很多了。实际行走中将会发现角色行走时会不停抖动,其实是因为格子太小,路径点太近,人物不停在改变朝向。
只要保证路径上两点间的距离足够远就行了。我们需要在A*给出的由格子序列组成的路径上进行过滤。
取其中两个格子,判断两个点之间有无不可行走区域,如果没有,这两个格子之间的格子都可以从路径中移除。
例子:格子12345678910。
我们采用分治的方法,先取中间点5,看1-5和5-10直线连接上有没有不可行走区域。
如可以则把两个端点加入路径,中间的格子去掉,不可以就继续对两个端点之间进行分治。
-
private function getRoute(startX : int,startY : int,endX : int,endY : int,aStarPath : Array,wall:Wall):void {
-
var xDistance :
int = endX - startX;
-
var yDistance :
int = endY - startY;
-
var distance :
int = Math.round(Math.pow(xDistance * xDistance + yDistance * yDistance,
1 /
2));
-
-
var tempX :
int,tempY :
int;
-
var flag : Boolean =
false;
-
var isExistStart : Boolean =
false;
-
var isExistEnd : Boolean =
false;
-
-
-
-
for (i =
1;i < distance;i += KEY_POINT_LENGTH) {
-
tempX = Math.round(startX + i * xDistance / distance);
-
tempY = Math.round(startY + i * yDistance / distance);
-
if (wall.isWall(tempX,tempY)) {
-
-
-
-
-
// 如果没有障碍物,则将起始点加入到新路径并返回
-
-
for (i =
0;i < route.length;i ++) {
-
-
if (note[
0] == startX && note[
1] == startY) isExistStart =
true;
-
if (note[
0] == endX && note[
1] == endY) isExistEnd =
true;
-
-
if (! isExistStart) route.push([startX,startY]);
-
if (! isExistEnd) route.push([endX,endY]);
-
-
-
-
var tmpDistance : Number;
-
var a :
int,b :
int,c :
int,tmpX :
int = startX,tmpY :
int = startY,tmpPos :
int;
-
-
-
c = endX * startY - startX * endY;
-
-
-
-
-
var length:
int = aStarPath.length;
-
-
for (i =
0;i < length;i ++) {
-
-
tmpDistance = Math.abs((a * note[
0] + b * note[
1] + c) / Math.sqrt(a * a + b * b));
-
if (distance < tmpDistance) {
-
-
-
-
-
-
-
-
-
-
-
-
for (i =
0;i < route.length;i ++) {
-
-
if (note[
0] == startX && note[
1] == startY) isExistStart =
true;
-
-
if (! isExistStart) route.push([startX,startY]);
-
-
var tmpFirstArray : Array =
new Array();
-
for (i =
0;i < tmpPos;i ++) {
-
tmpFirstArray[i] = aStarPath[i];
-
-
var tmpLastArray : Array =
new Array();
-
for (i = tmpPos;i < length;i ++) {
-
tmpLastArray[i - tmpPos] = aStarPath[i];
-
-
-
if (tmpFirstArray.length >
0) getRoute(startX,startY,tmpX,tmpY,tmpFirstArray,wall);
-
if (tmpLastArray.length >
0) getRoute(tmpX,tmpY,endX,endY,tmpLastArray,wall);
-
//如果目标点不在路径中,将目标点坐标加入到新的路径
-
for (i =
0;i < route.length;i ++) {
-
-
if (note[
0] == endX && note[
1] == endY) isExistEnd =
true;
-
-
if (! isExistEnd) route.push([endX,endY]);
-
-
-
2.让点击障碍物或者死路时可以走到离障碍物最近的点
我查了一下,网上给出的所有A*寻路算法都忽略了这点,即点击一个障碍物或死路(四周被障碍物环绕)时寻路者/玩家 就停在原地不动了,这样会让玩家有受挫、受限感,或者产生“你这个游戏有bug”的错觉。那么对于一些斜45度等角投影视角类游戏来说你玩家站原地不动是 没有多大关系的,谁叫你点了一个明显的障碍物格呢(如河流、山岭、建筑物等)
但若是对一些横版纯2D游戏来说,采用原始A*寻路这种方式就有点不能接受了……
那么要想解决这个问题,首先得找到问题的源头出在哪里,打开《动画高级教程》中提供的A*寻路源码,寻路部分代码如下:
- public function search():Boolean
- {
- var node:Node = _startNode;
- while(node != _endNode)
- {
- var startX:int = Math.max(0, node.x - 1);
- var endX:int = Math.min(_grid.numCols - 1, node.x + 1);
- var startY:int = Math.max(0, node.y - 1);
- var endY:int = Math.min(_grid.numRows - 1, node.y + 1);
-
- for(var i:int = startX; i <= endX; i++)
- {
- for(var j:int = startY; j <= endY; j++)
- {
- var test:Node = _grid.getNode(i, j);
- if(test == node ||
- !test.walkable ||
- !_grid.getNode(node.x, test.y).walkable ||
- !_grid.getNode(test.x, node.y).walkable)
- {
- continue;
- }
-
- var cost:Number = _straightCost;
- if(!((node.x == test.x) || (node.y == test.y)))
- {
- cost = _diagCost;
- }
- var g:Number = node.g + cost * test.costMultiplier;
- var h:Number = _heuristic(test);
- var f:Number = g + h;
- if(isOpen(test) || isClosed(test))
- {
- if(test.f > f)
- {
- test.f = f;
- test.g = g;
- test.h = h;
- test.parent = node;
- }
- }
- else
- {
- test.f = f;
- test.g = g;
- test.h = h;
- test.parent = node;
- _open.push(test);
- }
- }
- }
- for(var o:int = 0; o < _open.length; o++)
- {
- }
- _closed.push(node);
- if(_open.length == 0)
- {
- trace("no path found");
- return false
- }
- _open.sortOn("f", Array.NUMERIC);
- node = _open.shift() as Node;
- }
- buildPath();
- return true;
- }
了解A*算法的人都知道,所有可能被设置为路径的格子都会被放入开放列表_open中去,但是你再看16-19行这个判断:
- if(test == node ||
- !test.walkable ||
- !_grid.getNode(node.x, test.y).walkable ||
- !_grid.getNode(test.x, node.y).walkable)
- {
- continue;
- }
这个判断会把walkable为false的点(即障碍物点)以及不可穿越点(即与上一路径点处于对角线却不可直接从对角线上通过的点,如下图)
直接用 continue 语句给跳过了,这样的话不论是障碍物点还是不可穿越点都永远没有资格被加入到开启列表中去,那自然就不可能成为路径中的一员了。所以,当你点击一个障碍物点作为终点时,你的目的是让此障碍物点成为路径中的一员,然而A*算法的上述语句却直接把障碍物点给否定掉了,那自然最终会找不出路径来了……
那么如何解决这个问题呢?首先想到的方案是寻找“替代点 ”来替代我们点击的不可移动点作为终点,但是实践证明,你永远也无法正确地找到一个“替代点”。比如,你想从你点击的那个障碍物点开始向四周遍历寻找“替代点”,如果发现了一个walkable为true的点那就把它作为“替代点”吧,这个过程如下图所示:
这种方案的结果可能是上面这种情况,找到了一个位于原目标点周围的一个替代点,这有可能是正确的一个替代点,但是如果此替代点周围又被围了一圈围墙怎么办呢?
那么既然用“替代点”的方式行不通,就只能想点别的办法。在《动画高级教程》“寻路”这一章的末尾部分讲到了对一些不易行 的路径(如沼泽、高地等)增大寻路代价g值来让A*算法寻出的路径能够绕过这些不易行 的路径。
但是如果实在没有路走了,A*还是会选择走这些难走的路的。
而实现这一切只需要使用一句代码:
Java代码
- var g:Number = node.g + cost * test.costMultiplier;
上面这个costMultiplier变量是每一个节点都具备的属性,不易走的路径costMultiplier的值大,好走的路径costMultiplier值小。
这样就给了我一个启发,既然我的目的是让我点击的障碍物点能够有机会被加到开启列表_open中,但是在路上碰到的障碍物点还是能够正常地绕开的话,不妨 把障碍物点和不可穿越点的代价costMultiplier设为一个极大的值,当A*寻路实在找不到更佳的路径时它还是会回头来找我这个障碍物点的!这个 过程如下图所示:
有人说,那按你这样子走的话,我寻路者不是要走到障碍物里头去啦?那就是当你寻路完毕 返回一个路径数组path后,我会从前往后对path进行遍历,一旦遇到walkable为false或是不可穿越点就把path中位于这一点之后的全部 点都给我飞掉去
最后,看看代码的实现过程吧,下面给出的是经过修改的search方法以及buildPath方法:
Java代码
- public function search():Boolean
- {
- var startTime:int=getTimer();
- var node:Node = _startNode;
- var sortTime:int = 0;
- var tryCount:int = 0;
- while(node != _endNode)
- {
- tryCount++;
- var startX:int = Math.max(0, node.x - 1);
- var endX:int = Math.min(_grid.numCols - 1, node.x + 1);
- var startY:int = Math.max(0, node.y - 1);
- var endY:int = Math.min(_grid.numRows - 1, node.y + 1);
-
-
- for(var i:int = startX; i <= endX; i++)
- {
- for(var j:int = startY; j <= endY; j++)
- {
- var test:Node = _grid.getNode(i, j);
- if(test == node)
- {
- continue;
- }
-
- if( !test.walkable || !isDiagonalWalkable(node, test) )
- {
- //设其代价为超级大的一个值,比大便还大哦~
- test.costMultiplier = 1000;
- }
- else
- {
- test.costMultiplier = 1;
- }
-
- var cost:Number = _straightCost;
-
- if(!((node.x == test.x) || (node.y == test.y)))
- {
- cost = _diagCost;
- }
-
- var g:Number = node.g + cost * test.costMultiplier;
- var h:Number = _heuristic(test);
- var f:Number = g + h;
- if( isOpen(test) || isClosed(test)))
- {
- if(test.f > f)
- {
- test.f = f;
- test.g = g;
- test.h = h;
- test.parent = node;
- }
- }
- else
- {
- test.f = f;
- test.g = g;
- test.h = h;
- test.parent = node;
- _open.push( test );
- }
- }
- }
- _closed.push(node);
- if(_open.length == 0)
- {
- trace("no path found");
-
- return false
- }
- var sortStartTime:int = getTimer();
- _open.sortOn("f", Array.NUMERIC);
- sortTime += (getTimer() - sortStartTime);
- node = _open.shift() as Node;
- }
- trace( "time cost: " + (getTimer() - startTime) + "ms");
- trace( "sort cost: " + sortTime);
- trace( "try time: " + tryCount);
- buildPath();
- return true;
- }
-
- private function buildPath():void
- {
- _path = new Array();
- var node:Node = _endNode;
- _path.push(node);
-
- //不包含起始节点
- while(node.parent != _startNode)
- {
- node = node.parent;
- _path.unshift(node);
- }
- //排除无法移动点
- var len:int = _path.length;
- for( var i:int=0; i<len; i++ )
- {
- if( _path[i].walkable == false )
- {
- _path.splice(i, len-i);
- break;
- }
- //由于之前排除了起始点,所以当路径中只有一个元素时候判断该元素与起始点是否是不可穿越关系,若是,则连最后这个元素也给他弹出来~
- else if( len == 1 && !isDiagonalWalkable(_startNode, _endNode) )
- {
- _path.shift();
- }
- //判断后续节点间是否存在不可穿越点,若有,则把此点之后的元素全部拿下
- else if( i < len - 1 && !isDiagonalWalkable(_path[i], _path[i+1]) )
- {
- _path.splice(i+1, len-i-1);
- break;
- }
- }
- }
-
- /** 判断两个节点的对角线路线是否可走 */
- private function isDiagonalWalkable( node1:Node, node2:Node ):Boolean
- {
- var nearByNode1:Node = _grid.getNode( node1.x, node2.y );
- var nearByNode2:Node = _grid.getNode( node2.x, node1.y );
-
- if( nearByNode1.walkable && nearByNode2.walkable )return true;
- return false;
- }
上面的代码中我只改了几句,所以应该很容易就看出来差别在哪,我还加了几句测试语句用来测试寻路总耗时、排序耗时以及寻路过程中的试探次数以便之后进行效 率探讨。我还对buildPath方法做了更改,原始A*算法会在最终返回的path中包含起始点,其实这是没有必要的,如果你包含了起始点的话会造成下 图这种走回头路的结果:
之后,对寻路返回路径进行刚才所说的处理,去掉无效路径后就能得到正确的路径了。
3.算法效率优化
上面代码中加了几句测试效率的语句,在debug时候可以打印出寻路总耗时、对开放列表进行排序的总耗时以及寻路总尝试次数。当你按照我刚才给出的算法 去点击一个障碍物点进行寻路的时候,虽然能够找到正确的路径,但是会发现try time的值非常大,总耗时也不低。这是因为我把障碍物点的代价设置为很大后A*寻路会先掠过此点并找寻更佳的点,它不找完剩余全部的节点是不会甘心的。 对于这种情况,网上有一些解决方案,比如将整个网格划分成几个区域等方法,这些方案我没有尝试过,因为怕会出现什么差错。那么既然不能降低尝试次数,只好 从代码执行效率上来做做功课。在《动画高级教程》中提供的AStar.as源码中存在一些会减慢效率的代码:
1.将代码中的Math.abs()方法、Math.max()、Math.min()方法用 ? : 这个三元运算符来替代;
Java代码
- if(isOpen(test) || isClosed(test))
使用了isOpen和isClosed两个函数来判断test点是否存在于_open和_close这两个数组中,然而我们看看isOpen和isClosed这两个函数中是怎么做的:
Java代码
- private function isOpen(node:Node):Boolean
- {
- for(var i:int = 0; i < _open.length; i++)
- {
- if(_open[i] == node)
- {
- return true;
- }
- }
- return false;
- }
作者使用这种逐个遍历的方式来查找一个元素是否在一个数组中,这种方式的效率远远比数组自带的indexOf方法低下,因此,用Array.indexOf方法来替代isOpen与isClosed方法:
Java代码
- if(_open.indexOf(test) != -1 || _closed.indexOf(test) != -1)
经过上面两步优化之后,调试一下,发现效率有所提高,但是我们发现排序时间依然花了不少,占总耗时的50%左右,那么我想到使用网上流行的二叉堆法来替代数组自带的sortOn方法。
什么是二叉堆法呢?先看看有关二叉堆这个数据结构的介绍:http://www.blueidea.com/tech/multimedia/2007/4665_3.asp
二叉堆这个数据结构的好处在于它始终能够保证堆顶的那个元素是所有元素中最小的,当堆内元素发生改变时就会进行一系列的运算以确保堆内结构一致(添加、删 除、修改时该做什么工作在上面给出的链接文章中都已说明),但是这“一系列的运算”的运算量远比直接使用数组自带的sortOn方法小,
当元素数量极大的时候,二叉堆的这种排序上的效率优势就更加明显了。
根据二叉堆的原理得到我们的Binary.as类:
所有二叉堆对外开放的API命名规范都效仿Array。那么有了这个类之后就开始使用它来替代原先A*算法中的Array实例吧,在A*算法中,需要排序 的只有开放列表_open这一个实例,所以只需要将_open的类型改成Binary就可以了。下面列出更改了的一些语句:
Java代码
- // private var _open:Array
- private var _open:Binary;
-
- ……
-
- public function findPath(grid:Grid):Boolean
- {
- _grid = grid;
- // _open = new Array();
- _open = new Binary("f");
- ……
-
- return search();
- }
-
- public function search():Boolean
- {
- ……
- while(node != _endNode)
- {
- ……
-
-
- for(var i:int = startX; i <= endX; i++)
- {
- for(var j:int = startY; j <= endY; j++)
- {
- ……
-
- var g:Number = node.g + cost * test.costMultiplier;
- var h:Number = _heuristic(test);
- var f:Number = g + h;
- var isInOpen:Boolean = _open.indexOf(test) != -1;
- if( isInOpen || _closed.indexOf(test) != -1)
- {
- if(test.f > f)
- {
- test.f = f;
- test.g = g;
- test.h = h;
- test.parent = node;
- if( isInOpen )
- _open.updateNode( test );
- }
- }
- else
- {
- ……
- }
- }
- }
-
- ……
-
- // _open.sortOn("f", Array.NUMERIC);
- node = _open.shift() as Node;
- }
-
- buildPath();
- return true;
- }
最后,测试一下效率,提高了排序速率2-3倍以上……
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)