1. 程式人生 > >A*尋路 -- 更加真實 的路徑(二)

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

轉自:http://bbs.9ria.com/thread-95620-1-1.html

對於A*傳統尋路的結果不平滑的問題,我們討論了一種判斷兩點間是否存在障礙物的演算法,並在使用者點選滑鼠選擇了目的地後先判斷起終點間是否存在障礙物,若不存在,則路徑陣列中將只具有一個終點節點;否則進行A*尋路運算。大致過程可用下面程式碼表示:

Java程式碼  收藏程式碼
  1. //判斷起終點間是否存在障礙物,若存在則呼叫A*演算法進行尋路,通過A*尋路得到的路徑是一個個所要經過的節點陣列;否不存在障礙則直接把路徑設定為只含有一個終點元素的陣列  
  2. var hasBarrier:Boolean = _grid.hasBarrier(startPosX, startPosY, endPosX, endPosY);  
  3. if( hasBarrier )  
  4. {  
  5.       _grid.setStartNode(startPosX, startPosY);  
  6.       _grid.setEndNode(endPosX, endPosY);  
  7.       findPath();  
  8. }  
  9. else  
  10. {  
  11.         _path = [_grid.getNode(endPosX, endPosY)];  
  12.        _index = 0;  
  13.        addEventListener(Event.ENTER_FRAME, onEnterFrame);//開始行走  
  14. }  

如果你聽從了我的建議這麼做了,我只能說聲抱歉,因為在實際應用中發現了一些問題,什麼問題呢?就是當起終點間不存在障礙物時,主角行走的目的地將跳過 A*尋路演算法的“考驗”,直接設定為我點選的位置(endPosX, endPosY),因此,此位置若有時候會是路徑網格外的某一點,主角也以此為目的地進行移動,這樣導致的結果就是主角“飛”起來了,或是主角“穿越” 了……為了避免這個問題的發生,我們就必須時時刻刻地使用A*演算法去計算路徑,即使起終點間沒有障礙物也必須呼叫尋路方法進行尋路。但是如果用A*尋路算 法計算出來的路徑不平滑怎麼辦呢?別急,下面我們來討論A*尋路演算法的路徑平滑處理辦法:弗洛伊德路徑平滑演算法。這個演算法出之lizhi寫的文章:

http://wonderfl.net/c/aWCe ,並已被很多人採納~我們先看一下結果演示:MyPathFinding2 

弗洛伊德路徑平滑演算法應在通過A*尋路演算法得出路徑後進行,它的步驟分為兩步:一、合併路徑陣列中共線的節點;二、儘可能地去掉多餘拐點。這個過程如下圖所示:

原始A*尋路路徑



 去掉共線點



 去掉多餘拐點


可以看到,使用弗洛伊德路徑平滑處理 後的路徑正如我們期望的那樣,而且大大削減了路徑陣列中的節點數目。

那麼接下來來講講實現思路吧。首先,不難發現,若存在三點A(1,1), B(2,2), C(3,3),若B與A的橫、縱座標差值分別等於C與B的橫、縱座標差值,則A,B,C三點共線,使用程式碼來表示就是:

Java程式碼  收藏程式碼
  1. if( (bx -ax == cx - bx) && (by-ay == cy - by) )  
  2. {  
  3. //三點共線  
  4. }  
 

由上式可知去掉路徑中共線節點的方法。接下來討論如何去掉多餘的拐點。
仔細觀察第三幅圖你會發現,若路徑中存在節點A,B,C,D,E,F,G,如果A與G之間的連線所經過的節點中沒有一個節點是不可移動節點,則我們稱A與 G之間是不存在障礙物的。如兩節點間不存在障礙物,則可以去掉此兩點間其他所有節點。如上例中A-G這些節點,若A與G之間不存在障礙物,則我們可以去掉 A與G之間的B,C,D,E,F節點,最終路徑陣列中只剩下A與G兩個節點。那麼如何判斷兩個點之間存不存在障礙物呢?在上一篇的教程中我已詳細解釋過方 法,列位道友若不記得了,可以再回頭再去仔細研讀一二。
        那麼最後我們使用程式碼來實現弗洛伊德路徑平滑處理的演算法:

Java程式碼  收藏程式碼
  1. /** 弗洛伊德路徑平滑處理 */  
  2. public function floyd():void {  
  3.         if (path == null)  
  4.                 return;  
  5.         _floydPath = path.concat();  
  6.         var len:int = _floydPath.length;  
  7.         if (len > 2)  
  8.         {  
  9.                 var vector:ANode = new ANode(00);  
  10.                 var tempVector:ANode = new ANode(00);  
  11.                 //遍歷路徑陣列中全部路徑節點,合併在同一直線上的路徑節點  
  12.                 //假設有1,2,3,三點,若2與1的橫、縱座標差值分別與3與2的橫、縱座標差值相等則  
  13.                 //判斷此三點共線,此時可以刪除中間點2  
  14.                 floydVector(vector, _floydPath[len - 1], _floydPath[len - 2]);  
  15.                 for (var i:int = _floydPath.length - 3; i >= 0; i--)  
  16.                 {  
  17.                         floydVector(tempVector, _floydPath[i + 1], _floydPath[i]);  
  18.                         if (vector.x == tempVector.x && vector.y == tempVector.y)  
  19.                         {  
  20.                                 _floydPath.splice(i + 11);  
  21.                         }  
  22.                         else  
  23.                         {  
  24.                                 vector.x = tempVector.x;  
  25.                                 vector.y = tempVector.y;  
  26.                         }  
  27.                 }  
  28.         }  
  29.         //合併共線節點後進行第二步,消除拐點操作。演算法流程如下:  
  30.         //如果一個路徑由1-10十個節點組成,那麼由節點10從1開始檢查  
  31.         //節點間是否存在障礙物,若它們之間不存在障礙物,則直接合並  
  32.         //此兩路徑節點間所有節點。  
  33.         len = _floydPath.length;  
  34.         for (i = len - 1; i >= 0; i--)  
  35.         {  
  36.                 for (var j:int = 0; j <= i - 2; j++)  
  37.                 {  
  38.                         if ( _grid.hasBarrier(_floydPath[i].x, _floydPath[i].y, _floydPath[j].x, _floydPath[j].y) == false )  
  39.                         {  
  40.                                 for (var k:int = i - 1; k > j; k--)  
  41.                                 {  
  42.                                         _floydPath.splice(k, 1);  
  43.                                 }  
  44.                                 i = j;  
  45.                                 len = _floydPath.length;  
  46.                                 break;  
  47.                         }  
  48.                 }  
  49.         }  
  50. }  
 

接下來再講一講第二個棘手的問題,就是A*尋路會當你點選一個不可移動點之後返回false的尋路結果,即告知你無路可走。這不是我們想要的結果,我們想 要的是點選一個不可移動點後玩家應該走到離此不可移動點最近的一個可移動點位上面。那麼如果你閱讀過我的上一篇帖子,你會看到我解決此問題的一個方式是使 用“將不可移動點設定超大代價法”。不過使用此方法的一個弊病在於當你在選擇一個不可移動點作為終點後,A*尋路演算法會遍歷大量的點,造成效能的低下。那 麼在經過另一番研究後發現還有一種辦法能解決這個問題,就是“尋找替代點法”。
        先解釋一下什麼是“尋找替代點法”,當點選一個不可移動點U之後我們將尋找此不可移動點外圍一個離起點距離最短的可移動點R作為U的替代點,替代U來完成尋路且玩家將最終移動到此R點位置。如下圖所示



  那麼,為了儘可能地降低尋找替代點的時間,我們提出了一種“埋葬深度”的概念。先看下圖:


 這種情況下,U點由一圈甚至兩圈或更多圈不可移動點包圍著,若我們採用傳統的方式以輻射型遍歷U點外圈節點(從U點外圍第一圈開始遍歷,遍歷完畢沒有找 到替代點,繼續再遍歷更外邊的圈直到找到為止),將會在多次點選同一點時產生冗餘遍歷時間。那麼此時我們為了降低重複遍歷的次數,就引入了“埋葬深度的概 念”。若一個節點為不可移動點,則其埋葬深度為1;在此基礎上,若其周圍一圈全部為不可移動點,則其埋葬深度加一,為2;若更外圍一圈依然全部為不可移動 點,埋葬深度再加一,依次類推,下圖列出了埋葬深度為1-3的情況:



 在為某一個節點第一次尋找替代點時以輻射型遍歷此點周圍節點以計算出此節點的埋葬深度並記錄於每個節點物件Node類中新增的一個埋葬深度屬性buriedDepth中,下一次為此點尋找替代點時就可以根據埋葬深度從存在可移動點的那一圈遍歷



 

 在遍歷原始終點U周圍存在可移動點的那一圈之後把所有可移動點都存到一個數組中,之後比較此陣列中全部候選點與起點的距離,選出距離最短的一個點作為替代點R即可。



 好吧,尋找替代點的過程大致就是這樣,最後來看程式碼唄。先看節點類ANode類中一些要用到的新增屬性和方法:

Java程式碼  收藏程式碼
  1. public class ANode  
  2. {  
  3.         .....  
  4.         /** 埋葬深度 */  
  5.         public var buriedDepth:int = -1;  
  6.         /** 距離 */  
  7.         public var distance:Number;  
  8.         .....  
  9.         /** 得到此節點到另一節點的網格距離 */  
  10.         public function getDistanceTo( targetNode:ANode ):Number  
  11.         {  
  12.                 var disX:Number = targetNode.x - x;  
  13.                 var disY:Number = targetNode.y - y;  
  14.                 distance = Math.sqrt( disX * disX + disY * disY );  
  15.                 return distance;  
  16.         }  
  17. }  
 

再看節點網格NodeGrid類中新增方法。

Java程式碼  收藏程式碼
  1. /**當終點不可移動時尋找一個離原終點最近的可移動點來替代之 */  
  2. public function findReplacer( fromNode:ANode, toNode:ANode ):ANode  
  3. {  
  4.         var result:ANode;  
  5.         //若終點可移動則根本無需尋找替代點  
  6.         if( toNode.walkable )  
  7.         {  
  8.                 result = toNode;  
  9.         }  
  10.         //否則遍歷終點周圍節點以尋找離起始點最近一個可移動點作為替代點  
  11.         else  
  12.         {  
  13.                 //根據節點的埋葬深度選擇遍歷的圈  
  14.                 //若該節點是第一次遍歷,則計算其埋葬深度  
  15.                 if( toNode.buriedDepth == -1 )  
  16.                 {  
  17.                         toNode.buriedDepth = getNodeBuriedDepth( toNode, Math.max(_numCols, _numRows) );  
  18.                 }  
  19.                 var xFrom:int = toNode.x - toNode.buriedDepth < 0 ? 0 : toNode.x - toNode.buriedDepth;  
  20.                 var xTo:int = toNode.x + toNode.buriedDepth > numCols - 1 ? numCols - 1 : toNode.x + toNode.buriedDepth;  
  21.                 var yFrom:int = toNode.y - toNode.buriedDepth < 0 ? 0 : toNode.y - toNode.buriedDepth;  
  22.                 var yTo:int = toNode.y + toNode.buriedDepth > numRows - 1 ? numRows - 1 : toNode.y + toNode.buriedDepth;                  
  23.                 var n:ANode;//當前遍歷節點  
  24.                 for( var i:int=xFrom; i<=xTo; i++ )  
  25.                 {  
  26.                         for( var j:int=yFrom; j<=yTo; j++ )  
  27.                         {  
  28.                                 if( (i>xFrom && i<xTo) && (j>yFrom && j<yTo) )  
  29.                                 {  
  30.                                         continue;  
  31.                                 }  
  32.                                 n = getNode(i, j);  
  33.                                 if( n.walkable )  
  34.                                 {  
  35.                                         //計算此候選節點到起點的距離,記錄離起點最近的候選點為替代點  
  36.                                         n.getDistanceTo( fromNode );  
  37.                                         if( !result )  
  38.                                         {  
  39.                                                 result = n;  
  40.                                         }  
  41.                                         else if( n.distance < result.distance )  
  42.                                         {  
  43.                                                 result = n;  
  44.                                         }  
  45.                                 }  
  46.                         }  
  47.                 }  
  48.         }  
  49.         return result;  
  50. }  
  51. /** 計算一個節點的埋葬深度 
  52.  * @param node                欲計算深度的節點 
  53.  * @param loopCount        計算深度時遍歷此節點外圍圈數。預設值為10*/  
  54. private function getNodeBuriedDepth( node:ANode, loopCount:int=10 ):int  
  55. {  
  56.         //如果檢測節點本身是不可移動的則預設它的深度為1  
  57.         var result:int = node.walkable ? 0 : 1;  
  58.         var l:int = 1;  
  59.         while( l <= loopCount )  
  60.         {  
  61.                 var startX:int = node.x - l < 0 ? 0 : node.x - l;  
  62.                 var endX:int = node.x + l > numCols - 1 ? numCols - 1 : node.x + l;  
  63.                 var startY:int = node.y - l < 0 ? 0 : node.y - l;  
  64.                 var endY:int = node.y + l > numRows - 1 ? numRows - 1 : node.y + l;                  
  65.                 var n:ANode;  
  66.                 //遍歷一個節點周圍一圈看是否周圍一圈全部是不可移動點,若是,則深度加一,  
  67.                 //否則返回當前累積的深度值  
  68.                 for(var i:int = startX; i <= endX; i++)  
  69.                 {  
  70.                         for(var j:int = startY; j <= endY; j++)  
  71.                         {  
  72.                                 n = getNode(i, j);  
  73.                                 if( n != node && n.walkable )  
  74.                                 {  
  75.                                         return result;  
  76.                                 }  
  77.                         }  
  78.                 }  
  79.                 //遍歷完一圈,沒發現一個可移動點,則埋葬深度加一。接著遍歷下一圈  
  80.                 result++;  
  81.                 l++;  
  82.         }  
  83.         return result;  
  84. }  

那麼最後,看看實際應用這個方法進行尋路的部分吧。

Java程式碼  收藏程式碼
  1. private function onGridClick(event:MouseEvent):void  
  2. {  
  3.         var startTime:int = getTimer();  
  4.         var startPosX:int = Math.floor(_player.x / _cellSize);  
  5.         var startPosY:int = Math.floor(_player.y / _cellSize);  
  6.         var startNode:ANode = _grid.getNode(startPosX, startPosY);  
  7.         var endPosX:int = Math.floor(event.localX / _cellSize);  
  8.         var endPosY:int = Math.floor(event.localY / _cellSize);  
  9.         var endNode:ANode = _grid.getNode(endPosX, endPosY);  
  10.         if( endNode.walkable == false )  
  11.         {  
  12.                 replacer = _grid.findReplacer(startNode, endNode);  
  13.                 if( replacer )  
  14.                 {  
  15.                         endPosX = replacer.x;  
  16.                         endPosY = replacer.y;  
  17.                 }  
  18.         }  
  19.         _grid.setStartNode(startPosX, startPosY);  
  20.         _grid.setEndNode(endPosX, endPosY);  
  21.         findPath();  
  22. }  
 

OK, 這就是本次教程全部內容了,使用“弗洛伊德路徑平滑處理”結合“尋找替代點法”做尋路,不論效率還是結果都還算如人意啦。不過,細心的朋友可以發現,“尋找替代點法”的弊端在於無法在存在“孤島”和“回”字形的路徑網格中找到正確的替代點,如下圖:



 所以為了避免此情況,要麼就用上一篇帖子中提到的“極大代價法”來進行尋路(結果準確,效率過低),要麼就在佈置路徑網格時刻意避免編出這種形式的路徑網格。