路徑查詢演算法應用之A*演算法

騰鯤發表於2022-03-16

環境:Visual Studio 2017 + .Net Framework 4.5

應用場景:在畫板上查詢起始點和目標點之間的最短最直路徑,最後畫出連線兩個點之間的折線。

演算法簡介:A*演算法是一種效能較高的最優路徑搜尋演算法,由Stanford Research Institute(now SRI International)的Peter Hart,Nils Nilsson和Bertram Raphael於1968年發表。A*演算法可看做是對Dijkstra演算法的擴充套件和優化,其效能一般情況下比Dijkstra演算法快得多。在本文的應用場景中,(根據測試)通常比Dijkstra演算法快三倍以上,甚至可能比Dijkstra演算法快十幾倍甚至幾十倍。

A*演算法的應用範圍也比較廣泛,如機器人行走路徑規劃,遊戲中的NPC移動計算等。

更詳細的演算法說明請參考維基百科A* search algorithm

 

實現思想:

1,通過Locator把起始點座標和目標點座標對齊到步長(step,預設為20,)的整數倍。這樣,起始點和目標點就成了原來的起始點目標點的近似點。

2,把包含起始點和目標點的障礙物(如圖中所示,為矩形框)排除掉,不然折線遇到障礙物無法通過。

下圖中的矩形框的虛邊為避障區域,為了防止折線和障礙物碰撞。

3,把起始點新增到待遍歷點的集合中(本文中為SortedList<Vertex>)。

4,從待遍歷點的集合中取出第一個點(當前的最優點),遍歷其東、南、西、北四個方向的相鄰節點。東西兩個方向和當前點的Y座標相同,南北兩個方向和當前點的X座標相同。

相鄰點距當前點的距離為step引數設定的步長。step越大,搜尋速度越快,然而,可能導致折線無法通過間距較小的障礙物。

如果某個方向的相鄰點不存在,則建立新的相鄰點(如果相鄰點不在障礙物內部的話);同時,設定新建立點的四個相鄰點(也許新建立點的相鄰點已經被建立了)。

把新建立的相鄰點的Visited屬性設定為false(當前實現中,Visited屬性預設為false),然後對新建立點的所有相鄰點排序,取最優點,設定為新建立點的前一個點(呼叫SetPrev方法)。

再把新建立的點新增到待遍歷點的集合中(本文中為SortedList<Vertex>)。

當遍歷完當前點的四個方向後,把當前點的Visited屬性設定為true,並從帶遍歷點的集合中移除。

說明:當前演算法的實現中僅考慮總距離(從起始點到當前點的距離加上猜測距離,起始點距離為0)、猜測距離(從當前點到目標點的距離,為從當前點到目標點的折線長度)和拐點個數(X或Y座標變化時拐點個數加1)。

5,遞迴第4步。要麼找到和目標點座標相同的點(即,找到了目標點),要麼待遍歷點的集合為空(即,無法找到通往目標點的路徑)。

6,當找到通往目標點的路徑之後,通過Straightener(調直器)調直路徑,減少拐點。

7,處理起始點和目標點。用原來的起始點和目標點替換座標對齊到step整數倍的起始點和目標點,並調直其相鄰拐點的X座標或Y座標。

8,返回最終路徑。

 

如下兩個圖所示

第一張圖為A*演算法查詢出來的最優路徑(不一定是最短路徑,依賴於演算法的實現)

第二張圖為調直後的最直路徑(拐點最少)

 

 

程式碼

由於工程太大,僅上傳必要的程式碼檔案。

路徑查詢演算法應用之A*演算法
  1 using System;
  2 using System.Collections.Generic;
  3 using System.Drawing;
  4 
  5 namespace Pathfinding
  6 {
  7     /// <summary>
  8     /// A*演算法
  9     /// </summary>
 10     public class AStarAlgorithm
 11     {
 12         private Vertex m_goal;
 13         private Locator m_locator;
 14         private SortedList<Vertex> m_openSet;
 15         private Orientation m_orientation;
 16         private Vertex m_start;
 17         /// <summary>
 18         /// 查詢最優路徑
 19         /// </summary>
 20         /// <param name="canvas">畫布</param>
 21         /// <param name="obstacles">障礙物</param>
 22         /// <param name="step">步長</param>
 23         /// <param name="voDistance">避障距離</param>
 24         /// <param name="initOrient">第一層查詢的方向</param>
 25         /// <param name="start">起始點</param>
 26         /// <param name="goal">目標點</param>
 27         /// <returns></returns>
 28         public Point[] Find(Rectangle canvas, List<Rectangle> obstacles, int step, int voDistance, Orientation initOrient, Point start, Point goal)
 29         {
 30             if (start == goal)
 31                 return null;
 32 
 33             if (start.GetDistanceTo(goal) < step)
 34                 return this.ProcessShortPath(start, goal);
 35 
 36             this.Init(canvas, obstacles, step, voDistance, initOrient, start, goal);
 37             this.AddIntoOpenSet(this.m_start);
 38 
 39             Vertex optimal = null;
 40             while (this.m_openSet.Count > 0)
 41             {
 42                 optimal = this.GetOptimalVertex();
 43 
 44                 if (this.IsGoal(optimal))
 45                 {
 46                     this.WalkTarget();
 47                     var path = Straightener.Straighten(this.m_locator, this.m_goal.Lines);
 48                     this.ProcessEndpoint(start, 0, path);
 49                     this.ProcessEndpoint(goal, path.Length - 1, path);
 50 
 51                     return path;
 52                 }
 53 
 54                 this.Walk(optimal);
 55             }
 56 
 57             return null;
 58         }
 59 
 60         /// <summary>
 61         /// 新增待遍歷的點
 62         /// </summary>
 63         /// <param name="vertex"></param>
 64         private void AddIntoOpenSet(Vertex vertex)
 65         {
 66             if (!vertex.IsVisited)
 67                 this.m_openSet.Add(vertex);
 68         }
 69 
 70         /// <summary>
 71         /// 獲取最優點
 72         /// </summary>
 73         /// <returns></returns>
 74         private Vertex GetOptimalVertex()
 75         {
 76             var cheapest = this.m_openSet.TakeFirst();
 77             cheapest.IsCurrent = true;
 78 
 79             return cheapest;
 80         }
 81 
 82         /// <summary>
 83         /// 估算到目標點的距離
 84         /// </summary>
 85         /// <param name="vertex"></param>
 86         /// <returns></returns>
 87         private int GuessDistanceToGoal(Vertex vertex)
 88         {
 89             return Math.Abs(vertex.X - this.m_goal.X) + Math.Abs(vertex.Y - this.m_goal.Y);
 90         }
 91 
 92         /// <summary>
 93         /// 初始化資料
 94         /// </summary>
 95         /// <param name="canvas"></param>
 96         /// <param name="obstacles"></param>
 97         /// <param name="step"></param>
 98         /// <param name="voDistance"></param>
 99         /// <param name="initOrient"></param>
100         /// <param name="start"></param>
101         /// <param name="goal"></param>
102         private void Init(Rectangle canvas, List<Rectangle> obstacles, int step, int voDistance, Orientation initOrient, Point start, Point goal)
103         {
104             this.m_locator = new Locator(canvas, obstacles, step, voDistance);
105 
106             this.m_locator.AlignPoint(ref start);
107             this.m_locator.AlignPoint(ref goal);
108             this.m_locator.ExcludeObstacles(start);
109             this.m_locator.ExcludeObstacles(goal);
110 
111             this.m_start = new Vertex()
112             {
113                 Location = start
114             };
115             this.m_goal = new Vertex()
116             {
117                 Location = goal
118             };
119             this.m_openSet = new SortedList<Vertex>();
120             this.m_start.GuessDistance = this.GuessDistanceToGoal(this.m_start);
121             this.m_orientation = initOrient;
122         }
123 
124         /// <summary>
125         /// 是否是目標點
126         /// </summary>
127         /// <param name="vertex"></param>
128         /// <returns></returns>
129         private bool IsGoal(Vertex vertex)
130         {
131             if (vertex.Location == this.m_goal.Location)
132             {
133                 this.m_goal = vertex;
134                 return true;
135             }
136 
137             return false;
138         }
139 
140         /// <summary>
141         /// 處理端點(起始點或目標點)
142         /// </summary>
143         /// <param name="endpoint"></param>
144         /// <param name="idx"></param>
145         /// <param name="path"></param>
146         private void ProcessEndpoint(Point endpoint, int idx, Point[] path)
147         {
148             Point approximatePoint = path[idx];
149             if (0 == idx)
150             {
151                 path[0] = endpoint;
152                 idx += 1;
153             }
154             else
155             {
156                 path[idx] = endpoint;
157                 idx -= 1;
158             }
159 
160             if (approximatePoint.X == path[idx].X)
161                 path[idx].X = endpoint.X;
162             else
163                 path[idx].Y = endpoint.Y;
164         }
165 
166         /// <summary>
167         /// 處理短路徑
168         /// </summary>
169         /// <param name="start"></param>
170         /// <param name="goal"></param>
171         /// <returns></returns>
172         private Point[] ProcessShortPath(Point start, Point goal)
173         {
174             var dx = Math.Abs(goal.X - start.X);
175             var dy = Math.Abs(goal.Y - start.Y);
176             if (dx >= dy)
177                 return new Point[] { start, new Point(goal.X, start.Y), goal };
178             else
179                 return new Point[] { start, new Point(start.X, goal.Y), goal };
180         }
181 
182         /// <summary>
183         /// 設定前一個點
184         /// </summary>
185         /// <param name="vertex"></param>
186         private void SetPrev(Vertex vertex)
187         {
188             var neighbors = vertex.Neighbors;
189             neighbors.Sort();
190             vertex.SetPrev(neighbors[0]);
191             vertex.GuessDistance = this.GuessDistanceToGoal(vertex);
192         }
193 
194 
195         #region Traverse Neighbors
196 
197         /// <summary>
198         /// 建立東邊的相鄰點
199         /// </summary>
200         /// <param name="vertex"></param>
201         private void CreateEastNeighbor(Vertex vertex)
202         {
203             var location = new Point(vertex.X + this.m_locator.Step, vertex.Y);
204             if (this.m_locator.AlignPoint(ref location)
205                 && Orientation.East == vertex.Location.GetOrientation(location))
206             {
207                 var neighbor = new Vertex()
208                 {
209                     Location = location
210                 };
211 
212                 //213                 //     |
214                 // ◐---●---○
215                 //     |
216                 //
217                 vertex.EastNeighbor = neighbor;
218                 //     ◐---◐
219                 //     |   |
220                 // ◐---●---○
221                 //     |
222                 //
223                 neighbor.NorthNeighbor = vertex.NorthNeighbor?.EastNeighbor;
224                 //225                 //     |
226                 // ◐---●---○
227                 //     |   |
228                 //     ◐---◐
229                 neighbor.SouthNeighbor = vertex.SouthNeighbor?.EastNeighbor;
230 
231                 this.SetPrev(neighbor);
232                 this.AddIntoOpenSet(neighbor);
233             }
234             else
235                 vertex.CouldWalkEast = false;
236         }
237 
238         /// <summary>
239         /// 建立北邊的相鄰點
240         /// </summary>
241         /// <param name="vertex"></param>
242         private void CreateNorthNeighbor(Vertex vertex)
243         {
244             var location = new Point(vertex.X, vertex.Y - this.m_locator.Step);
245             if (this.m_locator.AlignPoint(ref location)
246                 && Orientation.North == vertex.Location.GetOrientation(location))
247             {
248                 var neighbor = new Vertex()
249                 {
250                     Location = location
251                 };
252 
253                 //254                 //     |
255                 // ◐---●---◐
256                 //     |
257                 //
258                 vertex.NorthNeighbor = neighbor;
259                 //     ○---◐
260                 //     |   |
261                 // ◐---●---◐
262                 //     |
263                 //
264                 neighbor.EastNeighbor = vertex.EastNeighbor?.NorthNeighbor;
265                 // ◐---○
266                 // |   |
267                 // ◐---●---◐
268                 //     |
269                 //
270                 neighbor.WestNeighbor = vertex.WestNeighbor?.NorthNeighbor;
271 
272                 this.SetPrev(neighbor);
273                 this.AddIntoOpenSet(neighbor);
274             }
275             else
276                 vertex.CouldWalkNorth = false;
277         }
278 
279         /// <summary>
280         /// 建立南邊的相鄰點
281         /// </summary>
282         /// <param name="vertex"></param>
283         private void CreateSouthNeighbor(Vertex vertex)
284         {
285             var location = new Point(vertex.X, vertex.Y + this.m_locator.Step);
286             if (this.m_locator.AlignPoint(ref location)
287                 && Orientation.South == vertex.Location.GetOrientation(location))
288             {
289                 var neighbor = new Vertex()
290                 {
291                     Location = location
292                 };
293 
294                 //295                 //     |
296                 // ◐---●---◐
297                 //     |
298                 //
299                 vertex.SouthNeighbor = neighbor;
300                 //301                 //     |
302                 // ◐---●---◐
303                 //     |   |
304                 //     ○---◐
305                 neighbor.EastNeighbor = vertex.EastNeighbor?.SouthNeighbor;
306                 //307                 //     |
308                 // ◐---●---◐
309                 // |   |
310                 // ◐---○
311                 neighbor.WestNeighbor = vertex.WestNeighbor?.SouthNeighbor;
312 
313                 this.SetPrev(neighbor);
314                 this.AddIntoOpenSet(neighbor);
315             }
316             else
317                 vertex.CouldWalkSouth = false;
318         }
319 
320         /// <summary>
321         /// 建立西邊的相鄰點
322         /// </summary>
323         /// <param name="vertex"></param>
324         private void CreateWestNeighbor(Vertex vertex)
325         {
326             var location = new Point(vertex.X - this.m_locator.Step, vertex.Y);
327             if (this.m_locator.AlignPoint(ref location)
328                 && Orientation.West == vertex.Location.GetOrientation(location))
329             {
330                 var neighbor = new Vertex()
331                 {
332                     Location = location
333                 };
334 
335                 //336                 //     |
337                 // ○---●---◐
338                 //     |
339                 //
340                 vertex.WestNeighbor = neighbor;
341                 //342                 //     |
343                 // ○---●---◐
344                 // |   |
345                 // ◐---◐
346                 neighbor.SouthNeighbor = vertex.SouthNeighbor?.WestNeighbor;
347                 // ◐---◐
348                 // |   |
349                 // ○---●---◐
350                 //     |
351                 //
352                 neighbor.NorthNeighbor = vertex.NorthNeighbor?.WestNeighbor;
353 
354                 this.SetPrev(neighbor);
355                 this.AddIntoOpenSet(neighbor);
356             }
357             else
358                 vertex.CouldWalkWest = false;
359         }
360 
361         /// <summary>
362         /// <para>遍歷四個方位的相鄰點:東、南、西、北</para>
363         /// <para>●(實心圓)表示訪問過的點</para>
364         /// <para>◐(半實心圓)表示可能訪問過的點</para>
365         /// <para>○(空心圓)表示未訪問過的點</para>
366         /// </summary>
367         /// <param name="vertex"></param>
368         private void Walk(Vertex vertex)
369         {
370             //371             //     |
372             // ◐---●---◐
373             //     |
374             //
375 
376             var count = 0;
377             while (count++ < 4)
378             {
379                 switch (this.m_orientation)
380                 {
381                     case Orientation.East:
382                         this.WalkEast(vertex);
383                         this.m_orientation = Orientation.South;
384                         break;
385                     case Orientation.South:
386                         this.WalkSouth(vertex);
387                         this.m_orientation = Orientation.West;
388                         break;
389                     case Orientation.West:
390                         this.WalkWest(vertex);
391                         this.m_orientation = Orientation.North;
392                         break;
393                     case Orientation.North:
394                         this.WalkNorth(vertex);
395                         this.m_orientation = Orientation.East;
396                         break;
397                     default:
398                         this.m_orientation = Orientation.East;
399                         break;
400                 }
401             }
402 
403             vertex.IsVisited = true;
404             vertex.IsCurrent = false;
405         }
406 
407         /// <summary>
408         /// 遍歷東邊的相鄰點
409         /// </summary>
410         /// <param name="vertex"></param>
411         private void WalkEast(Vertex vertex)
412         {
413             if (vertex.CouldWalkEast && vertex.EastNeighbor is null)
414                 this.CreateEastNeighbor(vertex);
415         }
416 
417         /// <summary>
418         /// 遍歷北邊的相鄰點
419         /// </summary>
420         /// <param name="vertex"></param>
421         private void WalkNorth(Vertex vertex)
422         {
423             if (vertex.CouldWalkNorth && vertex.NorthNeighbor is null)
424                 this.CreateNorthNeighbor(vertex);
425         }
426 
427         /// <summary>
428         /// 遍歷南邊的相鄰點
429         /// </summary>
430         /// <param name="vertex"></param>
431         private void WalkSouth(Vertex vertex)
432         {
433             if (vertex.CouldWalkSouth && vertex.SouthNeighbor is null)
434                 this.CreateSouthNeighbor(vertex);
435         }
436 
437         /// <summary>
438         /// 遍歷目標點
439         /// </summary>
440         private void WalkTarget()
441         {
442             // 遍歷目標點及其相鄰點
443             this.Walk(this.m_goal);
444 
445             if (null != this.m_goal.EastNeighbor)
446                 this.Walk(this.m_goal.EastNeighbor);
447             if (null != this.m_goal.SouthNeighbor)
448                 this.Walk(this.m_goal.SouthNeighbor);
449             if (null != this.m_goal.WestNeighbor)
450                 this.Walk(this.m_goal.WestNeighbor);
451             if (null != this.m_goal.NorthNeighbor)
452                 this.Walk(this.m_goal.NorthNeighbor);
453 
454             this.SetPrev(this.m_goal);
455         }
456 
457         /// <summary>
458         /// 遍歷西邊的相鄰點
459         /// </summary>
460         /// <param name="vertex"></param>
461         private void WalkWest(Vertex vertex)
462         {
463             if (vertex.CouldWalkWest && vertex.WestNeighbor is null)
464                 this.CreateWestNeighbor(vertex);
465         }
466 
467         #endregion Traverse Neighbors
468     }
469 }
AStarAlgorithm
路徑查詢演算法應用之A*演算法
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

namespace Pathfinding
{
    /// <summary>
    /// 定位器(用於避障,查詢相鄰點或者對齊座標等)
    /// </summary>
    public class Locator
    {
        /// <summary>
        /// 畫布
        /// </summary>
        private readonly Rectangle m_canvas;

        /// <summary>
        /// 障礙物
        /// </summary>
        private readonly List<Rectangle> m_obstacles;

        /// <summary>
        /// 步長
        /// </summary>
        private readonly int m_step;

        /// <summary>
        /// 避障距離
        /// </summary>
        private readonly int m_voDistance;

        public Locator(Rectangle canvas, List<Rectangle> obstacles, int step = 20, int voDistance = 10)
        {
            this.m_canvas = canvas;
            if (step < 20)
                step = 20;
            this.m_step = (step >= 20) ? step : 20;
            this.m_voDistance = (voDistance >= 10) ? voDistance : 10;
            this.m_obstacles = this.BuildObstacles(obstacles);
        }

        /// <summary>
        /// 畫板
        /// </summary>
        public Rectangle Canvas => this.m_canvas;

        /// <summary>
        /// 步長
        /// </summary>
        public int Step => this.m_step;

        /// <summary>
        /// 避障距離
        /// </summary>
        public int VODistance => this.m_voDistance;

        /// <summary>
        /// 對齊座標(把“點”的座標值對齊到Step的整數倍)
        /// </summary>
        /// <param name="point"></param>
        public Point AlignPoint(Point point)
        {
            return new Point(this.AlignCoord(point.X, 1), this.AlignCoord(point.Y, 1));
        }

        /// <summary>
        /// <para>對齊座標(把“點”的座標值對齊到Step的整數倍,同時校驗“點”是否在畫布內或是否和障礙物衝突)</para>
        /// <para>如果對齊前或對齊後的“點”座標不在畫布內或者和障礙物衝突,則返回false;否則,返回true。</para>
        /// </summary>
        /// <param name="point"></param>
        /// <returns></returns>
        public bool AlignPoint(ref Point point)
        {
            if (!this.m_canvas.Contains(point))
                return false;

            point = this.AlignPoint(point);

            if (!this.m_canvas.Contains(point))
                return false;

            return !this.InObstacle(point);
        }

        /// <summary>
        /// 排除包含“點”的障礙物
        /// </summary>
        /// <param name="point"></param>
        public void ExcludeObstacles(Point point)
        {
            this.m_obstacles.RemoveAll(o => o.Contains(point));
        }

        /// <summary>
        /// 判斷點是否在障礙物內
        /// </summary>
        /// <param name="point"></param>
        /// <returns></returns>
        public bool InObstacle(Point point)
        {
            return this.m_obstacles.Exists(obst => obst.Contains(point));
        }

        /// <summary>
        /// <para>判斷水平線段或垂直線段(ab)是否和障礙物相交</para>
        /// <para>a、b的順序和結果無關</para>
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public bool IntersectWithObstacle(Point a, Point b)
        {
            if (a.X == b.X)
                return this.IntersectVerticallyWithObstacle(a, b);
            else // a.Y == b.Y
                return this.IntersectHorizontallyWithObstacle(a, b);
        }

        /// <summary>
        /// 判斷水平線段(ab,其中a.X &#8804; b.X)是否和障礙物相交
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public bool IntersectHorizontallyWithObstacle(Point a, Point b)
        {
            if (a.X > b.X)
            {
                var t = a;
                a = b;
                b = t;
            }

            return this.m_obstacles.Exists(obst =>
                (obst.Top <= a.Y && a.Y <= obst.Bottom && ((a.X <= obst.Left && obst.Left <= b.X) || (a.X <= obst.Right && obst.Right <= b.X)))
                || obst.Contains(a)
                || obst.Contains(b));
        }

        /// <summary>
        /// 判斷垂直線段(ab,其中a.Y &#8804; b.Y)是否和障礙物相交
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public bool IntersectVerticallyWithObstacle(Point a, Point b)
        {
            if (a.Y > b.Y)
            {
                var t = a;
                a = b;
                b = t;
            }

            return this.m_obstacles.Exists(obst =>
                (obst.Left <= a.X && a.X <= obst.Right && ((a.Y <= obst.Top && obst.Top <= b.Y) || (a.Y <= obst.Bottom && obst.Bottom <= b.Y)))
                || obst.Contains(a)
                || obst.Contains(b));
        }

        /// <summary>
        /// 對齊座標的值
        /// </summary>
        /// <param name="val"></param>
        /// <param name="direction"></param>
        /// <returns></returns>
        private int AlignCoord(int val, int direction)
        {
            int md = val % this.m_step;
            if (0 == md)
                return val;
            else if (md <= this.m_step / 2)
                return val - md;
            else
                return val - md + (direction * this.m_step);
        }

        /// <summary>
        /// 構造障礙物(用於除錯)
        /// </summary>
        /// <param name="obstacles"></param>
        /// <returns></returns>
        private List<Rectangle> BuildDebugObstacles(List<Rectangle> obstacles)
        {
            if (obstacles is null || obstacles.Count <= 0)
                return new List<Rectangle>();
            else
                return obstacles.Select(o => new Rectangle(o.X - this.m_voDistance,
                                                           o.Y - this.m_voDistance,
                                                           o.Width + this.m_voDistance * 2,
                                                           o.Height + m_voDistance * 2)).ToList();
        }

        /// <summary>
        /// 構造障礙物
        /// </summary>
        /// <param name="obstacles"></param>
        /// <returns></returns>
        private List<Rectangle> BuildObstacles(List<Rectangle> obstacles)
        {
            if (obstacles is null || obstacles.Count <= 0)
                return new List<Rectangle>();
            else
                return obstacles.Select(o => new Rectangle(o.X - this.m_voDistance + 1,
                                                           o.Y - this.m_voDistance + 1,
                                                           o.Width + this.m_voDistance * 2 - 2,
                                                           o.Height + m_voDistance * 2 - 2)).ToList();
        }
    }
}
Locator
路徑查詢演算法應用之A*演算法
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

namespace Pathfinding
{
    /// <summary>
    /// 路徑調製器
    /// </summary>
    public class Straightener
    {
        /// <summary>
        /// 滿足調直的前提條件(路徑最少包含4個點)
        /// </summary>
        private const int MIN_COUNT_POINTS = 4;
        /// <summary>
        /// 定位器
        /// </summary>
        private Locator m_locator;
        /// <summary>
        /// 原始路徑
        /// </summary>
        private LinkedList<Point> m_originalPath;
        /// <summary>
        /// 調直後的路徑
        /// </summary>
        private LinkedList<Point> m_straightenedPath;

        private Straightener()
        {
            this.Reset();
        }

        /// <summary>
        /// 調直路徑,減少拐點(引數為空或小於4個點不做任何處理)
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public static Point[] Straighten(Locator locator, Point[] path)
        {
            if (locator is null || path is null || path.Length < MIN_COUNT_POINTS)
                return path;

            var worker = new Straightener();
            worker.m_locator = locator;
            worker.m_originalPath = new LinkedList<Point>(path);
            worker.Straighten();

            return worker.m_straightenedPath.ToArray();
        }

        /// <summary>
        /// 建立假設的拐點
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private static Point CreateHypotheticalInflection(LinkedListNode<Point> node)
        {
            if (node.Value.X == node.Next.Value.X)
                return new Point(node.Next.Next.Value.X, node.Value.Y);
            else
                return new Point(node.Value.X, node.Next.Next.Value.Y);
        }

        /// <summary>
        /// 計算線段(abcd)上拐點的個數
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="c"></param>
        /// <param name="d"></param>
        /// <returns></returns>
        private static int GetCountInflections(Point a, Point b, Point c, Point d)
        {
            var inflections = 0;
            if (c.X != a.X && c.Y != a.Y)
                inflections++;
            if (d.X != b.X && d.Y != b.Y)
                inflections++;

            return inflections;
        }

        private static int GetDistance(Point a, Point b)
        {
            if (a.X == b.X)
                return Math.Abs(a.Y - b.Y);
            else
                return Math.Abs(a.X - b.X);
        }
        /// <summary>
        /// 新增拐點
        /// </summary>
        /// <param name="inflection"></param>
        private void AddInflection(Point inflection)
        {
            if (null != this.m_straightenedPath.Last
                && this.m_straightenedPath.Last.Value == inflection)
                return;

            var last = this.m_straightenedPath.AddLast(inflection);
            if (null != last.Previous?.Previous
                && (last.Value.X == last.Previous.Previous.Value.X
                    || last.Value.Y == last.Previous.Previous.Value.Y))
                this.m_straightenedPath.Remove(last.Previous);
        }

        private void DoStraighten()
        {
            this.Reset();

            var current = this.m_originalPath.First;
            while (null != current.Next?.Next)
            {
                this.AddInflection(current.Value);

                this.RemoveRedundantInflections(current);
                if (current.Next?.Next is null)
                    break;

                var inflection = CreateHypotheticalInflection(current);
                if (!this.IntersectWithObstacle(current.Value, inflection, current.Next.Next.Value))
                {
                    var success = false;

                    if (null != current.Previous)
                    {
                        var i1 = GetCountInflections(current.Previous.Value, current.Value, current.Next.Value, current.Next.Next.Value);
                        var i2 = GetCountInflections(current.Previous.Value, current.Value, inflection, current.Next.Next.Value);
                        if (i2 < i1)
                        {
                            current.Next.Value = inflection;
                            success = true;
                        }
                    }
                    else if (null != current.Next?.Next?.Next)
                    {
                        var i3 = GetCountInflections(current.Value, current.Next.Value, current.Next.Next.Value, current.Next.Next.Next.Value);
                        var i4 = GetCountInflections(current.Value, inflection, current.Next.Next.Value, current.Next.Next.Next.Value);
                        if (i4 < i3)
                        {
                            current.Next.Value = inflection;
                            success = true;
                        }
                    }

                    if (success)
                    {
                        this.RemoveRedundantInflections(current.Next);
                        if (current.Next?.Next is null)
                            break;
                    }
                }

                this.RemoveTurnBackInflections(current);

                current = current.Next;
            }

            this.AddInflection(this.m_originalPath.Last.Previous.Value);
            this.AddInflection(this.m_originalPath.Last.Value);
        }

        private int GetDistance()
        {
            var dist = 0;
            var current = this.m_straightenedPath.First;
            do
            {
                if (null != current.Next)
                {
                    dist += GetDistance(current.Value, current.Next.Value);
                    current = current.Next;
                }
                else
                    break;

            } while (true);

            return dist;
        }

        /// <summary>
        /// 判斷線段(abc)是否和障礙物相交
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="c"></param>
        /// <returns></returns>
        private bool IntersectWithObstacle(Point a, Point b, Point c)
        {
            return this.m_locator.IntersectWithObstacle(a, b)
                || this.m_locator.IntersectWithObstacle(b, c);
        }

        /// <summary>
        /// 刪除冗餘拐點
        /// </summary>
        /// <param name="node"></param>
        private void RemoveRedundantInflections(LinkedListNode<Point> node)
        {
            while (true)
            {
                if (node.Next?.Next is null)
                    break;

                if (node.Value.X == node.Next.Next.Value.X
                    || node.Value.Y == node.Next.Next.Value.Y)
                    this.m_originalPath.Remove(node.Next);
                else
                    break;
            }
        }

        /// <summary>
        /// 刪除迴轉拐點
        /// </summary>
        /// <param name="node"></param>
        private void RemoveTurnBackInflections(LinkedListNode<Point> node)
        {
            if (node.Next?.Next?.Next is null)
                return;

            var point = node.Value;
            var nPoint = node.Next.Value;
            var nnPoint = node.Next.Next.Value;
            var nnnPoint = node.Next.Next.Next.Value;

            // ●為已知拐點;○為假設拐點
            // 消除如下形式的拐點
            //// |
            // ○---●
            // |   |
            // ●---●
            if (point.X == nPoint.X
                && nPoint.Y == nnPoint.Y
                && nnPoint.X == nnnPoint.X)
            {
                var dy1 = point.Y - nPoint.Y;
                var dy2 = nnnPoint.Y - nnPoint.Y;
                var p1 = new Point(nnnPoint.X, point.Y);
                if (Math.Abs(dy2) >= Math.Abs(dy1)
                    && Math.Abs(dy1) / dy1 == Math.Abs(dy2) / dy2
                    && !this.m_locator.IntersectHorizontallyWithObstacle(node.Value, p1))
                {
                    this.m_originalPath.Remove(node.Next);
                    this.m_originalPath.Remove(node.Next);
                    this.m_originalPath.AddAfter(node, p1);
                }
            }
            // ●為已知拐點;○為假設拐點
            // 消除如下形式的拐點
            // ●---○---●
            // |   |
            // ●---●
            else if (point.Y == nPoint.Y
                && nPoint.X == nnPoint.X
                && nnPoint.Y == nnnPoint.Y)
            {
                var dx1 = point.X - nPoint.X;
                var dx2 = nnnPoint.X - nnPoint.X;
                var p2 = new Point(point.X, nnnPoint.Y);
                if (Math.Abs(dx2) >= Math.Abs(dx1)
                    && Math.Abs(dx1) / dx1 == Math.Abs(dx2) / dx2
                    && !this.m_locator.IntersectVerticallyWithObstacle(node.Value, p2))
                {
                    this.m_originalPath.Remove(node.Next);
                    this.m_originalPath.Remove(node.Next);
                    this.m_originalPath.AddAfter(node, p2);
                }
            }
        }

        private void Reset()
        {
            this.m_straightenedPath = new LinkedList<Point>();
        }

        private void Straighten()
        {
            int prevDistance = 0;
            int prevInflections = 0;
            int distance = 0;
            int inflections = 0;

            while (true)
            {
                this.DoStraighten();
                this.m_originalPath = this.m_straightenedPath;

                distance = this.GetDistance();
                inflections = this.m_originalPath.Count;

                if (distance == prevDistance
                    && inflections == prevInflections)
                    break;

                prevDistance = distance;
                prevInflections = inflections;
            }
        }
    }
}
Straightener
路徑查詢演算法應用之A*演算法
using System;
using System.Collections;
using System.Collections.Generic;

namespace Pathfinding
{
    /// <summary>
    /// <para>有序連結串列</para>
    /// <para>此類不是執行緒安全的</para>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class SortedList<T> : IEnumerable<T> where T : IComparable<T>
    {
        private int m_count = 0;
        private Node m_first;
        private Node m_last;

        public SortedList()
        {
            // do nothing
        }

        public SortedList(IEnumerable<T> collection)
        {
            this.AddRange(collection);
        }

        /// <summary>
        /// 連結串列中的元素個數
        /// </summary>
        public int Count => this.m_count;

        /// <summary>
        /// 第一個元素
        /// </summary>
        public T First
        {
            get
            {
                if (null != this.m_first)
                    return this.m_first.Value;
                else
                    return default(T);
            }
        }

        public bool IsEmpty => this.m_count <= 0;

        /// <summary>
        /// 最後一個元素
        /// </summary>
        public T Last
        {
            get
            {
                if (null != this.m_last)
                    return this.m_last.Value;
                else
                    return default(T);
            }
        }

        public void Add(T value)
        {
            var node = new Node(value);
            if (this.IsEmpty)
            {
                this.m_first = node;
                this.m_last = node;
            }
            else if (value.CompareTo(this.m_first.Value) < 0)
            {
                node.Next = this.m_first;
                this.m_first.Prev = node;
                this.m_first = node;
            }
            else if (this.m_last.Value.CompareTo(value) <= 0)
            {
                node.Prev = this.m_last;
                this.m_last.Next = node;
                this.m_last = node;
            }
            else
            {
                Node current = this.m_first;
                do
                {
                    if (value.CompareTo(current.Value) < 0)
                        break;

                    current = current.Next;

                } while (current != null);


                var prev = current.Prev;
                prev.Next = node;
                node.Prev = prev;

                node.Next = current;
                current.Prev = node;
            }

            this.m_count++;
        }

        public void AddRange(IEnumerable<T> collection)
        {
            if (collection is null)
                return;

            foreach (var item in collection)
                this.Add(item);
        }

        /// <summary>
        /// 清除所有元素
        /// </summary>
        public void Clear()
        {
            this.m_first = null;
            this.m_last = null;
            this.m_count = 0;
        }

        /// <summary>
        /// 判斷連結串列是否包含指定的元素
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public bool Contains(T value)
        {
            if (this.IsEmpty)
                return false;

            var current = this.m_first;
            while (null != current)
            {
                if (value.CompareTo(current.Value) == 0)
                    return true;

                current = current.Next;
            }

            return false;
        }

        public int IndexOf(T value)
        {
            if (this.IsEmpty)
                return -1;

            var current = this.m_first;
            var idx = 0;
            while (null != current)
            {
                if (value.CompareTo(current.Value) == 0)
                    return idx;

                idx++;
                current = current.Next;
            }

            return -1;
        }

        /// <summary>
        /// 獲取IEnumerator<T>
        /// </summary>
        /// <returns></returns>
        public IEnumerator<T> GetEnumerator()
        {
            return new Enumerator(this);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        /// <summary>
        /// 刪除指定的元素
        /// </summary>
        /// <param name="value"></param>
        public void Remove(T value)
        {
            if (this.IsEmpty)
                return;

            var current = this.m_first;
            while (null != current)
            {
                if (value.CompareTo(current.Value) == 0)
                    break;

                current = current.Next;
            }

            if (null != current)
            {
                var prev = current.Prev;
                var next = current.Next;

                if (null != prev && null != next)
                {
                    prev.Next = next;
                    next.Prev = prev;
                }
                else if (null != prev)
                    this.SetLast(prev);
                else
                    this.SetFirst(next);

                this.m_count--;
            }
        }

        /// <summary>
        /// 返回並刪除第一個元素
        /// </summary>
        /// <returns></returns>
        public T TakeFirst()
        {
            if (this.IsEmpty)
                return default(T);

            var value = this.m_first.Value;

            this.SetFirst(this.m_first.Next);
            this.m_count--;

            return value;
        }

        /// <summary>
        /// 返回並刪除最後一個元素
        /// </summary>
        /// <returns></returns>
        public T TakeLast()
        {
            if (this.IsEmpty)
                return default(T);

            var value = this.m_last.Value;

            this.SetLast(this.m_last.Prev);
            this.m_count--;

            return value;
        }

        public T[] ToArray()
        {
            if (this.IsEmpty)
                return null;

            var a = new T[this.m_count];
            var current = this.m_first;
            var idx = 0;
            while (null != current)
            {
                a[idx++] = current.Value;
                current = current.Next;
            }

            return a;
        }

        public List<T> ToList()
        {
            if (this.IsEmpty)
                return null;

            var l = new List<T>(this.m_count);
            var current = this.m_first;
            while (null != current)
            {
                l.Add(current.Value);
            }

            return l;
        }

        private void SetFirst(Node first)
        {
            this.m_first = first;
            if (this.m_first is null)
                this.m_last = null;
            else
                this.m_first.Prev = null;
        }

        private void SetLast(Node last)
        {
            this.m_last = last;
            if (this.m_last is null)
                this.m_first = null;
            else
                this.m_last.Next = null;
        }

        /// <summary>
        /// 列舉器
        /// </summary>
        private class Enumerator : IEnumerator<T>
        {
            private readonly SortedList<T> m_list;
            private readonly Node m_prevFirst = new Node(default(T));
            private Node m_current;

            public Enumerator(SortedList<T> list)
            {
                this.m_list = list;
                this.Reset();
            }

            public T Current
            {
                get
                {
                    if (null != this.m_current)
                        return this.m_current.Value;
                    else
                        return default(T);
                }
            }

            object IEnumerator.Current
            {
                get
                {
                    if (null != this.m_current)
                        return this.m_current.Value;
                    else
                        return default(T);
                }
            }

            public void Dispose()
            {
                // do nothing
            }

            public bool MoveNext()
            {
                if (object.ReferenceEquals(this.m_current, this.m_prevFirst))
                    this.m_current = this.m_list.m_first;
                else
                    this.m_current = this.m_current?.Next;

                return null != this.m_current;
            }

            public void Reset()
            {
                this.m_current = this.m_prevFirst;
            }
        }

        /// <summary>
        /// 連結串列節點
        /// </summary>
        private class Node
        {
            public Node(T data)
            {
                this.Value = data;
            }

            public Node Next { get; set; }

            public Node Prev { get; set; }

            public T Value { get; }

            public override string ToString()
            {
                if (null != Value)
                    return Value.ToString();
                return null;
            }
        }
    }
}
SortedList
路徑查詢演算法應用之A*演算法
namespace Pathfinding
{
    /// <summary>
    /// 方向
    /// </summary>
    public enum Orientation
    {
        /// <summary>
        /// 無方向
        /// </summary>
        None = 0,
        /// <summary>
        ////// </summary>
        East = 0x1,
        /// <summary>
        ////// </summary>
        South = 0x10,
        /// <summary>
        /// 西
        /// </summary>
        West = 0x100,
        /// <summary>
        ////// </summary>
        North = 0x1000,
        /// <summary>
        /// 東西
        /// </summary>
        EastWest = East | West,
        /// <summary>
        /// 南北
        /// </summary>
        NorthSouth = South | North,
        /// <summary>
        /// 東南
        /// </summary>
        SouthEast = East | South,
        /// <summary>
        /// 西南
        /// </summary>
        SouthWest = South | West,
        /// <summary>
        /// 西北
        /// </summary>
        NorthWest = West | North,
        /// <summary>
        /// 東北
        /// </summary>
        NorthEast = North | East,
    }
}
Orientation
路徑查詢演算法應用之A*演算法
namespace Pathfinding
{
    public static class OrientationExtension
    {
        /// <summary>
        /// 是否為東西方向
        /// </summary>
        /// <param name="orient"></param>
        /// <returns></returns>
        public static bool IsEastWest(this Orientation orient)
        {
            return orient == Orientation.East
                || orient == Orientation.West
                || orient == Orientation.EastWest;
        }

        /// <summary>
        /// 是否為南北方向
        /// </summary>
        /// <param name="orient"></param>
        /// <returns></returns>
        public static bool IsNorthSouth(this Orientation orient)
        {
            return orient == Orientation.South
                || orient == Orientation.North
                || orient == Orientation.NorthSouth;
        }

        /// <summary>
        /// <para>把方向轉換為EastWest或NorthSouth</para>
        /// <para>如果方向不是東西方向或南北方向,則返回None</para>
        /// </summary>
        /// <param name="orient"></param>
        /// <returns></returns>
        public static Orientation ConvertToEWOrNS(this Orientation orient)
        {
            if (orient.IsEastWest())
                return Orientation.EastWest;
            else if (orient.IsNorthSouth())
                return Orientation.NorthSouth;
            else
                return Orientation.None;
        }
    }
}
OrientationExtension
路徑查詢演算法應用之A*演算法
using System;
using System.Drawing;

namespace Pathfinding
{
    public static class PointExtension
    {
        /// <summary>
        /// <para>獲取第二個點相對於第一個點的方位</para>
        /// <para>此方法只判斷是否為正南,正北,正東或正西四個方向。</para>
        /// <para>如果兩個點座標一樣,則返回Orientation.None。</para>
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        /// <returns>East、South、West、North</returns>
        public static Orientation GetOrientation(this Point from, Point to)
        {
            if (from.X == to.X)
            {
                if (to.Y > from.Y)
                    return Orientation.South;
                else if (to.Y < from.Y)
                    return Orientation.North;
            }
            else if (from.Y == to.Y)
            {
                if (to.X > from.X)
                    return Orientation.East;
                else if (to.X < from.X)
                    return Orientation.West;
            }

            return Orientation.None;
        }

        /// <summary>
        /// <para>判斷兩點之間的相對位置:東西方向或南北方向</para>
        /// <para>如果兩個點座標一樣或不是東西或南北方向,則返回Orientation.None。</para>
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        /// <returns>EastWest或NorthSouth</returns>
        public static Orientation GetOrientationEWOrNS(this Point from, Point to)
        {
            if (from.X == to.X && to.Y != from.Y)
            {
                return Orientation.NorthSouth;
            }
            else if (from.Y == to.Y && to.X != from.X)
            {
                return Orientation.EastWest;
            }

            return Orientation.None;
        }

        /// <summary>
        /// 兩點的位置是否為東西方向:Y座標相等,且X座標不相等
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        /// <returns></returns>
        public static bool IsEastWest(this Point from, Point to)
        {
            return from.Y == to.Y && to.X != from.X;
        }

        /// <summary>
        /// 兩點的位置是否為南北方向:X座標相等,且Y座標不相等
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        /// <returns></returns>
        public static bool IsNorthSouth(this Point from, Point to)
        {
            return from.X == to.X && to.Y != from.Y;
        }

        /// <summary>
        /// 計算兩點之間的距離(僅計算東西和南北方向的距離)
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static int GetAlignedDistanceTo(this Point a, Point b)
        {
            if (a.IsEastWest(b))
                return Math.Abs(a.X - b.X);
            else
                return Math.Abs(a.Y - b.Y);
        }

        /// <summary>
        /// 計算兩點之間的距離
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        /// <returns></returns>
        public static double GetDistanceTo(this Point from, Point to)
        {
            return Math.Sqrt(Math.Pow(from.X - to.X, 2.0d) + Math.Pow(from.Y - to.Y, 2.0d));
        }

        /// <summary>
        /// 判斷兩個點是否在東西方向或南北方向的同一條直線上
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static bool InStraightLine(this Point a, Point b)
        {
            return a.X == b.X || a.Y == b.Y;
        }
    }
}
PointExtension

 

相關文章