A* (路徑搜索)算法導引

前言:
A*算法是路徑搜索中的經典算法,也是公認的最優算法之一,網上找到一篇文章講的很好,適合入門,所以翻譯了一下,沒有完全參照原文,主要還是意譯,網上也有其他人翻譯好的,個人覺得還是自己來一遍比較好,讀者自斟:
原文
開始:搜索區域
這裏假設有人要從A點去往B點,另外有一面牆將兩者分開。如下圖所示,綠色方塊代表A點,紅色方塊代表B點,藍色代表分開他們的牆。
    
圖一

 

首先要注意的是我們這裏先將整個搜索區域網格化,以方便路徑搜索。這一方法將搜索區域簡化爲一個簡單的2爲數組。數組的每個元素代表網格上的一片區域,並且標記爲可通過或不可通過兩種狀態。路徑由從A點到達B點所經過的方塊來表示。
這些點被稱爲節點。之所以不直接叫方塊是因爲區域可以被劃分爲矩形,六邊形,等。而點也不一定是在中心,可以在邊緣或其他地方。

開始搜索

一旦我們如上圖將搜索區域網格化,將之轉化爲可操作數量的節點,然後下一步就是找到一個搜索算法找出最短路徑。我們從A點出發,搜索遞歸周圍領域,直到到達目標點

開始搜索的步驟如下:
   1.   從A點出發並且將其放入到“候選列表”備用。候選列表就像就像是購物清單。現在上面只有一項在清單上,但我們會陸續添加。其中包含一些潛在的路徑點。基本上,這個列表包含有待確認的方塊。
   2.   找到與起始點相鄰的所有可到達方塊,忽略那些牆,水等其他非法區域。將這些點加入到“候選列表”,存入方法與先前父方塊A的存入方法相同。這些父方塊會在我們進行路徑跟蹤回溯的時候派上用場。
   3.   從“候選列表”中將A方塊刪除,並將其加入到“排除列表”,表明在此列表中的方塊不需要再去關注。
 
關於這點,如下圖所示,中間的方塊是起始點,它周圍的亮藍色邊框表明此方塊一經被加入到“排除列表”。所有在“候選列表”上的方塊都需要進一步檢查,他們的邊框呈現亮綠色。每一個裏面都有一個箭頭指向其父節點,這裏是起始方框。


  圖2

接下來,我們在選擇“候選列表”中的一個領域然後大致上重複剛纔的步驟,如下所述。然後選擇擁有最小F值的那個方塊

路徑評估

在路徑選擇是關鍵指標等式描述如下:

F = G + H 

這裏:

  • G= 表示從A點到當前點的所經過的路徑長度
  • H=從當前點到目標點B的估計路徑長度。這經常被描述爲“啓發式”,有時這樣會引起歧義。之所以這麼叫是因爲這僅僅是一種猜測。事實上在找到路徑之前我們並不知道實際路徑長度,因爲會有如牆,水等東西阻擋。這裏給出一種計算H的方法,同時在網上有許多其他計算H值的方法。

我們的路徑正式不斷的遍歷“候選列表”並選擇最小F值的點。這個過程會在稍後詳細說明。首先來了解我們是如何計算等式的。

如上所述,G是利用先前產生的路徑到達當前點所得長度。此例中,出於簡化目的,我們定義單元格之間的距離爲10,對角單元格距離爲14,同時這樣做也會大大減少計算量。

有多中計算方法。這裏所使用的方法是曼哈頓方法,該方法累計從當前點到目標點在水平方向和垂直方向(忽略對角線方向)所要經過的所有方塊數,同時忽略可能遇到的障礙。然後將結果乘以10(單位距離),之所以成爲曼哈頓方法,是因爲這就像要數從一個地方到另一個地方要走過多少個街區,而同時你有不能斜穿街區。

看完這個描述,你或許會認爲所謂的“啓發式”不過是將剩餘的路徑長度粗略的估計爲兩點間的直線距離。事實上並非如此。我們正是在試圖估計實際剩餘距離(一般會比實際距離更長),與實際距離越接近,算法越快速,如果估計值過大,就會有可能出現找出的路徑不是最短路徑的情況。這種情況我們稱之爲“不可接受啓發式”
第一步搜索的F,G,H可表示如下圖

   
圖三

繼續搜索


繼續搜索,我只是從“候選列表”選出最小F值的方塊,然後執行如下操作

4) 將其從“候選列表”中刪除加入到“排除列表”

5) 檢查其領域內方塊,排除不可通過方塊,並且將沒有在“候選列表”中的方塊加入到“候選列表”。是當前選中方塊成爲父方塊。

6) 如果領域內存在方塊在“候選列表中”中,那麼就檢查是否從當前點經由那個候選方塊是一條更好路徑。換句話說,檢查那個方塊的G值是否會因爲我們使用了當前點到達那裏而變小,如果不是,什麼都不做,另一方面,如果新的路徑G值變小了,就將那個候選方塊的父節點改爲當前點。最後重新計算那個候選節點的F和G值

譯者注:
這一步是A*算法較爲難理解的一步,這一步的目的在於優化從起始點到候選列表中的點路徑,因爲在H函數既定的情況下,候選列表中H值就是一定的,此時優化G值可以幫助找到最小的F值。
    

現在來看一下,在候選列表中的8個方塊中,擁有最小F值的是右邊中間的方塊,所以選擇它爲下一個方塊,在圖解中該方塊邊緣以藍色高亮。

    
圖4

首先將其從“候選列表”中刪除,加入到“排除列表”。然後檢查其領域,右邊和左邊中間的方塊都被排除。
領域餘下的4個方塊都已經在“候選列表”中,所以我們需要檢查是否從當前點經由那個候選方塊是一條更好路徑。計算可以經由當前點並不能改善原有候選列表中方塊的G值。所以什麼都不做
譯者注:G是從起始點到所計算點所經過路徑點的G值的和

接下來繼續搜索候選列表,找到最小的F值的方塊有兩個,分別位於右上和右下,這裏其實選擇哪個沒有固定標準,我們選擇後加入“候選列表”的那個方塊作爲當前點,這樣做的結果是我們總是優先計算新加入的F值較的候選點,在大部分情況下這有助於我們更快的到達目的地。同樣這裏採用不同的篩選方法會導致不同的最終路徑但擁有相同的路徑長度。

所以就選擇右下那個方塊

  
圖5

 

這次,我們排除當前方塊右邊的三個方塊,至於右下那個被排除,則是這裏選擇的穿透策略導致,你也可以選擇其他的穿透策略。
重複剛纔的步驟知道我們將目標嗲加入到“排除列表”,如下圖所示:

    
圖6

那麼我們要如何來選擇路徑呢?答案很簡單,我們只要從目標點開始不斷的沿着其父方塊就可以找回到起始方塊。如圖中紅點方塊所示。

    
圖7

A*算法總結

現在將整個步驟總結如下:

1) 將起始點加入到“候選列表”

2) 重複如下步驟:

a) 搜索候選列表的最小F值,將其選爲當前點

b) 放入“排除列表”

c) 對於與其相鄰的8個方塊

  • 如果不可通過或其在“排除列表”中,就忽略掉,否則執行下面步驟。

  •  如果其不在“候選列表”,將他加入到“候選列表”,將當前點作爲其父節點,計算F,G,H

  • 如果領域內存在方塊在“候選列表中”中,那麼就檢查是否從當前點經由那個候選方塊是一條更好路徑。換句話說,檢查那個方塊的G值是否會因爲我們使用了當前點到達那裏而變小,如果不是,什麼都不做,另一方面,如果新的路徑G值變小了,就將那個候選方塊的父節點改爲當前點。最後重新計算那個候選節點的F和G值

d) 終止條件:

  • 目標方塊被加入到“排除類表”,這種情況下路徑被找到
  • 在“候選列表”中的點全部被放到了“排除列表中”,此時路徑查找失敗,沒有通路。

3)保存路徑,從目標點開始不斷的沿着其父方塊就可以找回到起始方塊。

Note: In earlier versions of this article, it was suggested that you can stop when the target square (or node) has been added to the open list, rather than the closed list.  Doing this will be faster and it will almost always give you the shortest path, but not always.  Situations where doing this could make a difference are when the movement cost to move from the second to the last node to the last (target) node can vary significantly -- as in the case of a river crossing between two nodes, for example.

 

 

 

上面這段紅色話直譯好理解,但是我還沒有想到作者所謂兩個節點中間的一條河這種情況是如何影響終止條件的,在我看來終止條件是隻要將目標點加入“候選列表”中就可以了,知道的達人說一聲,最好有圖解。謝謝
嘮叨幾句

網上有許多號稱是A*算法的,但其實不是,A*算法被認爲是在大多數情況下最好的算法,有如下構成要件:
“候選列表”
“排除列表”
評估等式 F=G+H

 

實現算法的注意事項:
以下與算法本身關係不大暫時不翻譯了,有興趣自己看一下

Now that you understand the basic method, here are some additional things to think about when you are writing your own program. Some of the following materials reference the program I wrote in C++ and Blitz Basic, but the points are equally valid in other languages.

 

 

1.  Other Units (collision avoidance): If you happen to look closely at my example code, you will notice that it completely ignores other units on the screen. The units pass right through each other. Depending on the game, this may be acceptable or it may not. If you want to consider other units in the pathfinding algorithm and have them move around one another, I suggest that you only consider units that are either stopped or adjacent to the pathfinding unit at the time the path is calculated, treating their current locations as unwalkable. For adjacent units that are moving, you can discourage collisions by penalizing nodes that lie along their respective paths, thereby encouraging the pathfinding unit to find an alternate route (described more under #2). 

If you choose to consider other units that are moving and not adjacent to the pathfinding unit, you will need to develop a method for predicting where they will be at any given point in time so that they can be dodged properly. Otherwise you will probably end up with strange paths where units zig-zag to avoid other units that aren't there anymore. 

You will also, of course, need to develop some collision detection code because no matter how good the path is at the time it is calculated, things can change over time. When a collision occurs a unit must either calculate a new path or, if the other unit is moving and it is not a head-on collision, wait for the other unit to step out of the way before proceeding with the current path.

These tips are probably enough to get you started. If you want to learn more, here are some links that you might find helpful:

譯者注:看了以上這段我想起魔獸中那些小人在移動時遇到一起會停一下,然後排隊繼續運動,原因就在這裏。

2. Variable Terrain Cost: In this tutorial and my accompanying program, terrain is just one of two things – walkable or unwalkable. But what if you have terrain that is walkable, but at a higher movement cost? Swamps, hills, stairs in a dungeon, etc. – these are all examples of terrain that is walkable, but at a higher cost than flat, open ground. Similarly, a road might have a lower movement cost than the surrounding terrain.

This problem is easily handled by adding the terrain cost in when you are calculating the G cost of any given node. Simply add a bonus cost to such nodes. The A* pathfinding algorithm is already written to find the lowest cost path and should handle this easily. In the simple example I described, when terrain is only walkable or unwalkable, A* will look for the shortest, most direct path. But in a variable-cost terrain environment, the least cost path might involve traveling a longer distance – like taking a road around a swamp rather than plowing straight through it.

An interesting additional consideration is something the professionals call “influence mapping.” Just as with the variable terrain costs described above, you could create an additional point system and apply it to paths for AI purposes. Imagine that you have a map with a bunch of units defending a pass through a mountain region. Every time the computer sends somebody on a path through that pass, it gets whacked. If you wanted, you could create an influence map that penalized nodes where lots of carnage is taking place. This would teach the computer to favor safer paths, and help it avoid dumb situations where it keeps sending troops through a particular path, just because it is shorter (but also more dangerous).

Yet another possible use is penalizing nodes that lie along the paths of nearby moving units. One of the downsides of A* is that when a group of units all try to find paths to a similar location, there is usually a significant amount of overlap, as one or more units try to take the same or similar routes to their destinations. Adding a penalty to nodes already 'claimed' by other units will help ensure a degree of separation, and reduce collisions. Don't treat such nodes as unwalkable, however, because you still want multiple units to be able to squeeze through tight passageways in single file, if necessary. Also, you should only penalize the paths of units that are near the pathfinding unit, not all paths, or you will get strange dodging behavior as units avoid paths of units that are nowhere near them at the time. Also, you should only penalize path nodes that lie along the current and future portion of a path, not previous path nodes that have already been visited and left behind.

3. Handling Unexplored Areas: Have you ever played a PC game where the computer always knows exactly what path to take, even though the map hasn't been explored yet? Depending upon the game, pathfinding that is too good can be unrealistic. Fortunately, this is a problem that is can be handled fairly easily.

The answer is to create a separate "knownWalkability" array for each of the various players and computer opponents (each player, not each unit -- that would require a lot more computer memory). Each array would contain information about the areas that the player has explored, with the rest of the map assumed to be walkable until proven otherwise. Using this approach, units will wander down dead ends and make similar wrong choices until they have learned their way around. Once the map is explored, however, pathfinding would work normally.

4. Smoother Paths: While A* will automatically give you the shortest, lowest cost path, it won't automatically give you the smoothest looking path. Take a look at the final path calculated in our example (in Figure 7). On that path, the very first step is below, and to the right of the starting square. Wouldn't our path be smoother if the first step was instead the square directly below the starting square?

There are several ways to address this problem. While you are calculating the path you could penalize nodes where there is a change of direction, adding a penalty to their G scores. Alternatively, you could run through your path after it is calculated, looking for places where choosing an adjacent node would give you a path that looks better. For more on the whole issue, check out Toward More Realistic Pathfinding, a (free, but registration required) article at Gamasutra.com by Marco Pinter.

5. Non-square Search Areas: In our example, we used a simple 2D square layout. You don't need to use this approach. You could use irregularly shaped areas. Think of the board game Risk, and the countries in that game. You could devise a pathfinding scenario for a game like that. To do this, you would need to create a table for storing which countries are adjacent to which, and a G cost associated with moving from one country to the next. You would also need to come up with a method for estimating H. Everything else would be handled the same as in the above example. Instead of using adjacent squares, you would simply look up the adjacent countries in the table when adding new items to your open list.

Similarly, you could create a waypoint system for paths on a fixed terrain map. Waypoints are commonly traversed points on a path, perhaps on a road or key tunnel in a dungeon. As the game designer, you could pre-assign these waypoints. Two waypoints would be considered "adjacent" to one another if there were no obstacles on the direct line path between them. As in the Risk example, you would save this adjacency information in a lookup table of some kind and use it when generating your new open list items. You would then record the associated G costs (perhaps by using the direct line distance between the nodes) and H costs (perhaps using a direct line distance from the node to the goal). Everything else would proceed as usual.

Amit Patel has written a brief article delving into some alternatives. For another example of searching on an isometric RPG map using a non-square search area, check out my article Two-Tiered A* Pathfinding.

6. Some Speed Tips: As you develop your own A* program, or adapt the one I wrote, you will eventually find that pathfinding is using a hefty chunk of your CPU time, particularly if you have a decent number of pathfinding units on the board and a reasonably large map. If you read the stuff on the net, you will find that this is true even for the professionals who design games like Starcraft or Age of Empires. If you see things start to slow down due to pathfinding, here are some ideas that may speed things up: 

  • Consider a smaller map or fewer units.
  • Never do path finding for more than a few units at a time. Instead put them in a queue and spread them out over several game cycles. If your game is running at, say, 40 cycles per second, no one will ever notice. But they will notice if the game seems to slow down every once in a while when a bunch of units are all calculating paths at the same time.
  • Consider using larger squares (or whatever shape you are using) for your map. This reduces the total number of nodes searched to find the path. If you are ambitious, you can devise two or more pathfinding systems that are used in different situations, depending upon the length of the path. This is what the professionals do, using large areas for long paths, and then switching to finer searches using smaller squares/areas when you get close to the target. If you are interested in this concept, check out my article Two-Tiered A* Pathfinding.
  • For longer paths, consider devising precalculated paths that are hardwired into the game.
  • Consider pre-processing your map to figure out what areas are inaccessible from the rest of the map. I call these areas "islands." In reality, they can be islands or any other area that is otherwise walled off and inaccessible. One of the downsides of A* is that if you tell it to look for paths to such areas, it will search the whole map, stopping only when every accessible square/node has been processed through the open and closed lists. That can waste a lot of CPU time. It can be prevented by predetermining which areas are inaccessible (via a flood-fill or similar routine), recording that information in an array of some kind, and then checking it before beginning a path search.
  • In a crowded, maze-like environment, consider tagging nodes that don't lead anywhere as dead ends. Such areas can be manually pre-designated in your map editor or, if you are ambitious, you could develop an algorithm to identify such areas automatically. Any collection of nodes in a given dead end area could be given a unique identifying number. Then you could safely ignore all dead ends when pathfinding, pausing only to consider nodes in a dead end area if the starting location or destination happen to be in the particular dead end area in question.

7. Maintaining the Open List: This is actually one of the most time consuming elements of the A* pathfinding algorithm. Every time you access the open list, you need to find the square that has the lowest F cost. There are several ways you could do this. You could save the path items as needed, and simply go through the whole list each time you need to find the lowest F cost square. This is simple, but really slow for long paths. This can be improved by maintaining a sorted list and simply grabbing the first item off the list every time you need the lowest F-cost square. When I wrote my program, this was the first method I used. 

This will work reasonably well for small maps, but it isn’t the fastest solution. Serious A* programmers who want real speed use something called a binary heap, and this is what I use in my code. In my experience, this approach will be at least 2-3 times as fast in most situations, and geometrically faster (10+ times as fast) on longer paths. If you are motivated to find out more about binary heaps, check out my article, Using Binary Heaps in A* Pathfinding.

 

Another possible bottleneck is the way you clear and maintain your data structures between pathfinding calls. I personally prefer to store everything in arrays. While nodes can be generated, recorded and maintained in a dynamic, object-oriented manner, I find that the amount of time needed to create and delete such objects adds an extra, unnecessary level of overhead that slows things down. If you use arrays, however, you will need to clean things up between calls. The last thing you will want to do in such cases is spend time zero-ing everything out after a pathfinding call, especially if you have a large map. 

I avoid this overhead by creating a 2d array called whichList(x,y) that designates each node on my map as either on the open list or closed list. After pathfinding attempts, I do not zero out this array. Instead I reset the values of onClosedList and onOpenList in every pathfinding call, incrementing both by +5 or something similar on each path finding attempt. This way, the algorithm can safely ignore as garbage any data left over from previous pathfinding attempts. I also store values like F, G and H costs in arrays. In this case, I simply write over any pre-existing values and don't bother clearing the arrays when I'm done.

Storing data in multiple arrays consumes more memory, though, so there is a trade off. Ultimately, you should use whatever method you are most comfortable with.

8. Dijkstra's Algorithm: While A* is generally considered to be the best pathfinding algorithm (see rant above), there is at least one other algorithm that has its uses - Dijkstra's algorithm. Dijkstra's is essentially the same as A*, except there is no heuristic (H is always 0). Because it has no heuristic, it searches by expanding out equally in every direction. As you might imagine, because of this Dijkstra's usually ends up exploring a much larger area before the target is found. This generally makes it slower than A*. 

So why use it? Sometimes we don't know where our target destination is. Say you have a resource-gathering unit that needs to go get some resources of some kind. It may know where several resource areas are, but it wants to go to the closest one. Here, Dijkstra's is better than A* because we don't know which one is closest. Our only alternative is to repeatedly use A* to find the distance to each one, and then choose that path. There are probably countless similar situations where we know the kind of location we might be searching for, want to find the closest one, but not know where it is or which one might be closest.

Further Reading

If you do not have access to C++ or Blitz Basic, two small exe files can be found in the C++ version. The Blitz Basic version can be run by downloading the free demo version of Blitz Basic 3D (not Blitz Plus) at the Blitz Basic web site.

You should also consider reading through the following web pages. They should be much easier to understand now that you have read this tutorial.

Amit’s A* Pages: This is a very widely referenced page by Amit Patel, but it can be a bit confusing if you haven’t read this article first. Well worth checking out. See especially Amit's own thoughts on the topic. Smart Moves: Intelligent Path Finding: This article by Bryan Stout at Gamasutra.com requires registration to read. The registration is free and well worth it just to reach this article, much less the other resources that are available there. The program written in Delphi by Bryan helped me learn A*, and it is the inspiration behind my A* program. It also describes some alternatives to A*. Terrain Analysis: This is an advanced, but interesting, article by Dave Pottinger, a professional at Ensemble Studios. This guy coordinated the development of Age of Empires and Age of Kings. Don’t expect to understand everything here, but it is an interesting article that might give you some ideas of your own. It includes some discussion of mip-mapping, influence mapping, and some other advanced AI/pathfinding concepts.

Some other sites worth checking out:

aiGuru: Pathfinding Game AI Resource: Pathfinding GameDev.net: Pathfinding

 

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