C#實現A*尋路
網上有關A*演算法的文章已經非常豐富了,各種語言各種思路都有,本來我實在不想再寫一篇,,但因為最近工作動盪因此專門抽空又看了一下,然後就想寫個文章防止以後印象模糊,好歹看自己寫的東西可以很快回憶起來。如果是初次接觸A*演算法的朋友可以先看看這篇參考文章,我這邊只是做一個總結,然後先貼上我之前的筆記吧:
二維陣列map資料:
0=可通過的格子,S=起點,E=終點,B=障礙物
基礎定義:G=目標點到起點的步長總和 H=目標點到終點的步長總和(曼哈頓距離),F=兩總和之和
定義:OpenList,CloseList,current,adjacentCube,g(步長,這個例子假定為1)
1.將起點A加入到OpenList中,遍歷OpenList,每次取最小F值的item,將其賦值給current,隨後將這個item從OpenList中刪除,加入到CloseList(例子中在這裡遍歷CloseList中是否存在目標點,有的話則立刻break,我個人覺得沒必要每次都遍歷,直接判斷取到的current是否目標路徑即可)。
2.獲取到該item四周的item(需要判斷map該位置是否為"0"或"E",否則過濾,因為障礙物和邊緣不能加入尋路OpenList中),設定X和Y並insert入adjacentCube中。
3.遍歷adjacentCube,檢查每個item是否已存在於OpenList中,兩種情況:
(1.)已存在OpenList中,則判斷如果current的G值+g步長和大於這個item的G值的話,則將這個item的parent置為current,同時將這個item的G值更新為current父物體的G值+g步長(主要是找一個相對最短的路徑,因為最短路徑最終需要從終點通過parent回溯,而不是正向尋找一遍就結束了)。
(2.)未存在OpenList中,則設定這個item的各種屬性值(G,H,F)以及parent,然後將其insert入OpenList中。
4.重複以上步驟,直到找到目標點則break隨後開始回溯輸出。回溯的過程比較簡單,因為之前有記錄parent,所以當前current的parent已經記錄了一條最短的路徑。
然後如果你看過原文章的程式碼的話可能會發現那個變數g會有問題,原始碼每次OpenList遍歷一次g值就會+1,但是有可能會有一種情況就是順著一個格子C的周圍的其中一條路徑走了一段發現是錯誤的(即F值不再是OpenList中最小的),這時需要繼續從OpenList中重新找最小F值的item繼續找找新的路徑,假如此時找到了之前C格子周圍的另一個格子的F值最小,此時就需要從這裡重新走新的路徑,新找到的這個格子的G值當然也應該重新變回之前C格子的G+g步長(這個例子中步長為1),而不是一直自加。下面我會上我修改後的程式碼,在修改的地方都有加註釋,你可以去掉原始碼的註釋然後註釋掉我的程式碼測試一下兩者的區別,我這裡上一下圖,在我之前說的這個例子中兩個程式碼的執行結果:
原始碼:修改後:
可以看到搜尋的路徑差距很大,我的這個執行結果我能看懂,因為從起點到終點最大的F值只有80,而周圍檢索不到的點F值都達到了100所以不遍歷,但是原始碼的這個。。遍歷了這麼大的範圍說實話我是有點蒙圈的,可能是因為原文章作者的那個路徑比較簡單看不太出問題,我大概想了一會沒想通,就先不糾結啦,反正肯定是和g值這個問題脫不了干係的。
然後貼一下當時實驗的程式碼,其實只是整理了一下那篇參考文章的程式碼,這裡只關注演算法所以並沒有考慮其他東西:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AStarPathFindingTest { class Location { public int X; public int Y; public int F; public int G; public int H; public Location Parent; } class Program { static string[] map = new string[] { "+--------+", "|00000000|", "|00000000|", "|0000X000|", "|00S0X0E0|", "|0000X000|", "|00000000|", "|00000000|", "|00000000|", "+--------+", }; static void DrawMapToConsole() { for (var i = 0; i < map.Length; ++i) { Console.WriteLine(map[i]); } } static void Main(string[] args) { DrawMapToConsole(); Location current = null; var start = new Location { X = 3, Y = 4 }; var target = new Location { X = 7, Y = 4 }; var openList = new List<Location>(); var closedList = new List<Location>(); //修改處,g變為只表示步長 //int g = 0; int g = 1; // start by adding the original position to the open list openList.Add(start); while (openList.Count > 0) { // algorithm's logic goes here // get the square with the lowest F score var lowest = openList.Min(l => l.F); current = openList.First(l => l.F == lowest); // add the current square to the closed list closedList.Add(current); // remove it from the open list openList.Remove(current); // show current square on the map Console.SetCursorPosition(current.X, current.Y); Console.Write('.'); Console.SetCursorPosition(current.X, current.Y); ///System.Threading.Thread.Sleep(1000); // if we added the destination to the closed list, we've found a path if (closedList.FirstOrDefault(l => l.X == target.X && l.Y == target.Y) != null) break; var adjacentSquares = GetWalkableAdjacentSquares(current.X, current.Y, map); //修改處,不能這樣用g // g++; foreach (var adjacentSquare in adjacentSquares) { // if this adjacent square is already in the closed list, ignore it if (closedList.FirstOrDefault(l => l.X == adjacentSquare.X && l.Y == adjacentSquare.Y) != null) continue; // if it's not in the open list... if (openList.FirstOrDefault(l => l.X == adjacentSquare.X && l.Y == adjacentSquare.Y) == null) { // compute its score, set the parent //修改處,不能這樣用g //adjacentSquare.G = g; adjacentSquare.G = g + current.G; adjacentSquare.H = ComputeHScore(adjacentSquare.X, adjacentSquare.Y, target.X, target.Y); adjacentSquare.F = adjacentSquare.G + adjacentSquare.H; adjacentSquare.Parent = current; // and add it to the open list openList.Insert(0, adjacentSquare); } else { // test if using the current G score makes the adjacent square's F score // lower, if yes update the parent because it means it's a better path //修改處 if (/*g*/ current.G + g < adjacentSquare.G) { //修改處,不能這樣用g //adjacentSquare.G = g; adjacentSquare.G = g + current.G; adjacentSquare.F = adjacentSquare.G + adjacentSquare.H; adjacentSquare.Parent = current; } } } } // assume path was found; let's show it while (current != null) { Console.SetCursorPosition(current.X, current.Y); Console.Write('_'); Console.SetCursorPosition(current.X, current.Y); current = current.Parent; //System.Threading.Thread.Sleep(1000); } Console.ReadKey(); } static List<Location> GetWalkableAdjacentSquares(int x, int y, string[] map) { var proposedLocations = new List<Location>() { new Location { X = x, Y = y - 1 }, new Location { X = x, Y = y + 1 }, new Location { X = x - 1, Y = y }, new Location { X = x + 1, Y = y }, }; return proposedLocations.Where( l => map[l.Y][l.X] == '0' || map[l.Y][l.X] == 'E').ToList(); } static int ComputeHScore(int x, int y, int targetX, int targetY) { return Math.Abs(targetX - x) + Math.Abs(targetY - y); } } }
最後說一下上面這些只是最簡單的用曼哈頓距離+4方向實現的A*演算法,因為之前專案也只有4方向,另外還有八方向,斜邊的g值可能會跟平行豎直移動的g值步長不同,例如平行豎直移動g值步長為10,那斜方向可能為15。然後還有地形的不同也會影響g步長的不同,這些都算是特殊需求,如果懂了A*演算法本身的話做這些修改應該不會很難。還有H值得計算方法除了曼哈頓距離,還有對角線距離,歐幾里得距離等很多計算方法,尋路也分廣度優先和深度優先。還有二叉樹的a*尋路等等等等。。。這些如果到時候做專案的時候有需求我就再整理相關文章。另外還有一篇比較有意思的參考文章,這個系列主要說明和解決了一些使用a*尋路時遇到的一些坑,大概看了一下感覺對以後入坑時會很有幫助,所以一併貼出來。