原文地址:http://www.redblobgames.com/pathfinding/a-star/introduction.html
如果想嘗試文中的小程序,請點擊上述鏈接,找到對應畫面即可。
在游戲中我們想要找到從一個位置到另一個位置的路徑。我們不僅嘗試着找到最短距離的路徑;我們還想要顧忌到消耗的時間。在一張地圖上,穿過一片池塘速度會明顯減慢,所以我們想要找到一條如果可以的話,繞過水路的路徑。這是一個互動的圖。在地圖上單擊去通過地面,草地,沙灘,水池以及牆/樹。
如何去計算這樣一條路徑呢?A*算法在游戲領域是最常見的算法。它是圖形搜索算法家族中遵循相同結構的一員。這些算法的特征是:地圖化作一張圖結構,然后在圖中查找路徑。如果你之前不了解節點-邊緣結構的圖的話,這里是我的介紹性文章(http://www.redblobgames.com/pathfinding/grids/graphs.html)。在這個文章中,圖節點將定位在地圖中。廣度優先算法是一種最簡單的圖搜索算法,所以我們就從它開始,然后以我們的方式講述A*。
這些算法的關鍵思想是:我們跟蹤一個叫做前驅的擴張圈。開始這個動畫去觀看前驅如何擴張。
The expanding frontier can be viewed as contour lines that stop at walls; this process is sometimes called “flood fill”:
這個擴張的前驅可以被看做是一個停在牆邊的等高線;這個過程有時候也被稱為“泛洪填充”(用詞不當,多多見諒)。
我們該怎樣去實現這個算法呢?重復這些步驟知道“前驅frontier”為空:
1.從前驅中挑選並且移除一個位置;
2.為訪問過的位置打上標志,讓我們可以知道以后不會再去訪問這個重復的節點。
3.通過它的鄰居節點來擴張它。任何一個我們沒有訪問過的節點添加到前驅節點序列中。
讓我們來仔細的看看這個過程。這些區塊被按照我們訪問的順去來排列序號。一步一步前進來觀察這個過程:
python寫的該算法只有10行(如下所示):
frontier = Queue()
frontier.put(start) visited = {} visited[start] = True while not frontier.empty(): current = frontier.get() for next in graph.neighbors(current): if next not in visited: frontier.put(next) visited[next] = True
程序中的這個循環結構是這個網頁中的圖形搜索算法中的本質,也包括A*算法。但是我們怎樣才能找到最短的路徑呢?這個循環事實上不能構造出這樣的路徑;它僅僅告訴我們怎么去訪問圖中的每一個節點。那是因為廣度優先搜索算法能夠被用在更多的地方,而不僅僅是搜索路徑;再這篇文章中我展示了它是如何應用在塔防游戲中的,但是它也可以被用在距離地圖以及程序地圖生成中,以及大量的其他方面。這里,雖然我們想要使用它去查找路徑,所以,讓我們來修改這個循環(跟蹤我們過來時訪問過的每一個位置,並且為這些之前走過的節點打上visited標志):
frontier = Queue()
frontier.put(start) came_from = {} came_from[start] = None while not frontier.empty(): current = frontier.get() for next in graph.neighbors(current): if next not in came_from: frontier.put(next) came_from[next] = current
構造這條路徑的代碼也是非常簡單:
current = goal path = [current] while current != start: current = came_from[current] path.append(current)
那就是最簡單的路徑查找算法。它不僅可以實現在網格中的路徑查找,也可以應用在任何圖結構序列上。在一個地下城中,圖形位置可能是房間和圖形邊沿之間的門道。
在一個平台游戲中,圖形位置可能是位置和圖形邊緣的可能的操作,如左移,右移,跳起來,跳下去。在一般情況下,認為圖作為狀態和改變狀態的行動。
我有更多的寫了地圖表示在這里(http://theory.stanford.edu/~amitp/GameProgramming/MapRepresentations.html)。
在本文的其余部分,我會繼續使用有網格的例子,並探討為什么你可能會使用廣度優先搜索的變種。
Early exit
We’ve found paths from one location to all other locations. Often we don’t need all the paths; we only need a path from one location to one other location. We can stop expanding the frontier as soon as we’ve found our goal. Drag the X around see how the frontier stops expanding as soon as it reaches the X.
我們已經找出了從一個位置到另一個位置的路徑。通常我們不需要所有的路徑;我們只需要其中的一條路經。我們只要盡可能快的找到我們的目標,就可以停止擴張這個“前驅”。四處拖拽X,並且觀看前驅是如何在到達X處 立刻停止擴張的。
frontier = Queue()
frontier.put(start) came_from = {} came_from[start] = None while not frontier.empty(): current = frontier.get() if current == goal: break for next in graph.neighbors(current): if next not in came_from: frontier.put(next) came_from[next] = current
Movement costs
移動的代價
到目前為止,我們都是假定移動的消耗是一樣的。在一些路徑查找方案中,對於不同類型的移動有着不同的代價消耗。例如,在市區,通過平原或沙漠可能消耗1元(為了簡單,咱們用“元”作為單位),但是通過森林或者小山可能會消耗5元。在這網頁的頂端地圖中,穿過河流需要10倍或者更多的代價比通過草地。另一個例子是:網格中的對角線移動比軸向(繞着走)移動消耗更多。我們想要將這些消耗考慮進去,然后再計算查找路徑。讓我們來比較從起始位置的步數以及從起始位置的距離:
這里我們想用Dijkstra算法來解決這個問題。它跟廣度優先算法有什么不同呢?我們需要跟蹤它們的移動代價,所以讓我們來添加新的變量cost_so_far,去跟蹤記錄從起始位置開始總共的移動消耗代價。我們想要將移動消耗代價考慮進去,然后決定怎樣評估位置;讓我們把隊列轉為優先級隊列。不太明顯的,我們最終可能會訪問一個位置多次,不同的成本,所以我們需要一點點的改變邏輯。不要再 在遇到沒有訪問過的節點情況下添加該節點到前驅序列中,而是要 在到目標位置的新路徑優於之前的路徑的情況下才將該節點添加進去。
frontier = PriorityQueue()
frontier.put(start, 0) came_from = {} cost_so_far = {} came_from[start] = None cost_so_far[start] = 0 while not frontier.empty(): current = frontier.get() if current == goal: break for next in graph.neighbors(current): new_cost = cost_so_far[current] + graph.cost(current, next) if next not in cost_so_far or new_cost < cost_so_far[next]: cost_so_far[next] = new_cost priority = new_cost frontier.put(next, priority) came_from[next] = current
使用一個優先級隊列,