A* 尋路算法

概述
雖然掌握了 A* 算法的人認爲它容易,可是對於初學者來講, A* 算法仍是很複雜的。
搜索區域(The Search Area)
咱們假設某人要從 A 點移動到 B 點,可是這兩點之間被一堵牆隔開。如圖 1 ,綠色是 A ,紅色是 B ,中間藍色是牆。
image001.jpg
圖 1
你應該注意到了,咱們把要搜尋的區域劃分紅了正方形的格子。這是尋路的第一步,簡化搜索區域,就像咱們這裏作的同樣。 這個特殊的方法把咱們的搜索區域簡化爲了 2 維數組。數組的每一項表明一個格子,它的狀態就是可走 (walkalbe) 和不可走 (unwalkable) 。經過計算出從 A 到 B 須要走過哪些方格,就找到了路徑。一旦路徑找到了,人物便從一個方格的中心移動到另外一個方格的中心,直至到達目的地。
方格的中心點咱們成爲「節點 (nodes) 」。若是你讀過其餘關於 A* 尋路算法的文章,你會發現人們經常都在討論節點。爲何不直接描述爲方格呢?由於咱們有可能把搜索區域劃爲爲其餘多變形而不是正方形,例如能夠是六邊形, 矩形,甚至能夠是任意多變形。而節點能夠放在任意多邊形裏面,能夠放在多變形的中心,也能夠放在多邊形的邊上。咱們使用這個系統,由於它最簡單。
開始搜索(Starting the Search)
一旦咱們把搜尋區域簡化爲一組能夠量化的節點後,就像上面作的同樣,咱們下一步要作的即是查找最短路徑。在 A* 中,咱們從起點開始,檢查其相鄰的方格,而後向四周擴展,直至找到目標。
咱們這樣開始咱們的尋路旅途:
1.       從 起點 A 開始,並把它就加入到一個由方格組成的 open list( 開放列表 ) 中。這個 open list 有點像是一個購物單。固然如今 open list 裏只有一項,它就是起點 A ,後面會慢慢加入更多的項。 Open list 裏的格子是路徑可能會是沿途通過的,也有可能不通過。基本上 open list 是一個待檢查的方格列表。
2.       查 看與起點 A 相鄰的方格 ( 忽略其中牆壁所佔領的方格,河流所佔領的方格及其餘非法地形佔領的方格 ) ,把其中可走的 (walkable) 或可到達的 (reachable) 方格也加入到 open list 中。把起點 A 設置爲這些方格的父親 (parent node 或 parent square) 。當咱們在追蹤路徑時,這些父節點的內容是很重要的。稍後解釋。
3.       把 A 從 open list 中移除,加入到 close list( 封閉列表 ) 中, close list 中的每一個方格都是如今不須要再關注的。
以下圖所示,深綠色的方格爲起點,它的外框是亮藍色,表示該方格被加入到了 close list 。與它相鄰的黑色方格是須要被檢查的,他們的外框是亮綠色。每一個黑方格都有一個灰色的指針指向他們的父節點,這裏是起點 A 。
image002.jpg
圖 2 。
下一步,咱們須要從 open list 中選一個與起點 A 相鄰的方格,按下面描述的同樣或多或少的重複前面的步驟。可是到底選擇哪一個方格好呢?具備最小 F 值的那個。
路徑排序(Path Sorting)
計算出組成路徑的方格的關鍵是下面這個等式:
F = G + H
這裏,
G = 從起點 A 移動到指定方格的移動代價,沿着到達該方格而生成的路徑。
H = 從指定的方格移動到終點 B 的估算成本。這個一般被稱爲試探法,有點讓人混淆。爲何這麼叫呢,由於這是個猜想。直到咱們找到了路徑咱們纔會知道真正的距離,由於途中有各類各樣的東 西 ( 好比牆壁,水等 ) 。本教程將教你一種計算 H 的方法,你也能夠在網上找到其餘方法。
咱們的路徑是這麼產生的:反覆遍歷 open list ,選擇 F 值最小的方格。這個過程稍後詳細描述。咱們仍是先看看怎麼去計算上面的等式。
如上所述, G 是從起點A移動到指定方格的移動代價。在本例中,橫向和縱向的移動代價爲 10 ,對角線的移動代價爲 14 。之因此使用這些數據,是由於實際的對角移動距離是 2 的平方根,或者是近似的 1.414 倍的橫向或縱向移動代價。使用 10 和 14 就是爲了簡單起見。比例是對的,咱們避免了開放和小數的計算。這並非咱們沒有這個能力或是不喜歡數學。使用這些數字也可使計算機更快。稍後你便會發 現,若是不使用這些技巧,尋路算法將很慢。
既然咱們是沿着到達指定方格的路徑來計算 G 值,那麼計算出該方格的 G 值的方法就是找出其父親的 G 值,而後按父親是直線方向仍是斜線方向加上 10 或 14 。隨着咱們離開起點而獲得更多的方格,這個方法會變得更加明朗。
有不少方法能夠估算 H 值。這裏咱們使用 Manhattan 方法,計算從當前方格橫向或縱向移動到達目標所通過的方格數,忽略對角移動,而後把總數乘以 10 。之因此叫作 Manhattan 方法,是由於這很像統計從一個地點到另外一個地點所穿過的街區數,而你不能斜向穿過街區。重要的是,計算 H 是,要忽略路徑中的障礙物。這是對剩餘距離的估算值,而不是實際值,所以才稱爲試探法。
把 G 和 H 相加便獲得 F 。咱們第一步的結果以下圖所示。每一個方格都標上了 F , G , H 的值,就像起點右邊的方格那樣,左上角是 F ,左下角是 G ,右下角是 H 。
image003.jpg
圖 3
好,如今讓咱們看看其中的一些方格。在標有字母的方格, G = 10 。這是由於水平方向從起點到那裏只有一個方格的距離。與起點直接相鄰的上方,下方,左方的方格的 G 值都是 10 ,對角線的方格 G 值都是 14 。
H 值經過估算起點於終點 ( 紅色方格 ) 的 Manhattan 距離獲得,僅做橫向和縱向移動,而且忽略沿途的牆壁。使用這種方式,起點右邊的方格到終點有 3 個方格的距離,所以 H = 30 。這個方格上方的方格到終點有 4 個方格的距離 ( 注意只計算橫向和縱向距離 ) ,所以 H = 40 。對於其餘的方格,你能夠用一樣的方法知道 H 值是如何得來的。
每一個方格的 F 值,再說一次,直接把 G 值和 H 值相加就能夠了。
繼續搜索(Continuing the Search)
爲了繼續搜索,咱們從 open list 中選擇 F 值最小的 ( 方格 ) 節點,而後對所選擇的方格做以下操做:
4.       把它從 open list 裏取出,放到 close list 中。
5.       檢查全部與它相鄰的方格,忽略其中在 close list 中或是不可走 (unwalkable) 的方格 ( 好比牆,水,或是其餘非法地形 ) ,若是方格不在 open lsit 中,則把它們加入到 open list 中。
把咱們選定的方格設置爲這些新加入的方格的父親。
6.       若是某個相鄰的方格已經在 open list 中,則檢查這條路徑是否更優,也就是說經由當前方格 ( 咱們選中的方格 ) 到達那個方格是否具備更小的 G 值。若是沒有,不作任何操做。
相反,若是 G 值更小,則把那個方格的父親設爲當前方格 ( 咱們選中的方格 ) ,而後從新計算那個方格的 F 值和 G 值。若是你仍是很混淆,請參考下圖。
image004.jpg
圖 4
Ok ,讓咱們看看它是怎麼工做的。在咱們最初的 9 個方格中,還有 8 個在 open list 中,起點被放入了 close list 中。在這些方格中,起點右邊的格子的 F 值 40 最小,所以咱們選擇這個方格做爲下一個要處理的方格。它的外框用藍線打亮。
首先,咱們把它從 open list 移到 close list 中 ( 這就是爲何用藍線打亮的緣由了 ) 。而後咱們檢查與它相鄰的方格。它右邊的方格是牆壁,咱們忽略。它左邊的方格是起點,在 close list 中,咱們也忽略。其餘 4 個相鄰的方格均在 open list 中,咱們須要檢查經由這個方格到達那裏的路徑是否更好,使用 G 值來斷定。讓咱們看看上面的方格。它如今的 G 值爲 14 。若是咱們經由當前方格到達那裏, G 值將會爲 20( 其中 10 爲到達當前方格的 G 值,此外還要加上從當前方格縱向移動到上面方格的 G 值 10) 。顯然 20 比 14 大,所以這不是最優的路徑。若是你看圖你就會明白。直接從起點沿對角線移動到那個方格比先橫向移動再縱向移動要好。
當把 4 個已經在 open list 中的相鄰方格都檢查後,沒有發現經由當前方格的更好路徑,所以咱們不作任何改變。如今咱們已經檢查了當前方格的全部相鄰的方格,並也對他們做了處理,是時候選擇下一個待處理的方格了。
所以再次遍歷咱們的 open list ,如今它只有 7 個方格了,咱們須要選擇 F 值最小的那個。有趣的是,此次有兩個方格的 F 值都 54 ,選哪一個呢?沒什麼關係。從速度上考慮,選擇最後加入 open list 的方格更快。這致使了在尋路過程當中,當靠近目標時,優先使用新找到的方格的偏好。可是這並不重要。 ( 對相同數據的不一樣對待,致使兩中版本的 A* 找到等長的不一樣路徑 ) 。
咱們選擇起點右下方的方格,以下圖所示。
image005.jpg
圖 5
此次,當咱們檢查相鄰的方格時,咱們發現它右邊的方格是牆,忽略之。上面的也同樣。
咱們把牆下面的一格也忽略掉。爲何?由於若是不穿越牆角的話,你不能直接從當前方格移動到那個方格。你須要先往下走,而後再移動到那個方格,這樣來繞過牆角。 ( 注意:穿越牆角的規則是可選的,依賴於你的節點是怎麼放置的 )
這樣還剩下 5 個相鄰的方格。當前方格下面的 2 個方格尚未加入 open list ,因此把它們加入,同時把當前方格設爲他們的父親。在剩下的 3 個方格中,有 2 個已經在 close list 中 ( 一個是起點,一個是當前方格上面的方格,外框被加亮的 ) ,咱們忽略它們。最後一個方格,也就是當前方格左邊的方格,咱們檢查經由當前方格到達那裏是否具備更小的 G 值。沒有。所以咱們準備從 open list 中選擇下一個待處理的方格。
不斷重複這個過程,直到把終點也加入到了 open list 中,此時以下圖所示。
image006.jpg
圖 6
注意,在起點下面 2 格的方格的父親已經與前面不一樣了。以前它的 G 值是 28 而且指向它右上方的方格。如今它的 G 值爲 20 ,而且指向它正上方的方格。這在尋路過程當中的某處發生,使用新路徑時 G 值通過檢查而且變得更低,所以父節點被從新設置, G 和 F 值被從新計算。儘管這一變化在本例中並不重要,可是在不少場合中,這種變化會致使尋路結果的巨大變化。
那麼咱們怎麼樣去肯定實際路徑呢?很簡單,從終點開始,按着箭頭向父節點移動,這樣你就被帶回到了起點,這就是你的路徑。以下圖所示。從起點 A 移動到終點 B 就是簡單從路徑上的一個方格的中心移動到另外一個方格的中心,直至目標。就是這麼簡單!
image007.jpg
圖 7
A*算法總結(Summary of the A* Method)
Ok ,如今你已經看完了整個的介紹,如今咱們把全部步驟放在一塊兒:
1.         把起點加入 open list 。
2.         重複以下過程:
a.         遍歷 open list ,查找 F 值最小的節點,把它做爲當前要處理的節點。
b.         把這個節點移到 close list 。
c.         對當前方格的 8 個相鄰方格的每個方格?
     若是它是不可抵達的或者它在 close list 中,忽略它。不然,作以下操做。
     若是它不在 open list 中,把它加入 open list ,而且把當前方格設置爲它的父親,記錄該方格的 F , G 和 H 值。
     如 果它已經在 open list 中,檢查這條路徑 ( 即經由當前方格到達它那裏 ) 是否更好,用 G 值做參考。更小的 G 值表示這是更好的路徑。若是是這樣,把它的父親設置爲當前方格,並從新計算它的 G 和 F 值。若是你的 open list 是按 F 值排序的話,改變後你可能須要從新排序。
d.         中止,當你
     把終點加入到了 open list 中,此時路徑已經找到了,或者
     查找終點失敗,而且 open list 是空的,此時沒有路徑。
3.         保存路徑。從終點開始,每一個方格沿着父節點移動直至起點,這就是你的路徑。
題外話(Small Rant)
請原諒個人離題,當你在網上或論壇上看到各類關於 A* 算法的討論時,你偶爾會發現一些 A* 的代碼,實際上他們不是。要使用 A* ,你必須包含上面討論的全部元素 ---- 尤爲是 open list , close list 和路徑代價 G , H 和 F 。也有不少其餘的尋路算法,這些算法並非 A* 算法, A* 被認爲是最好的。在本文末尾引用的一些文章中 Bryan Stout 討論了他們的一部分,包括他們的優缺點。在某些時候你能夠二中擇一,但你必須明白本身在作什麼。 Ok ,不廢話了。回到文章。
實現的註解(Notes on Implemetation)
如今你已經明白了基本方法,這裏是你在寫本身的程序是須要考慮的一些額外的東西。下面的材料引用了一些我用 C++ 和 Basic 寫的程序,可是對其餘語言一樣有效。
1.     維護 Open List :這是 A* 中最重要的部分。每次你訪問 Open list ,你都要找出具備最小    F 值的方格。有幾種作法能夠作到這個。你能夠隨意保存路徑元素,當你須要找到具     有最小 F 值的方格時,遍歷整個 open list 。這個很簡單,但對於很長的路徑會很慢。這個方法能夠經過維護一個排好序的表來改進,每次當你須要找到具備最小 F 值的方格時,僅取出表的第一項便可。我寫程序時,這是我用的第一個方法。
      
       對 於小地圖,這能夠很好的工做,但這不是最快的方案。追求速度的 A* 程序員使用了叫作二叉堆的東西,個人程序裏也用了這個。以個人經驗,這種方法在多數場合下會快 2—3 倍,對於更長的路徑速度成幾何級數增加 (10 倍甚至更快 ) 。若是你想更多的瞭解二叉堆,請閱讀 Using Binary Heaps in A* Pathfinding
2.       其 他單位:若是你碰巧很仔細的看了個人程序,你會注意到我徹底忽略了其餘單位。個人尋路者實際上能夠互相穿越。這取決於遊戲,也許能夠,也許不能夠。若是你 想考慮其餘單位,並想使他們移動時繞過彼此,我建議你的尋路程序忽略它們,再寫一些新的程序來判斷兩個單位是否會發生碰撞。若是發生碰撞,你能夠產生一個 新的路徑,或者是使用一些標準的運動法則(好比永遠向右移動,等等)直至障礙物不在途中,而後產生一個新的路徑。爲何在計算初始路徑是不包括其餘單位 呢?由於其餘單位是能夠動的,當你到達的時候它們可能不在本身的位置上。這能夠產生一些怪異的結果,一個單位忽然轉向來避免和一個已不存在的單位碰撞,在 它的路徑計算出來後和穿越它路徑的那些單位碰撞了。
在尋路代碼中忽略其餘單位,意味着你必須寫另外一份代碼來處理碰撞。這是遊戲的細節,因此我把解決方案留給你。本文末尾引用的 Bryan Stout's 的文章中的幾種解決方案很是值得了解。
3.       一 些速度方面的提示:若是你在開發本身的 A* 程序或者是改編我寫的程序,最後你會發現尋路佔用了大量的 CPU 時間,尤爲是當你有至關多的尋路者和一塊很大的地圖時。若是你閱讀過網上的資料,你會發現就算是開發星際爭霸,帝國時代的專家也是這樣。若是你發現事情由 於尋路而變慢了,這裏有些主意很不錯:
     使用小地圖或者更少的尋路者。
     千萬不要同時給多個尋路者尋路。取而代之的是把它們放入隊列中,分散到幾個遊戲週期中。若是你的遊戲以每秒 40 週期的速度運行,沒人能察覺到。可是若是同時有大量的尋路者在尋路的話,他們會立刻就發現遊戲慢下來了。
     考慮在地圖中使用更大的方格。這減小了尋路時須要搜索的方格數量。若是你是有雄心的話,你能夠設計多套尋路方案,根據路徑的長度而使用在不一樣場合。這也是專業人士的作法,對長路徑使用大方格,當你接近目標時使用小方格。若是你對這個有興趣,請看 Two-Tiered A* Pathfinding
     對於很長的路徑,考慮使用路徑點系統,或者能夠預先計算路徑並加入遊戲中。
     預 先處理你的地圖,指出哪些區域是不可到達的。這些區域稱爲「孤島」。實際上,他們能夠是島嶼,或者是被牆壁等包圍而不可到達的任意區域。 A* 的下限是,你告訴他搜尋通往哪些區域的路徑時,他會搜索整個地圖,直到全部能夠抵達的方格都經過 open list 或 close list 獲得了處理。這會浪費大量的 CPU 時間。這能夠經過預先設定不可到達的區域來解決。在某種數組中記錄這些信息,在尋路前檢查它。在個人 Blitz 版程序中,我寫了個地圖預處理程序來完成這個。它能夠提早識別尋路算法會忽略的死路徑,這又進一步提升了速度。
4.     不一樣的地形損耗:在這個教程和個人程序中,地形只有 2 種:可抵達的和不可抵達        的。可是若是你有些可抵達的地形,移動代價會更高些,沼澤,山丘,地牢的樓梯
       等都是可抵達的地形,可是移動代價比平地就要高。相似的,道路的移動代價就比        它周圍的地形低。
在你計算給定方格的 G 值時加上地形的代價就很容易解決了這個問題。簡單的給這些方格加上一些額外的代價就能夠了。 A* 算法用來查找代價最低的路徑,應該很容易處理這些。在個人簡單例子中,地形只有可達和不可達兩種, A* 會搜尋最短和最直接的路徑。可是在有地形代價的環境中,代價最低的的路徑可能會很長。
就像沿着公路繞過沼澤而不是直接穿越它。
另外一個須要考慮的是專家所謂的「 influence Mapping 」,就像上面描述的可變成本地形同樣,你能夠建立一個額外的計分系統,把它應用到尋路的 AI 中。假設你有這樣一張地圖,地圖上由個通道穿過山丘,有大批的尋路者要經過這個通道,電腦每次產生一個經過那個通道的路徑都會變得很擁擠。若是須要,你可 以產生一個 influence map ,它懲罰那些會發生大屠殺的方格。這會讓電腦選擇更安全的路徑,也能夠幫助它避免由於路徑短(固然也更危險)而持續把隊伍或尋路者送往某一特定路徑。
5.     維 護未探測的區域:你玩 PC 遊戲的時候是否發現電腦老是能精確的選擇路徑,甚至地圖都未被探測。對於遊戲來講,尋路過於精確反而不真實。幸運的是,這個問題很容易修正。答案就是爲每 個玩家和電腦(每一個玩家,不是每一個單位 --- 那會浪費不少內存)建立一個獨立的 knownWalkability 數組。每一個數組包含了玩家已經探測的區域的信息,和假設是可到達的其餘區域,直到被證明。使用這種方法,單位會在路的死端徘徊,並會作出錯誤的選擇,直到 在它周圍找到了路徑。地圖一旦被探測了,尋路又向日常同樣工做。
6.     平滑路徑: A* 自動給你花費最小的,最短的路徑,但它不會自動給你最平滑的路徑。看看咱們的例子所找到的路徑(圖 7 )。在這條路徑上,第一步在起點的右下方,若是第一步在起點的正下方是否是路徑會更平滑呢?
       有幾個方法解決這個問題。在你計算路徑時,你能夠懲罰那些改變方向的方格,把它的 G 值增長一個額外的開銷。另外一種選擇是,你能夠遍歷你生成的路徑,查找那些用相鄰的方格替代會使路徑更平滑的地方。要了解更多,請看 Toward More Realistic Pathfinding
7.     非 方形搜索區域:在咱們的例子中,咱們使用都是 2D 的方形的區域。你可使用不規則的區域。想一想冒險遊戲中的那些國家,你能夠設計一個像那樣的尋路關卡。你須要創建一張表格來保存國家相鄰關係,以及從一個 國家移動到另外一個國家的 G 值。你還須要一個方法了估算 H 值。其餘的均可以向上面的例子同樣處理。當你向 open list 添加新項時,不是使用相鄰的方格,而是查看錶裏相鄰的國家。
相似的,你能夠爲一張固定地形的地圖的路徑創建 路徑點系統。路徑點一般是道路或地牢通道的轉折點。做爲遊戲設計者,你能夠預先設定路徑點。若是兩個路徑點的連線沒有障礙物的話它們被視爲相鄰的。在冒險 遊戲的例子中,你能夠保存這些相鄰信息在某種表中,當 open list 增長新項時使用。而後記錄 G 值(可能用兩個結點間的直線距離)和 H 值(可能使用從節點到目標的直線距離)。其它的都想往常同樣處理。
進一步閱讀(Further Reading)
Ok ,如今你已經對 A* 有了個基本的瞭解,同時也認識了一些高級的主題。我強烈建議你看看個人代碼,壓縮包裏包含了 2 個版本的實現,一個是 C++ ,另外一個是 Blitz Basic 。 2 個版本都有註釋,你以該能夠很容易就看懂。下面是連接:
若是你不會使用 C++ 或是 BlitzBasic ,在 C++ 版本下你能夠找到兩個 exe 文件。 BlitzBasic 版本必須去網站 Blitz Basic 下載 BlitzBasic 3D 的免費 Demo 才能運行。 在這裏 here 你能夠看到一個 Ben O'Neill 的 A* 在線驗證明例。
你應該閱讀下面這幾個站點的文章。在你讀完本教程後你能夠更容易理解他們。
Amit's A* Pages Amit Patel 的這篇文章被普遍引用,可是若是你沒有閱讀本教程的話,你可能會感到很迷惑。尤爲是你能夠看到 Amit Patel 本身的一些想法。
Smart Moves: Intelligent Path Finding Bryan Stout 的這篇須要去 Gamasutra.com 註冊才能閱讀。 Bryan 用 Delphi 寫的程序幫助我學習了 A* ,同時給了我一些個人程序中的一些靈感。他也闡述了 A* 的其餘選擇。
Terrain Analysis Dave Pottinger 一篇很是高階的,有吸引力的文章。他是 Ensemble Studios 的一名專家。這個傢伙調整了遊戲帝國時代和王者時代。不要指望可以讀懂這裏的每同樣東西,可是這是一篇能給你一些不錯的主意的頗有吸引力的文章。它討論了 包 mip-mapping ,
influence mapping ,和其餘高階 AI 尋路主題。他的 flood filling 給了我在處理死路徑 」dead ends」 和孤島 」island」 時的靈感。這包含在個人 Blitz 版本的程序裏。
下面的一些站點也值得去看看:
·                     aiGuru: Pathfinding
·                     Game AI Resource: Pathfinding
·                     GameDev.net: Pathfinding
相關文章
相關標籤/搜索