A*尋路 -- 更加真實 的路徑(一)

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


  對於A*尋路算法,可能是遊戲開發者討論最多的話題之一,很多遊戲都會用到它來實現遊戲角色的尋路。那麼我這篇帖子的價值何在呢?先來看看傳統A*算法存在的問題:
1.尷尬的Z型路徑 
        當你在用A*算法實現了角色行走邏輯後,點擊一個目標點,雖然你起點和目標點間沒有任何障礙物,但角色還TMD蛋疼地進行了Z型行走路線,拐了好多次彎,他丫的那麼風騷的走位,以爲有人在用狙擊槍——瞄準他呢!?
2.無路可走 
        當你使用A*算法的時候你可能會發現,當你選擇一個不可移動點或者一個被障礙物圍住的“島嶼”點作爲目標點的時候A*尋路算法會返回false的尋 路結果,通知你它沒有找到一條通路,那麼此時你怎麼辦?角色只能站在原地乾瞪眼,用戶見狀可能還以爲自己鼠標失靈了呢。“咦?我明明點了那裏,這角色咋不 動捏?shit~!”
3.效率不高 
       你可能在網上或者書籍(如《ActionScript3 動畫高級編程》)附贈光盤中下載過A*尋路源碼,但運行後發現尋路一次會耗費10毫秒以上,有時候點擊一個不可移動點或者一個封閉區域時更會耗費上百毫秒 (因爲此時A*算法會遍歷全部的格子直至最終無法找到路徑)。

      這些問題相信很多A*算法使用者都遇到過,但是有一些人因爲水平不足,無法繼續深究下去,而且網上對此問題的解決方案也是沒有任何參考資料可循。另一部 分高手已找到解決之道,但是不願意開源,以至於那麼多年來網上依然缺乏對A*算法這些不足之處的解決之道。其實有時候想想,遊戲公司的策劃也蠻可憐的,他 的想法很不錯,但是程序員往往會以一句“沒辦法解決”或者“實現不了”就給澆了冷水,唉,可能這也是國內缺乏優秀遊戲的原因之一吧。
       
     那麼,廢話少說,馬上開始我們的講解吧,希望我這篇帖子能給一些迷途的道友們指引一下方向。

更合理的行走方式 
     對於第一個問題,可以用下圖來解釋這一現象,當我們選擇一個目標點後,即使目標點與起始點間無障礙物存在,A*尋路產生的路徑依然是曲折的:


 這是由於A*尋路算法是基於格子進行尋路的,因此返回的尋路結果將是一個包含很多格子對象(假設格子對象的類名爲Node)的數組,那麼在行走的過程中 自然是根據“到達路徑數組中一個Node對象的屏幕座標所在處之後以數組中下一個Node對象的屏幕座標所在處爲下一個目的地”這樣的行走過程進行行走 的.

        那麼如何避免這種情況的發生呢?我想只在必要的時候調用A*尋路行不行呢?當終點與起點間無任何障礙物的時候直線行走,有障礙物了再進行尋路行走,如下圖所示:


有想法就有可能,impossible is nothing!
        首先要解決的問題是如何判斷起點和終點間是否存在障礙物,如果你還記得初中數學中“直線”這一章的內容(很多人看到這裏估計要罵一句“holy shit”)的話應該不難想到利用直線的數學特性來解決這一難題。什麼?你數學學的東西全TMD忘光了?好吧好吧,還是讓貧道來指引一下吧……
      先看到下圖,我們把兩點的中心點用直線連接起來,直線經過的格子都以屎黃色標示(我就喜歡屎黃色),當然,不包含這兩個當事點^_^


 此時我們就可以依次檢查這些大便節點(即用屎黃色填充的點)中是否有一個是障礙點,若有任意一個是障礙點,那麼就表示着我這兩個當事點之間走直線是行不通地!
     說着簡單吧?那就做着吧……可是……用代碼怎麼寫啊?說到這,我當初也確實被難住了,用代碼實現這一數學思想的確有些困難,那麼,我們一步步來吧。
     首先我們要想正確地用代碼獲知這些大便節點並非易事,我首先想到的方案是以一個節點寬度爲步長,從起點到終點橫向遍歷出它們之間連線(假設此連線叫 l )與每個節點左邊緣的交點,見下圖:


圖上綠色的點就是我們第一步需要求得的線段 l 與起點終點間所有節點的交點了,從圖上我們發覺,由於 l 是起點與終點節點中心點的連線,所以第一次遍歷時取的步長是半個節點寬,之後遍歷的步長則是一個節點寬了。那麼求得這些點有什麼用呢?從圖上看到,只要正 確地得到了這些關鍵點,之後就可以求每個關鍵點所毗鄰的全部節點以最終得到全部的大便節點,如上圖中最左邊這個綠色的關鍵點它所毗鄰的兩個節點是起點(紅 色圓球所在點)以及起點右邊那個(1,0)號點。由此,我們可以先把循環體給定下來了,如果假設我們用來計算兩點間是否存在障礙物的方法名叫做 hasBarrier,那麼它的代碼雛形如下:

/**
 * 判斷兩節點之間是否存在障礙物 
 * 
 */                
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值橫向遍歷會不會漏掉一些節點呢?答案是肯定的,看下圖這種情況


按上面我所說的橫向遍歷的規則,第一次遍歷我求得了上圖左邊這個綠色點,第二次遍歷求得了右邊這個綠色點,在求得此二關鍵點後求出它們各自所毗鄰的節點並在圖上以屎黃色標示,發現遍歷結果中漏掉了中間這塊節點。
        那麼咋辦呢?細心的道友會提出一個方案:對 l 傾斜角大於45度角的情況(此時起點與終點間縱向距離大於橫向距離)使用縱向遍歷,而對傾斜角小於45度 的情況(此時起點與終點間橫向距離大於縱向距離)使用橫向遍歷,這樣就不會漏掉任何一個大便點了。沒有錯,答案就是如此,獎勵這位回答正確的同學一隻小紅 花,哦不,還是獎勵小菊花吧~再回頭看看剛纔那個漏掉大便點的情況,那時 l 傾斜角已大於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;
        }
}

好了,接下來該做的就是決定循環體中應該執行的邏輯了。前面寡人說過,我們的最終目的是得到線段 l 經過的大便點,那麼要得到這些大便點就必須先求得那些綠色的關鍵點才行。現在我們已經知道了遍歷的規則,可能是橫向遍歷也可能是縱向遍歷,假設我們使用橫 向遍歷的情況下,再假設每個格子的尺寸都是1,那麼這些綠色關鍵點的 x 值就都是已知的了。


  要求得綠點的 y 值,只需要將它們的 x 值代入線段 l 的直線方程(假設直線方程爲 y = ax + b )中即可。所以接下來要做的事情就是先求出這個直線方程中的未知數 a 與 b 的值。
      既然我們已知了該線段兩段的端點座標,把它們的座標值代入方程即可求得未知數 a 與 b。我把這一數學求解方程的代碼放在一個數學類MathUtil.as中,代碼如下:

package
{
        import flash.geom.Point;

        /**
         * 尋路算法中使用到的數學方法 
         * @author Wangzhouquan
         * 
         */        
        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;                        
}
我的代碼是在《動畫高級教程》第四章“尋路”的源代碼基礎上改的,所以要想看懂接下來的代碼,最好事先閱讀過《動畫高級教程》第四章的內容。 

      問:寫hasBarrier這個方法的目的是什麼?()
      A:隨便寫着玩玩
      B:for the lich king!
      C:避免在不必要的時候依然使用A*尋路
      D:喂,不要問不該問的東西

      上題的參考答案是 C,你答對了嗎?如果你答對了,那麼在恭喜你的同時我們也該繼續下一步操作了。通常人物的行走是由鼠標點擊觸發的,那麼在鼠標點擊事件的處理函數中,需要根據點擊的目的地來選擇是否啓用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);//開始行走
        }
}


樓上的道友們慾望很強烈啊,我還沒連載完就迫切地需要看看結果了,好吧,先給出一個在線預覽版本讓你們先把玩一下,代碼也放出來先:

 

效果在線預覽: http://we.zuisg.com/?p=257

 

全部源碼: (附件)

 

 

讓點擊障礙物或者死路時可以走到離障礙物最近的點 
     我查了一下,網上給出的所有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*還是會選擇走這些難走的路的。


而實現這一切只需要使用一句代碼:

var g:Number = node.g + cost * test.costMultiplier;

上面這個costMultiplier變量是每一個節點都具備的屬性,不易走的路徑costMultiplier的值大,好走的路徑costMultiplier值小。


       這樣就給了我一個啓發,既然我的目的是讓我點擊的障礙物點能夠有機會被加到開啓列表_open中,但是在路上碰到的障礙物點還是能夠正常地繞開的話,不妨 把障礙物點和不可穿越點的代價costMultiplier設爲一個極大的值,當A*尋路實在找不到更佳的路徑時它還是會回頭來找我這個障礙物點的!這個 過程如下圖所示:


有人說,那按你這樣子走的話,我尋路者不是要走到障礙物裏頭去啦?不要着急,不要着急,休息,休息一會兒……咳咳,其實我還留了一手,那就是當你尋路完畢 返回一個路徑數組path後,我會從前往後對path進行遍歷,一旦遇到walkable爲false或是不可穿越點就把path中位於這一點之後的全部 點都TMD給我飛掉去,避免這些孽畜再爲禍人間!


最後,看看代碼的實現過程吧,下面給出的是經過修改的search方法以及buildPath方法:

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中包含起始點,其實這是沒有必要的,如果你包含了起始點的話會造成下 圖這種走回頭路的結果:


 之後,對尋路返回路徑進行剛纔所說的處理,去掉無效路徑後就能得到正確的路徑了。

 

算法效率優化 
      上面代碼中加了幾句測試效率的語句,在debug時候可以打印出尋路總耗時、對開放列表進行排序的總耗時以及尋路總嘗試次數。當你按照我剛纔給出的算法 去點擊一個障礙物點進行尋路的時候,雖然能夠找到正確的路徑,但是會發現try time的值非常大,總耗時也不低。這是因爲我把障礙物點的代價設置爲很大後A*尋路會先掠過此點並找尋更佳的點,它不找完剩餘全部的節點是不會甘心的。 對於這種情況,網上有一些解決方案,比如將整個網格劃分成幾個區域等方法,這些方案我沒有嘗試過,因爲怕會出現什麼差錯。那麼既然不能降低嘗試次數,只好 從代碼執行效率上來做做功課。在《動畫高級教程》中提供的AStar.as源碼中存在一些會減慢效率的代碼:
1.將代碼中的Math.abs()方法、Math.max()、Math.min()方法用 ? : 這個三元運算符來替代;

if(isOpen(test) || isClosed(test))

使用了isOpen和isClosed兩個函數來判斷test點是否存在於_open和_close這兩個數組中,然而我們看看isOpen和isClosed這兩個函數中是怎麼做的:

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方法:

if(_open.indexOf(test) != -1 || _closed.indexOf(test) != -1)

經過上面兩步優化之後,調試一下,發現效率有所提高,但是我們發現排序時間依然花了不少,佔總耗時的50%左右,那麼我想到使用網上流行的二叉堆法來替代數組自帶的sortOn方法。

什麼是二叉堆法呢?先看看有關二叉堆這個數據結構的介紹:http://www.blueidea.com/tech/multimedia/2007/4665_3.asp 

二叉堆這個數據結構的好處在於它始終能夠保證堆頂的那個元素是所有元素中最小的,當堆內元素髮生改變時就會進行一系列的運算以確保堆內結構一致(添加、刪 除、修改時該做什麼工作在上面給出的鏈接文章中都已說明),但是這“一系列的運算”的運算量遠比直接使用數組自帶的sortOn方法小,就比如這樣子的一 個二叉堆:
(圖片丟了)
當我修改右下方的那個24爲5之後,只要把24與它的父節點20交換一次位置,再與新的父節點10交換一次位置這兩次操作即可,如果使用sortOn的話會遍歷全部元素,消耗不少時間。當元素數量極大的時候,二叉堆的這種排序上的效率優勢就更加明顯了。

      根據二叉堆的原理得到我們的Binary.as類:

package
{
        /**
         * 二叉堆數據結構 
         * @author S_eVent
         * 
         */        
        public class Binary
        {
                private var _data:Array;
                private var _compareValue:String;
                /**
                 * @param compareValue        排序字段,若爲空字符串則直接比較被添加元素本身的值
                 * 
                 */                
                public function Binary( compareValue:String="" )
                {
                        _data = new Array();
                        _compareValue = compareValue;
                }
                
                /** 向二叉堆中添加元素 
                 * @param node                        欲添加的元素對象
                 */
                public function push( node:Object ):void
                {
                        //將新節點添至末尾先
                        _data.push( node );
                        var len:int = _data.length;
                        
                        //若數組中只有一個元素則省略排序過程,否則對新元素執行上浮過程
                        if( len > 1 )
                        {
                                /** 新添入節點當前所在索引 */
                                var index:int = len;
                                
                                /** 新節點當前父節點所在索引 */
                                var parentIndex:int = index / 2 - 1;
                                
                                var temp:Object;
                                
                                //和它的父節點(位置爲當前位置除以2取整,比如第4個元素的父節點位置是2,第7個元素的父節點位置是3)比較,
                                //如果新元素比父節點元素小則交換這兩個元素,然後再和新位置的父節點比較,直到它的父節點不再比它大,
                                //或者已經到達頂端,及第1的位置
                                
                                while( compareTwoNodes(node, _data[parentIndex]) )
                                {
                                        temp = _data[parentIndex];
                                        _data[parentIndex] = node;
                                        _data[index - 1] = temp;
                                        index /= 2;
                                        parentIndex = index / 2 - 1;
                                }
                                
                        }
                        
                }
                
                /** 彈出開啓列表中第一個元素 */
                public function shift():Object
                {
                        //先彈出列首元素
                        var result:Object =  _data.shift();
                        
                        /** 數組長度 */
                        var len:int = _data.length;
                        
                        //若彈出列首元素後數組空了或者其中只有一個元素了則省略排序過程,否則對列尾元素執行下沉過程
                        if( len > 1 )
                        {
                                /** 列尾節點 */
                                var lastNode:Object = _data.pop();
                                
                                //將列尾元素排至首位
                                _data.unshift( lastNode );
                                
                                /** 末尾節點當前所在索引 */
                                var index:int = 0;
                                
                                /** 末尾節點當前第一子節點所在索引 */
                                var childIndex:int = (index + 1) * 2 - 1;
                                
                                /** 末尾節點當前兩個子節點中較小的一個的索引 */
                                var comparedIndex:int;
                                
                                var temp:Object;
                                
                                //和它的兩個子節點比較,如果較小的子節點比它小就將它們交換,直到兩個子節點都比它大
                                while( childIndex < len )
                                {
                                        //只有一個子節點的情況
                                        if( childIndex + 1 == len )
                                        {
                                                comparedIndex = childIndex;
                                        }
                                        //有兩個子節點則取其中較小的那個
                                        else
                                        {
                                                comparedIndex = compareTwoNodes(_data[childIndex], _data[childIndex + 1]) ? childIndex : childIndex + 1;
                                        }
                                        
                                        if( compareTwoNodes(_data[comparedIndex], lastNode) )
                                        {
                                                temp = _data[comparedIndex];
                                                _data[comparedIndex] = lastNode;
                                                _data[index] = temp;
                                                index = comparedIndex;
                                                childIndex = (index + 1) * 2 - 1;
                                        }
                                        else
                                        {
                                                break;
                                        }
                                }
                                
                        }
                        
                        
                        return result;
                }
                
                /** 更新某一個節點的值。在你改變了二叉堆中某一節點的值以後二叉堆不會自動進行排序,所以你需要手動
                 *  調用此方法進行二叉樹更新 */
                public function updateNode( node:Object ):void
                {
                        var index:int = _data.indexOf( node ) + 1;
                        if( index == 0 )
                        {
                                throw new Error("!更新一個二叉堆中不存在的節點作甚!?");
                        }
                        else
                        {
                                var parentIndex:int = index / 2 - 1;
                                var temp:Object;
                                //上浮過程開始嘍
                                while( compareTwoNodes(node, _data[parentIndex]) )
                                {
                                        temp = _data[parentIndex];
                                        _data[parentIndex] = node;
                                        _data[index - 1] = temp;
                                        index /= 2;
                                        parentIndex = index / 2 - 1;
                                }
                        }
                }
                
                /** 查找某節點所在索引位置 */
                public function indexOf( node:Object ):int
                {
                        return _data.indexOf(node);
                }
                
                
                public function get length():uint
                {
                        return _data.length;
                }
                
                /**比較兩個節點,返回true則表示第一個節點小於第二個*/
                private function compareTwoNodes( node1:Object, node2:Object ):Boolean
                {
                        if( _compareValue )
                        {
                                return node1[_compareValue] < node2[_compareValue];
                        }
                        else
                        {
                                return node1 < node2;
                        }
                        return false;
                }
        }
}

所有二叉堆對外開放的API命名規範都效仿Array。那麼有了這個類之後就開始使用它來替代原先A*算法中的Array實例吧,在A*算法中,需要排序 的只有開放列表_open這一個實例,所以只需要將_open的類型改成Binary就可以了。下面列出更改了的一些語句:

//                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倍以上……
        在原A*算法的基礎上做優化,我差不多就優化到這裏爲止,如果你的地圖非常大,你想追求更進一步的快速,那看來只能換算法或者使用鍊金術(alchemy)了……源碼已在7樓給出~
        此貼只是個人的一點思路,並引出一個話題大家討論,相信一定有更好的方法存在.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章