C#實現AStar尋路演算法

来源:https://www.cnblogs.com/shiyidu/archive/2018/01/31/8387872.html
-Advertisement-
Play Games

AStar尋路演算法是一種在一個靜態路網中尋找最短路徑的演算法,也是在游戲開發中最常用到的尋路演算法之一;最近剛好需要用到尋路演算法,因此把自己的實現過程記錄下來。 先直接上可視化之後的效果圖,圖中黑色方格代表障礙物,綠色的方格代表最終路線,紅色方格為關閉列表,藍色方格為開啟列表;關於這一部分我會在稍後詳細 ...


AStar尋路演算法是一種在一個靜態路網中尋找最短路徑的演算法,也是在游戲開發中最常用到的尋路演算法之一;最近剛好需要用到尋路演算法,因此把自己的實現過程記錄下來。

先直接上可視化之後的效果圖,圖中黑色方格代表障礙物,綠色的方格代表最終路線,紅色方格為關閉列表,藍色方格為開啟列表;關於這一部分我會在稍後詳細敘述。(可視化的實現部分我就不討論了,這一篇主要說一下演算法的實現)

 

一、演算法原理

在描述具體演算法邏輯之前,需要先理解幾個基本概念:

    • 啟髮式搜索:聽起來很炫酷,其實很簡單;想象你在一個九宮格的中間,你現在需要走到九宮格的右上角;這個時候你的第一步有四個選擇:上下左右。雖然你還不知道具體路徑怎麼走,但你知道左邊和下邊距離終點似乎更遠,所以你會優先選擇先往右走或者先往上走。這就是啟髮式搜索——優先搜索最有可能產生最佳路徑的節點;通過啟髮式搜索,可以有效減少不必要的搜索。
    • 估價函數:上面說到啟髮式搜索會優先搜索最有可能產生最佳路徑的節點,那麼估價函數的作用就是對節點與終點的距離進行預估。預估距離最短的那個節點,就是目前最有可能產生最佳路徑的節點。
    • 開啟列表:當估價函數對一個節點估價完畢後,這個節點就會被放入開啟列表中。那麼開啟列表中的所有節點就是下一步所有可能被搜索的節點
    • 關閉列表:當演算法對開啟列表中最有可能是最佳路徑的節點搜索完畢後,會將這個節點放入關閉列表。那麼關閉列表中的所有節點就是已經搜索完的所有路線的節點

接下來,我用一個簡單的例子來描述演算法的邏輯。

 

首先,假設我們有一個4*4的方格,左下角坐標(0,0),右上角坐標(3,3),黑色格子為障礙物;這個時候時候我們需要尋找點(0,1)到點(3,1)的最短路線;這個時候我們把起點加入開啟列表(藍色格子),即所有下一步可能被搜索的節點。

 

接下來我們要對開啟列表進行搜索了,這個時候開啟列表只有一個格子即起點,因此我們對起點進行搜索:

  1. 這個時候我們把起點作為當前節點(即當前正在搜索的節點),然後找出當前節點所有下一步可能的節點,把他們加入開啟列表(藍色格子),表示這些都是下一步可能被搜索的節點。
  2. 這個時候我們要對所有新增的節點用估價函數進行估價,估價函數的表達式為f(n)=g(n)+h(n), 其中g(h)代表節點n到起點的實際距離,h(n)即啟發值,代表節點n到終點的預估距離,以節點(0,2)為例子:
    • g:可以看到,(0,2)從起點往上移動了一格,假設一個格子邊長為10,那麼當前格子到起點的距離為10,因此g值為10,我們把它標在格子左下角。
    • h:h有很多種估價的方式,在這裡我們就直接取 忽略障礙物直接走到終點的距離;如圖所示,該節點若要直接走到終點,它的路線為:右-右-右下,那麼它的h值就是:10+10+14 = 34;我們把它標記在格子右下角。
    • f:f為包含當前節點的路線的預估總距離,即g+h = 10+34 = 44,我們把它標在格子左上角。
    • 父節點:父節點表示當前節點的估價是由哪個節點產生的,或者當前節點是從哪個節點走過來的;因此它的父節點為(0,1),我們用一個箭頭指向(0,1),表示(0,1)是(0,2)的父節點。
  3. 對所有下一步可能的節點估價完畢後,我們將當前節點即起點從開啟列表移動到關閉列表中(紅色方格),表示我們對這個節點已經搜索完畢;結果如下圖所示。

 

接下來我們繼續對開啟列表進行搜索,經過上一步後,我們的開啟列表有三個節點;這個時候我們尋找f值最小的節點對它進行搜索,可以看到坐標為(1,0)的節點有著最小的f值38;因此節點(1,0)是目前最有可能產生最佳路線的節點,我們把它作為當前節點進行搜索:

  1. 這個時候我們重覆上一步的動作,找出當前節點所有下一步可能的節點,並對它們進行估價
  2. 在找出節點的過程中,我們發現它左邊的節點是它下一步可能的新節點(0,0)之一,但是左邊的節點已經存在於開啟列表中了;那我們是否讓新節點覆蓋舊節點呢?這個時候我們如果對新產生的節點(0,0)進行估價,我們會發現新產生的節點h值不變,g值為24,f值為24+34 = 58;我們發現新節點的f值58大於舊節點的f值44,那麼我們可以知道新節點所產生的路線總距離大於老節點產生的路線的總距離,因此我們選擇保留老節點。(當然如果我們發現新節點所產生的f值小於老節點所產生的f值,我們就要用新節點覆蓋老節點,因為新節點所產生的路線總距離更近)
  3. 估價完畢後,我們對把當前節點從開啟列表移動到關閉列表中(紅色方格),結果如下圖

 

這個時候我們發現最小f值的節點有兩個,我們隨便選一個(2,1)繼續搜索,可以得到下圖的結果:

 

這個時候依然有兩個節點f值最小,我們隨便選一個(3,1),把它作為當前節點,繼續搜索;這個時候我們發現當前節點位置就是終點的位置,我們按照箭頭線路走到起點,於是我們終於找到了路線,如下圖所示:

  

 

於是我們可以總結一下Astar尋路演算法的演算法邏輯(偽代碼):

  new openList;

  new closeList;

  openList.Add(start); //把起點加入開啟列表

    loop{

      currentNode = lowest f cost in openList;//把開啟列表中f值最低的節點作為當前節點

      if (currentNode == end) return; //找到路線

      foreach(neighbour in currentNode.Neighbours){ //對所有當前節點的相鄰節點進行迴圈

        if (closeList.Contains(neighbour) or neighbour == obstacle ) continue; //跳過關閉列表中的節點和障礙物節點

        if( new fCost <= old fCost || !openList.Contains(neighbour) ){ //如果新節點的f值小於老節點的f值,用新節點替換老節點

          neighbour.fCost = new fCost;

          neighbour.parent = currentNode;

          if(!openList.Contains(neighbour)) openList.Add(neighbour);//如果新節點不在開啟列表,將其加入開啟列表

        }

      }

    }

 

二、演算法實現

首先,在路徑網格中每一個網格都有一個對應的坐標,因此我創建了一個Point2類用來表示網格坐標

 1 //A class used to store the position information
 2 public class Point2
 3 {
 4     public Point2(int x, int y)
 5     {
 6         this.x = x;
 7         this.y = y;
 8     }
 9 
10     public int x { get; set; }
11 
12     public int y { get; set; }
13 
14     public override bool Equals(object obj)
15     {
16         return this.x == (obj as Point2).x && this.y == (obj as Point2).y;
17     }
18 
19     public override int GetHashCode()
20     {
21         return x ^ (y * 256);
22     }
23 
24     public override string ToString()
25     {
26         return x + "," + y;
27     }
28 
29     public static bool operator ==(Point2 a, Point2 b)
30     {
31         return a.Equals(b);
32     }
33 
34     public static bool operator !=(Point2 a, Point2 b)
35     {
36         return !a.Equals(b);
37     }
38 }
View Code

 

  接下來,我創建了一個PathNode類用來記錄單個節點的信息

 

 1 public class PathNode
 2 {
 3     public PathNode(bool isWall, Point2 position)
 4     {
 5         this.isWall = isWall;
 6         this.position = position;
 7     }
 8 
 9     public readonly Point2 position;
10 
11     public bool isWall { get; set; }
12 
13     public PathNode parent { get; set; }
14 
15     public int gCost { get; set; }
16 
17     public int hCost { get; set; }
18 
19     public int fCost {
20         get {
21             return gCost + hCost;
22         }
23     }
24 
25     public override bool Equals(object obj)
26     {
27         PathNode node = obj as PathNode;
28         return node.isWall == this.isWall && node.gCost == this.gCost && node.hCost == this.hCost && node.parent == this.parent && this.position == node.position;
29     }
30 
31     public override int GetHashCode()
32     {
33         return gCost ^ (hCost * 128) + position.GetHashCode();
34     }
35 
36     public static bool operator ==(PathNode a, PathNode b)
37     {
38         return a.Equals(b);
39     }
40 
41     public static bool operator !=(PathNode a, PathNode b)
42     {
43         return !a.Equals(b);
44     }
45 }
View Code

 

  最後是我們的PathGrid類,通過創建該類的實例來創建一個網格信息,其中包含了網格大小以及所有障礙物信息。使用中輸入起點信息和終點信息可以返迴路徑信息。關於代碼部分,由於Astar演算法中開銷最大的部分是開啟列表和關閉列表的維護以及在開啟列表中尋找f值最低的部分。因此我用C#的SortedDictionary額外創建了一個開啟列表用於查詢f值最低的節點。當然演算法還存在很大的優化空間,不過像32*32這樣的小的網格中已經夠用了。

  1 using System.Collections.Generic;
  2 using System.Linq;
  3 using System;
  4 
  5 public class PathGrid
  6 {
  7     private SortedDictionary<int, List<Point2>> openTree = new SortedDictionary<int, List<Point2>>();
  8 
  9     private HashSet<Point2> openSet = new HashSet<Point2>();
 10     private HashSet<Point2> closeSet = new HashSet<Point2>();
 11     private Dictionary<Point2, PathNode> allNodes = new Dictionary<Point2, PathNode>();
 12 
 13     private Point2 endPos;
 14     private Point2 gridSize;
 15 
 16     private List<Point2> currentPath;
 17 
 18     //這一部分在實際尋路中並不需要,只是為了方便外部程式實現尋路可視化
 19     public HashSet<Point2> GetCloseList()
 20     {
 21         return closeSet;
 22     }
 23 
 24     //這一部分在實際尋路中並不需要,只是為了方便外部程式實現尋路可視化
 25     public HashSet<Point2> GetOpenList()
 26     {
 27         return openSet;
 28     }
 29 
 30     //這一部分在實際尋路中並不需要,只是為了方便外部程式實現尋路可視化
 31     public List<Point2> GetCurrentPath()
 32     {
 33         return currentPath;
 34     }
 35 
 36     //新建一個PathGrid,包含了網格大小和障礙物信息
 37     public PathGrid(int x, int y, List<Point2> walls)
 38     {
 39         gridSize = new Point2(x, y);
 40         for (int i = 0; i < x; i++) {
 41             for (int j = 0; j < y; j++) {
 42                 Point2 newPos = new Point2(i, j);
 43                 allNodes.Add(newPos, new PathNode(walls.Contains(newPos), newPos));
 44             }
 45         }
 46     }
 47 
 48     //尋路主要邏輯,通過調用該方法來獲取路徑信息,由一串Point2代表
 49     public List<Point2> FindPath(Point2 beginPos, Point2 endPos)
 50     {
 51         List<Point2> result = new List<Point2>();
 52 
 53         this.endPos = endPos;
 54         Point2 currentPos = beginPos;
 55         openSet.Add(currentPos);
 56 
 57         while (!currentPos.Equals(this.endPos)) {
 58             UpdatePath(currentPos);
 59             if (openSet.Count == 0) return null;
 60 
 61             currentPos = openTree.First().Value.First();
 62         }
 63 
 64         Point2 path = currentPos;
 65 
 66         while (!path.Equals(beginPos)) {
 67             result.Add(path);
 68             path = allNodes[path].parent.position;
 69             currentPath = result;
 70         }
 71 
 72         result.Add(beginPos);
 73         return result;
 74     }
 75 
 76     //尋路
 77     private void UpdatePath(Point2 currentPos)
 78     {
 79         closeSet.Add(currentPos);
 80         RemoveOpen(currentPos, allNodes[currentPos]);
 81         List<Point2> neighborNodes = FindNeighbor(currentPos);
 82         foreach (Point2 nodePos in neighborNodes) {
 83 
 84             PathNode newNode = new PathNode(false, nodePos);
 85             newNode.parent = allNodes[currentPos];
 86 
 87             int g;
 88             int h;
 89 
 90             g = currentPos.x == nodePos.x || currentPos.y == nodePos.y ? 10 : 14;
 91 
 92             int xMoves = Math.Abs(nodePos.x - endPos.x);
 93             int yMoves = Math.Abs(nodePos.y - endPos.y);
 94 
 95             int min = Math.Min(xMoves, yMoves);
 96             int max = Math.Max(xMoves, yMoves);
 97             h = min * 14 + (max - min) * 10;
 98 
 99 
100             newNode.gCost = g + newNode.parent.gCost;
101             newNode.hCost = h;
102 
103             PathNode originNode = allNodes[nodePos];
104 
105             if (openSet.Contains(nodePos)) {
106                 if (newNode.fCost < originNode.fCost) {
107                     UpdateNode(newNode, originNode);
108                 }
109             } else {
110                 allNodes[nodePos] = newNode;
111                 AddOpen(nodePos, newNode);
112             }
113         }
114     }
115 
116     //將舊節點更新為新節點
117     private void UpdateNode(PathNode newNode, PathNode oldNode)
118     {
119         Point2 nodePos = newNode.position;
120         int oldCost = oldNode.fCost;
121         allNodes[nodePos] = newNode;
122         List<Point2> sameCost;
123 
124         if (openTree.TryGetValue(oldCost, out sameCost)) {
125             sameCost.Remove(nodePos);
126             if (sameCost.Count == 0) openTree.Remove(oldCost);
127         }
128 
129         if (openTree.TryGetValue(newNode.fCost, out sameCost)) {
130             sameCost.Add(nodePos);
131         } else {
132             sameCost = new List<Point2> { nodePos };
133             openTree.Add(newNode.fCost, sameCost);
134         }
135     }
136 
137     //將目標節點移出開啟列表
138     private void RemoveOpen(Point2 pos, PathNode node)
139     {
140         openSet.Remove(pos);
141         List<Point2> sameCost;
142         if (openTree.TryGetValue(node.fCost, out sameCost)) {
143             sameCost.Remove(pos);
144             if (sameCost.Count == 0) openTree.Remove(node.fCost);
145         }
146     }
147 
148     //將目標節點加入開啟列表
149     private void AddOpen(Point2 pos, PathNode node)
150     {
151         openSet.Add(pos);
152         List<Point2> sameCost;
153         if (openTree.TryGetValue(node.fCost, out sameCost)) {
154             sameCost.Add(pos);
155         } else {
156             sameCost = new List<Point2> { pos };
157             openTree.Add(node.fCost, sameCost);
158         }
159     }
160 
161     //找到某節點的所有相鄰節點
162     private List<Point2> FindNeighbor(Point2 nodePos)
163     {
164         List<Point2> result = new List<Point2>();
165 
166         for (int x = -1; x < 2; x++) {
167             for (int y = -1; y < 2; y++) {
168                 if (x == 0 && y == 0) continue;
169 
170                 Point2 currentPos = new Point2(nodePos.x + x, nodePos.y + y);
171 
172                 if (currentPos.x >= gridSize.x || currentPos.y >= gridSize.y || currentPos.x < 0 || currentPos.y < 0) continue; //out of bondary
173                 if (closeSet.Contains(currentPos)) continue; // already in the close list
174                 if (allNodes[currentPos].isWall) continue;  // the node is a wall
175 
176                 result.Add(currentPos);
177             }
178         }
179 
180         return result;
181     }
182 }
View Code

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 前面redis弄了那麼多, 就是為了在項目中使用. 那這裡, 就分別來看一下, 單機版和集群版在springboot中的使用吧. 在裡面, 我會同時貼出Jedis版, 作為比較. 單機版 1. pom.xml 2. application.yml 這裡為redis設置了一個密碼, 可以在 re ...
  • 一、模塊初識 便捷目錄: sys.path 獲取指定模塊搜索路徑的字元串集合(當前是sys) sys.argv 從外部程式向內部程式傳遞參數 sys.getdefaultencoding() 獲取當前系統編碼 sys.getfilesystemencoding()獲取文件系統使用編碼方式,Windo ...
  • ajax 快速入門 ajax作用:ajax 是在不重新載入整個頁面的情況下與伺服器交換數據並更新部分網頁的技術.(實現瀏覽器與伺服器之間的數據交互,實現頁面的無刷新請求伺服器,提高用戶體驗) 基本使用: 1.創建ajax對象: new XMLHttpRequest() //普通瀏覽器使用,ie瀏覽器 ...
  • Resource is out of sync with the file system: 該錯誤為替換了image中的圖片而沒有進行更新,造成找不到該資源,進而保存,解決只要eclipse刷新一下F5就可以了 ...
  • CAS 單點登錄設計與實現22 ...
  • springboot+mybatis+通用mapper+多數據源 ...
  • 註:和上一篇有關聯 (一) finally 和 輸出異常信息 (二) 使用 with (1) 上面的代碼如果文件不存在,就不會創建the_man對象,那麼執行the_man.close()就會出現NameError錯誤,所以得先判斷是否存在文件 test.txt是否存在 (2) 用(1)中的比較麻煩 ...
  • 文檔:https://docs.microsoft.com/en-us/windows/uwp/design/style/acrylic Acrylic 能帶來類似 win7 的毛玻璃效果 要使用 Acrylic ,需要 win10 的版本最低為 1709 ,在模擬器中是 16299 Acrylic ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...