深入解析AS3寻路

转载地址如下:

http://bbs.9ria.com/thread-86464-1-1.html


对于A*寻路算法,可能是游戏开发者讨论最多的话题之一,很多游戏都会用到它来实现游戏角色的寻路。那么我这篇帖子的价值何在呢?先来看看传统A*算法存在的问题:
1.尴尬的Z型路径
        当你在用A*算法实现了角色行走逻辑后,点击一个目标点,虽然你起点和目标点间没有任何障碍物,但角色还TMD蛋疼地进行了Z型行走路线,拐了好多次弯,他丫的那么风骚的走位,以为有人在用狙击枪瞄准他呢!?
2.无路可走
        当你使用A*算法的时候你可能会发现,当你选择一个不可移动点或者一个被障碍物围住的“岛屿”点作为目标点的时候A*寻路算法会返回false的寻路结果,通知你它没有找到一条通路,那么此时你怎么办?角色只能站在原地干瞪眼,用户见状可能还以为自己鼠标失灵了呢。“咦?我明明点了那里,这角色咋不动捏?shit~!”
3.效率不高
       你可能在网上或者书籍(如《ActionScript3 动画高级编程》)附赠光盘中下载过A*寻路源码,但运行后发现寻路一次会耗费10毫秒以上,有时候点击一个不可移动点或者一个封闭区域时更会耗费上百毫秒(因为此时A*算法会遍历全部的格子直至最终无法找到路径)。

      这些问题相信很多A*算法使用者都遇到过,但是有一些人因为水平不足,无法继续深究下去,而且网上对此问题的解决方案也是没有任何参考资料可循。另一部分高手已找到解决之道,但是不愿意开源,以至于那么多年来网上依然缺乏对A*算法这些不足之处的解决之道。其实有时候想想,游戏公司的策划也蛮可怜的,他的想法很不错,但是程序员往往会以一句“没办法解决”或者“实现不了”就给浇了冷水,唉,可能这也是国内缺乏优秀游戏的原因之一吧。
      
     那么,废话少说,马上开始我们的讲解吧,希望我这篇帖子能给一些迷途的道友们指引一下方向。

更合理的行走方式
     对于第一个问题,可以用下图来解释这一现象,当我们选择一个目标点后,即使目标点与起始点间无障碍物存在,A*寻路产生的路径依然是曲折的:
1.jpg

下载 (6.81 KB)
2011-6-25 10:43

        这是由于A*寻路算法是基于格子进行寻路的,因此返回的寻路结果将是一个包含很多格子对象(假设格子对象的类名为Node)的数组,那么在行走的过程中自然是根据“到达路径数组中一个Node对象的屏幕坐标所在处之后以数组中下一个Node对象的屏幕坐标所在处为下一个目的地”这样的行走过程进行行走的.
        那么如何避免这种情况的发生呢?我想只在必要的时候调用A*寻路行不行呢?当终点与起点间无任何障碍物的时候直线行走,有障碍物了再进行寻路行走,如下图所示:
3.jpg
下载 (6.48 KB)
2011-6-25 10:43
4.JPG
下载 (6.73 KB)
2011-6-25 10:43

        有想法就有可能,impossible is nothing!
        首先要解决的问题是如何判断起点和终点间是否存在障碍物,如果你还记得初中数学中“直线”这一章的内容(很多人看到这里估计要骂一句“holy shit”)的话应该不难想到利用直线的数学特性来解决这一难题。什么?你数学学的东西全TMD忘光了?好吧好吧,还是让贫道来指引一下吧……
      先看到下图,我们把两点的中心点用直线连接起来,直线经过的格子都以屎黄色标示(我就喜欢屎黄色),当然,不包含这两个当事点^_^
5.jpg
下载 (7.52 KB)
2011-6-25 10:43

        此时我们就可以依次检查这些大便节点(即用屎黄色填充的点)中是否有一个是障碍点,若有任意一个是障碍点,那么就表示着我这两个当事点之间走直线是行不通地!
     说着简单吧?那就做着吧……可是……用 代码怎么写啊?说到这,我当初也确实被难住了,用代码实现这一数学思想的确有些困难,那么,我们一步步来吧。
     首先我们要想正确地用代码获知这些大便节点并非易事,我首先想到的方案是以一个节点宽度为步长,从起点到终点横向遍历出它们之间连线(假设此连线叫 l )与每个节点左边缘的交点,见下图:
6.JPG
下载 (9.96 KB)
2011-6-25 10:43

     图上绿色的点就是我们第一步需要求得的线段 l 与起点终点间所有节点的交点了,从图上我们发觉,由于 l 是起点与终点节点中心点的连线,所以第一次遍历时取的步长是半个节点宽,之后遍历的步长则是一个节点宽了。那么求得这些点有什么用呢?从图上看到,只要正确地得到了这些关键点,之后就可以求每个关键点所毗邻的全部节点以最终得到全部的大便节点,如上图中最左边这个绿色的关键点它所毗邻的两个节点是起点(红色圆球所在点)以及起点右边那个(1,0)号点。由此,我们可以先把循环体给定下来了,如果假设我们用来计算两点间是否存在障碍物的方法名叫做hasBarrier,那么它的代码雏形如下:
  1. /**
  2. * 判断两节点之间是否存在障碍物
  3. *
  4. */               
  5. public function hasBarrier( startX:int, startY:int, endX:int, endY:int ):Boolean
  6. {
  7. //为了运算方便,以下运算全部假设格子尺寸为1,格子坐标就等于它们的行、列号
  8.         /** 循环递增量 */
  9.         var i:Number;      

  10.         /** 循环起始值 */
  11.         var loopStart:Number;
  12.                        
  13.         /** 循环终结值 */
  14.         var loopEnd:Number;

  15.        loopStart = Math.min( startX, endX );
  16.        loopEnd = Math.max( startX, endX );

  17.        //开始横向遍历起点与终点间的节点看是否存在障碍(不可移动点)
  18.         for( i=loopStart; i<=loopEnd; i++ )
  19.         {
  20.                 //由于线段方程是根据终起点中心点连线算出的,所以对于起始点来说需要根据其中心点
  21.                 //位置来算,而对于其他点则根据左上角来算
  22.                 if( i==loopStart )i += .5;                                       
  23.                                                              
  24.                 ............

  25.                 if( i == loopStart + .5 )i -= .5;
  26.         }
  27. }
复制代码
但是这样根据x值横向遍历会不会漏掉一些节点呢?答案是肯定的,看下图这种情况:
7.JPG
下载 (7.74 KB)
2011-6-25 10:43

        按上面我所说的横向遍历的规则,第一次遍历我求得了上图左边这个绿色点,第二次遍历求得了右边这个绿色点,在求得此二关键点后求出它们各自所毗邻的节点并在图上以屎黄色标示,发现遍历结果中漏掉了中间这块节点。
        那么咋办呢?细心的道友会提出一个方案:对 l 倾斜角大于45度角的情况(此时起点与终点间纵向距离大于横向距离)使用纵向遍历,而对倾斜角小于45度 的情况(此时起点与终点间横向距离大于纵向距离)使用横向遍历,这样就不会漏掉任何一个大便点了。没有错,答案就是如此,奖励这位回答正确的同学一只小红花,哦不,还是奖励小菊花吧~再回头看看刚才那个漏掉大便点的情况,那时 l 倾斜角已大于45度,因此采用纵向遍历,结果如下:
8.JPG
下载 (9.01 KB)
2011-6-25 10:43

        oh, yeah, perfect!
        既然遍历的方向要根据情况而定,所以原先代码将更改为下面这样:
  1. //根据起点终点间横纵向距离的大小来判断遍历方向
  2. var distX:Number = Math.abs(endX - startX);
  3. var distY:Number = Math.abs(endY - startY);

  4. /**遍历方向,为true则为横向遍历,否则为纵向遍历*/
  5. var loopDirection:Boolean = distX > distY ? true : false;

  6. /** 循环递增量 */
  7. var i:Number;
  8.                        
  9. /** 循环起始值 */
  10. var loopStart:Number;
  11.                        
  12. /** 循环终结值 */
  13. var loopEnd:Number;
  14.                        
  15. //为了运算方便,以下运算全部假设格子尺寸为1,格子坐标就等于它们的行、列号
  16. if( loopDirection )
  17. {                               
  18.                                
  19.         loopStart = Math.min( startX, endX );
  20.         loopEnd = Math.max( startX, endX );
  21.                                
  22.         //开始横向遍历起点与终点间的节点看是否存在障碍(不可移动点)
  23.         for( i=loopStart; i<=loopEnd; i++ )
  24.         {
  25.                 //由于线段方程是根据终起点中心点连线算出的,所以对于起始点来说需要根据其中心点
  26.                 //位置来算,而对于其他点则根据左上角来算
  27.                 if( i==loopStart )i += .5;
  28.                
  29.                 …………
  30.                                        
  31.                 if( i == loopStart + .5 )i -= .5;
  32.         }
  33. }
  34. else
  35. {                               
  36.         loopStart = Math.min( startY, endY );
  37.         loopEnd = Math.max( startY, endY );
  38.                                
  39.         //开始纵向遍历起点与终点间的节点看是否存在障碍(不可移动点)
  40.         for( i=loopStart; i<=loopEnd; i++ )
  41.         {
  42.                 if( i==loopStart )i += .5;

  43.                 …………
  44.                                        
  45.                 if( i == loopStart + .5 )i -= .5;
  46.         }
  47. }
复制代码
好了,接下来该做的就是决定循环体中应该执行的逻辑了。前面寡人说过,我们的最终目的是得到线段 l 经过的大便点,那么要得到这些大便点就必须先求得那些绿色的关键点才行。现在我们已经知道了遍历的规则,可能是横向遍历也可能是纵向遍历,假设我们使用横向遍历的情况下,再假设每个格子的尺寸都是1,那么这些绿色关键点的 x 值就都是已知的了。
9.JPG
下载 (8.76 KB)
2011-6-25 12:00

      要求得绿点的 y 值,只需要将它们的 x 值代入线段 l 的直线方程(假设直线方程为 y = ax + b )中即可。所以接下来要做的事情就是先求出这个直线方程中的未知数 a 与 b 的值。
      既然我们已知了该线段两段的端点坐标,把它们的坐标值代入方程即可求得未知数 a 与 b。我把这一数学求解方程的代码放在一个数学类MathUtil.as中,代码如下:
  1. package
  2. {
  3.         import flash.geom.Point;

  4.         /**
  5.          * 寻路算法中使用到的数学方法
  6.          * @author Wangzhouquan
  7.          *
  8.          */       
  9.         public class MathUtil
  10.         {
  11.                
  12.                 /**
  13.                  * 根据两点确定这两点连线的二元一次方程 y = ax + b或者 x = ay + b
  14.                  * @param ponit1
  15.                  * @param point2
  16.                  * @param type                指定返回函数的形式。为0则根据x值得到y,为1则根据y得到x
  17.                  *
  18.                  * @return 由参数中两点确定的直线的二元一次函数
  19.                  */               
  20.                 public static function getLineFunc(ponit1:Point, point2:Point, type:int=0):Function
  21.                 {
  22.                         var resultFuc:Function;
  23.                        
  24.                         // 先考虑两点在一条垂直于坐标轴直线的情况,此时直线方程为 y = a 或者 x = a 的形式
  25.                         if( ponit1.x == point2.x )
  26.                         {
  27.                                 if( type == 0 )
  28.                                 {
  29.                                         throw new Error("两点所确定直线垂直于y轴,不能根据x值得到y值");
  30.                                 }
  31.                                 else if( type == 1 )
  32.                                 {
  33.                                         resultFuc =        function( y:Number ):Number
  34.                                                                 {
  35.                                                                         return ponit1.x;
  36.                                                                 }
  37.                                                
  38.                                 }
  39.                                 return resultFuc;
  40.                         }
  41.                         else if( ponit1.y == point2.y )
  42.                         {
  43.                                 if( type == 0 )
  44.                                 {
  45.                                         resultFuc =        function( x:Number ):Number
  46.                                         {
  47.                                                 return ponit1.y;
  48.                                         }
  49.                                 }
  50.                                 else if( type == 1 )
  51.                                 {
  52.                                         throw new Error("两点所确定直线垂直于y轴,不能根据x值得到y值");
  53.                                 }
  54.                                 return resultFuc;
  55.                         }
  56.                        
  57.                         // 当两点确定直线不垂直于坐标轴时直线方程设为 y = ax + b
  58.                         var a:Number;
  59.                        
  60.                         // 根据
  61.                         // y1 = ax1 + b
  62.                         // y2 = ax2 + b
  63.                         // 上下两式相减消去b, 得到 a = ( y1 - y2 ) / ( x1 - x2 )
  64.                         a = (ponit1.y - point2.y) / (ponit1.x - point2.x);
  65.                        
  66.                         var b:Number;
  67.                        
  68.                         //将a的值代入任一方程式即可得到b
  69.                         b = ponit1.y - a * ponit1.x;
  70.                        
  71.                         //把a,b值代入即可得到结果函数
  72.                         if( type == 0 )
  73.                         {
  74.                                 resultFuc =        function( x:Number ):Number
  75.                                                         {
  76.                                                                 return a * x + b;
  77.                                                         }
  78.                         }
  79.                         else if( type == 1 )
  80.                         {
  81.                                 resultFuc =        function( y:Number ):Number
  82.                                 {
  83.                                         return (y - b) / a;
  84.                                 }
  85.                         }
  86.                        
  87.                         return resultFuc;
  88.                 }       
  89.         }
  90. }
复制代码
这个方法将会根据两个参数点求得它们连线的直线方程并返回一个函数实例,如果你第三个参数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个节点共享一个关键点,最少则是一个节点拥有一个关键点:
10.JPG
下载 (7.56 KB)
2011-6-25 12:24

       如果假设一个节点的宽、高均为1,那么如果一个点的 x 、y 值都不是整数那就可以判定它只可能由一个节点拥有;如果 x 值为整数则表示此点会落在两个节点横向的临边上;如果 y 值为整数则表示此点会落在两个节点纵向的临边上。由此可得getNodesUnderPoint方法:
  1. /**
  2. * 得到一个点下的所有节点
  3. * @param xPos                点的横向位置
  4. * @param yPos                点的纵向位置
  5. * @param exception        例外格,若其值不为空,则在得到一个点下的所有节点后会排除这些例外格
  6. * @return                         共享此点的所有节点
  7. *
  8. */               
  9. public function getNodesUnderPoint( xPos:Number, yPos:Number, exception:Array=null ):Array
  10. {
  11.         var result:Array = [];
  12.         var xIsInt:Boolean = xPos % 1 == 0;
  13.         var yIsInt:Boolean = yPos % 1 == 0;
  14.                        
  15.         //点由四节点共享情况
  16.         if( xIsInt && yIsInt )
  17.         {
  18.                 result[0] = getNode( xPos - 1, yPos - 1);
  19.                 result[1] = getNode( xPos, yPos - 1);
  20.                 result[2] = getNode( xPos - 1, yPos);
  21.                 result[3] = getNode( xPos, yPos);
  22.         }
  23.         //点由2节点共享情况
  24.         //点落在两节点左右临边上
  25.         else if( xIsInt && !yIsInt )
  26.         {
  27.                 result[0] = getNode( xPos - 1, int(yPos) );
  28.                 result[1] = getNode( xPos, int(yPos) );
  29.         }
  30.         //点落在两节点上下临边上
  31.         else if( !xIsInt && yIsInt )
  32.         {
  33.                 result[0] = getNode( int(xPos), yPos - 1 );
  34.                 result[1] = getNode( int(xPos), yPos );
  35.         }
  36.         //点由一节点独享情况
  37.         else
  38.         {
  39.                 result[0] = getNode( int(xPos), int(yPos) );
  40.         }
  41.                        
  42.         //在返回结果前检查结果中是否包含例外点,若包含则排除掉
  43.         if( exception && exception.length > 0 )
  44.         {
  45.                 for( var i:int=0; i<result.length; i++ )
  46.                 {
  47.                         if( exception.indexOf(result[i]) != -1 )
  48.                        {
  49.                              result.splice(i, 1);
  50.                              i--;
  51.                        }
  52.                 }
  53.         }
  54.                        
  55.         return result;
  56. }
复制代码
万事具备,只欠东风了,最后把之前写的两个方法用到hasBarrier方法的循环体中去。下面是完整的hasBarrier方法代码:
  1. /**
  2. * 判断两节点之间是否存在障碍物
  3. *
  4. */               
  5. public function hasBarrier( startX:int, startY:int, endX:int, endY:int ):Boolean
  6. {
  7.         //如果起点终点是同一个点那傻子都知道它们间是没有障碍物的
  8.         if( startX == endX && startY == endY )return false;
  9.                        
  10.         //两节点中心位置
  11.         var point1:Point = new Point( startX + 0.5, startY + 0.5 );
  12.         var point2:Point = new Point( endX + 0.5, endY + 0.5 );
  13.                        
  14.         //根据起点终点间横纵向距离的大小来判断遍历方向
  15.         var distX:Number = Math.abs(endX - startX);
  16.         var distY:Number = Math.abs(endY - startY);                                                                       
  17.                        
  18.         /**遍历方向,为true则为横向遍历,否则为纵向遍历*/
  19.         var loopDirection:Boolean = distX > distY ? true : false;
  20.                        
  21.         /**起始点与终点的连线方程*/
  22.         var lineFuction:Function;
  23.                        
  24.         /** 循环递增量 */
  25.         var i:Number;
  26.                        
  27.         /** 循环起始值 */
  28.         var loopStart:Number;
  29.                        
  30.         /** 循环终结值 */
  31.         var loopEnd:Number;
  32.                        
  33.         /** 起终点连线所经过的节点 */
  34.         var passedNodeList:Array;
  35.         var passedNode:Node;
  36.                        
  37.         //为了运算方便,以下运算全部假设格子尺寸为1,格子坐标就等于它们的行、列号
  38.         if( loopDirection )
  39.         {                               
  40.                 lineFuction = MathUtil.getLineFunc(point1, point2, 0);
  41.                                
  42.                 loopStart = Math.min( startX, endX );
  43.                 loopEnd = Math.max( startX, endX );
  44.                                
  45.                 //开始横向遍历起点与终点间的节点看是否存在障碍(不可移动点)
  46.                 for( i=loopStart; i<=loopEnd; i++ )
  47.                 {
  48.                         //由于线段方程是根据终起点中心点连线算出的,所以对于起始点来说需要根据其中心点
  49.                         //位置来算,而对于其他点则根据左上角来算
  50.                         if( i==loopStart )i += .5;
  51.                         //根据x得到直线上的y值
  52.                         var yPos:Number = lineFuction(i);
  53.                                        
  54.                         //检查经过的节点是否有障碍物,若有则返回true
  55.                         passedNodeList = getNodesUnderPoint( i, yPos );
  56.                         for each( passedNode in passedNodeList )
  57.                         {
  58.                                 if( passedNode.walkable == false )return true;
  59.                         }
  60.                                        
  61.                         if( i == loopStart + .5 )i -= .5;
  62.                 }
  63.         }
  64.         else
  65.         {
  66.                 lineFuction = MathUtil.getLineFunc(point1, point2, 1);
  67.                                
  68.                 loopStart = Math.min( startY, endY );
  69.                 loopEnd = Math.max( startY, endY );
  70.                                
  71.                 //开始纵向遍历起点与终点间的节点看是否存在障碍(不可移动点)
  72.                 for( i=loopStart; i<=loopEnd; i++ )
  73.                 {
  74.                         if( i==loopStart )i += .5;
  75.                         //根据y得到直线上的x值
  76.                         var xPos:Number = lineFuction(i);
  77.                                        
  78.                         passedNodeList = getNodesUnderPoint( xPos, i );
  79.                                        
  80.                         for each( passedNode in passedNodeList )
  81.                         {
  82.                                 if( passedNode.walkable == false )return true;
  83.                         }
  84.                                        
  85.                         if( i == loopStart + .5 )i -= .5;
  86.                 }
  87.         }

  88.         return false;                       
  89. }
复制代码
我的代码是在《动画高级教程》第四章“寻路”的源代码基础上改的,所以要想看懂接下来的代码,最好事先阅读过《动画高级教程》第四章的内容。

      问:写hasBarrier这个方法的目的是什么?()
      A:随便写着玩玩
      B:for the lich king!
      C:避免在不必要的时候依然使用A*寻路
      D:喂,不要问不该问的东西

      上题的参考答案是 C,你答对了吗?如果你答对了,那么在恭喜你的同时我们也该继续下一步操作了。通常人物的行走是由鼠标点击触发的,那么在鼠标点击事件的处理函数中,需要根据点击的目的地来选择是否启用A*寻路算法来进行寻路。
  1. private function onGridClick(event:MouseEvent):void
  2. {                                               
  3.         //起点是玩家对象所在节点位置,终点是鼠标点击的节点
  4.         var startPosX:int = Math.floor(_player.x / _cellSize);
  5.         var startPosY:int = Math.floor(_player.y / _cellSize);
  6.                        
  7.         var endPosX:int = Math.floor(mouseX / _cellSize);
  8.         var endPosY:int = Math.floor(mouseY / _cellSize);
  9.                        
  10.         //判断起终点间是否存在障碍物,若存在则调用A*算法进行寻路,通过A*寻路得到的路径是一个个所要经过的节点数组;否不存在障碍则直接把路径设置为只含有一个终点元素的数组
  11.         var hasBarrier:Boolean = _grid.hasBarrier(startPosX, startPosY, endPosX, endPosY);
  12.         if( hasBarrier )
  13.         {
  14.                 _grid.setStartNode(startPosX, startPosY);
  15.                 _grid.setEndNode(endPosX, endPosY);
  16.                                
  17.                 findPath();
  18.         }
  19.         else
  20.         {
  21.                 _path = [_grid.getNode(endPosX, endPosY)];
  22.                 _index = 0;
  23.                 addEventListener(Event.ENTER_FRAME, onEnterFrame);//开始行走
  24.         }
  25.                        
  26. }
  27.                
  28. private function findPath():void
  29. {
  30.         var astar:AStar = new AStar();
  31.         if(astar.findPath(_grid))
  32.         {
  33.                 _path = astar.path;
  34.                 _index = 0;
  35.                 addEventListener(Event.ENTER_FRAME, onEnterFrame);//开始行走
  36.         }
  37. }
复制代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值