核心代碼
使用說明:
需要自行設置,地圖數據,起點,終點
直接調用 AStarPath.FindPath 即可
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// A星尋路 /// </summary> public class AStarPath { /// <summary> /// 尋路 /// </summary> /// <param name="map">地圖數據</param> /// <param name="start">起點</param> /// <param name="end">終點</param> /// <returns></returns> public static List<Point> FindPath(Point[,] map, Point start, Point end) { List<Point> openList = new List<Point>(); List<Point> closeList = new List<Point>(); openList.Add(start); while(openList.Count>0)//只要開放列表還存在元素就繼續 { Point point = GetMinFOfList(openList);//選出open集合中F值最小的點 openList.Remove(point); closeList.Add(point); List<Point> SurroundPoints = GetSurroundPoint(map, point.X, point.Y); foreach(Point p in closeList)//在周圍點中把已經在關閉列表的點刪除 { if(SurroundPoints.Contains(p)) { SurroundPoints.Remove(p); } } foreach (Point p in SurroundPoints)//遍歷周圍的點 { if (openList.Contains(p))//周圍點已經在開放列表中 { //重新計算G,如果比原來的G更小,就更改這個點的父親 int newG = 1 + point.G; if(newG<p.G) { p.SetParent(point, newG); } } else { //設置父親和F並加入開放列表 p.parent = point; GetF(p, end); openList.Add(p); } } if (openList.Contains(end))//只要出現終點就結束 { break; } } List<Point> pathList = new List<Point>(); Point temp = end.parent; if (temp == null) { //路線不通 Debug.Log("選擇的路線不通"); return null; } while(temp!=start) { pathList.Add(temp); temp = temp.parent; } return pathList; } //得到一個點周圍的點 static List<Point> GetSurroundPoint(Point[,] map, int x,int y) { List<Point> PointList = new List<Point>(); if(x > 0 && !map[x-1,y].isObstacle) { PointList.Add(map[x - 1, y]); } if(y > 0 && !map[x , y-1].isObstacle) { PointList.Add(map[x, y - 1]); } if(x < map.GetLength(0)-1 && !map[x + 1, y].isObstacle) { PointList.Add(map[x + 1, y]); } if(y < map.GetLength(1)-1 && !map[x , y+1].isObstacle) { PointList.Add(map[x, y + 1]); } return PointList; } //計算某個點的F值 static void GetF(Point point, Point end) { int G = 0; int H = Mathf.Abs(end.X - point.X) + Mathf.Abs(end.Y - point.Y); if(point.parent!=null) { G = 1 + point.parent.G; } int F = H + G; point.H = H; point.G = G; point.F = F; } //得到一個集合中F值最小的點 static Point GetMinFOfList(List<Point> list) { int min = int.MaxValue; Point point = null; foreach(Point p in list) { if(p.F<min) { min = p.F; point = p; } } return point; } } public class Point { public int X; public int Y; public int F;//總代價 public int G;//起點到此點的代價 public int H;//此點到終點的代價 public Point parent = null;//連接此點的上一個點 public bool isObstacle = false;//障礙物 //類型 空0 障礙物1 棋子2 //public int typeNum; public Point(int x,int y) { X = x; Y = y; } public void SetParent(Point parent,int g) { this.parent = parent; G = g; F = G + H; } }
A星尋路算法是什么#
游戲開發中往往有這樣的需求,讓玩家控制的角色自動尋路到目標地點,或是讓AI角色移動到目標位置,實際的情況可能很復雜,比如地圖上有無法通過的障礙或者需要付出代價(時間或其他資源)才能通過的河流、沼澤等,想要讓角色找到一條付出最小代價到達目標的路徑,就需要使用一些特殊的算法,而A星尋路算法就是目前應用最廣泛的尋路算法之一,unity asset store上廣受好評的A* Pathfinding project插件也是基於A星尋路算法實現的,簡單來說:A星算法是一種尋找最短路徑並避開障礙物的算法。
A星算法的基本概念#
要實現A星算法,首先需要將紛繁復雜的游戲地圖抽象成尋路網格,最簡單的方式是將游戲地圖划分為多個正方形單元或正多邊形單元,也可以划分為非均勻的凸多邊形,這些網格可以看做是一個個“尋路點”,網格越精細,尋路的效果越好,但計算量也越大,所以針對實際的游戲環境,需要好好平衡一下性能和效果。
A星算法的基本思想就是借助這些網格實現尋路,從起點開始遍歷四周的點,尋找最有可能在最短路徑上的點,並以這個點為基准繼續向四周遍歷,直至遍歷到終點,路徑也就找到了。
通過這個思想也可以看出,A星算法其實只能得到一種近似最優解,實際上對於尋路問題,往往存在不止一個最優解,如果非要找出所有的解就只能遍歷所有可能的路徑一一比較,但這樣效率太低,所以A星算法並不去遍歷整個地圖,而是只遍歷了最短路徑上的點和其周圍的點,所以得到的是一種近似最優解。
那么遍歷周圍的點時怎樣確定哪個點最有可能在最短路徑上呢?這就是A星算法的核心:F=G+H
每個尋路點都有F、G、H這三個屬性,F可以理解為通過這個點的總代價,代價越低,這個點當然就更有可能在最短路徑上。G是從起點到這個點的代價,H是從這個點到終點的代價,這兩個代價加起來就是這個點的總代價,關於具體如何計算,下面給出示例。
我們還需要兩個集合,一個是open集合,一個是close集合,open集合里存放的是還未計算代價的點,close集合里是已經計算過的點。開始時open集合里只有起點,close集合沒有元素,每次迭代將open集合里F最小的點作為基點,對於基點周圍的相鄰點做如下處理:
(1)如果這個點是障礙,直接無視。
(2)如果這個點不在open表和close表中,則加入open表
(3)如果這個點已經在open表中,並且當前基點所在路徑代價更低,則更新它的G值和父親
(4)如果這個點在close表中,忽略。
處理完之后將基點加入close集合。
當終點出現在open表中的時候,迭代結束。
如果到達終點前open表空了,說明沒有路徑可以到達終點。
A星算法實現#
下面來動手實現最簡單的A星算法,A星算法針對實際開發有着相當多的變化,怎樣設計跟游戲的需求有關,這里用unity來實現一個最基本的2D正方形網格尋路,實際開發中也可以直接使用unity的導航網格或者A* Pathfinding Project插件。
在這個實現中,我定義了一個10x10的網格,網格中有一些無法通過的障礙。
public class Point { public int X; public int Y; public int F; public int G; public int H; public Point parent=null; public bool isObstacle = false; public Point(int x,int y) { X = x; Y = y; } public void SetParent(Point parent,int g) { this.parent = parent; G = g; F = G + H; } }
這里定義了一個Point類代表每一個尋路點,X和Y代表坐標,F、G、H就是上面說的三個屬性,isObstacle代表這個點是否是障礙(無法通過),parent則代表這個點的父親結點,每當我們遍歷到下一個可能在最短路徑上的點時,就把它的父親設為當前結點,這樣尋路結束后我們可以從終點通過訪問父親結點一步步回溯到起點,將路徑存儲下來。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class AStar : MonoBehaviour { public const int width = 10; public const int height = 10; public Point[,] map = new Point[height,width]; public SpriteRenderer[,] sprites = new SpriteRenderer[height, width];//圖片和結點一一對應 public GameObject prefab; //代表結點的圖片 public Point start; public Point end; void Start() { InitMap(); //測試代碼 AddObstacle(2, 4); AddObstacle(2, 3); AddObstacle(2, 2); AddObstacle(2, 0); AddObstacle(6, 4); AddObstacle(8, 4); SetStartAndEnd(0, 0, 7, 7); FindPath(); ShowPath(); } public void InitMap()//初始化地圖 { for(int i=0;i<width;i++) { for (int j = 0; j < height; j++) { sprites[i, j] = Instantiate(prefab, new Vector3(i, j, 0),Quaternion.identity).GetComponent<SpriteRenderer>(); map[i, j] = new Point(i, j); } } } public void AddObstacle(int x,int y)//添加障礙 { map[x, y].isObstacle = true; sprites[x, y].color = Color.black; } public void SetStartAndEnd(int startX,int startY,int endX,int endY)//設置起點和終點 { start = map[startX,startY]; sprites[startX, startY].color = Color.green; end = map[endX, endY]; sprites[endX, endY].color = Color.red; } public void ShowPath()//顯示路徑 { Point temp = end.parent; while(temp!=start) { sprites[temp.X, temp.Y].color = Color.gray; temp = temp.parent; } } public void FindPath() { List<Point> openList = new List<Point>(); List<Point> closeList = new List<Point>(); openList.Add(start); while(openList.Count>0)//只要開放列表還存在元素就繼續 { Point point = GetMinFOfList(openList);//選出open集合中F值最小的點 openList.Remove(point); closeList.Add(point); List<Point> SurroundPoints = GetSurroundPoint(point.X,point.Y); foreach(Point p in closeList)//在周圍點中把已經在關閉列表的點刪除 { if(SurroundPoints.Contains(p)) { SurroundPoints.Remove(p); } } foreach (Point p in SurroundPoints)//遍歷周圍的點 { if (openList.Contains(p))//周圍點已經在開放列表中 { //重新計算G,如果比原來的G更小,就更改這個點的父親 int newG = 1 + point.G; if(newG<p.G) { p.SetParent(point, newG); } } else { //設置父親和F並加入開放列表 p.parent = point; GetF(p); openList.Add(p); } } if (openList.Contains(end))//只要出現終點就結束 { break; } } } public List<Point> GetSurroundPoint(int x,int y)//得到一個點周圍的點 { List<Point> PointList = new List<Point>(); if(x>0&&!map[x-1,y].isObstacle) { PointList.Add(map[x - 1, y]); } if(y>0 && !map[x , y-1].isObstacle) { PointList.Add(map[x, y - 1]); } if(x<height-1 && !map[x + 1, y].isObstacle) { PointList.Add(map[x + 1, y]); } if(y<width-1 && !map[x , y+1].isObstacle) { PointList.Add(map[x, y + 1]); } return PointList; } public void GetF(Point point)//計算某個點的F值 { int G = 0; int H = Mathf.Abs(end.X - point.X) + Mathf.Abs(end.Y - point.Y); if(point.parent!=null) { G = 1 + point.parent.G; } int F = H + G; point.H = H; point.G = G; point.F = F; } public Point GetMinFOfList(List<Point> list)//得到一個集合中F值最小的點 { int min = int.MaxValue; Point point = null; foreach(Point p in list) { if(p.F<min) { min = p.F; point = p; } } return point; } }
上面是A星算法的代碼,我使用了一張100x100像素的圖片代表每一個結點,修改它們的顏色用來表示起點、終點、障礙和路徑。在這里我計算的方式是每移動一個格子代價為1,所以起點的G值為0,每次遍歷把G+1,H則是當前結點和終點在x軸和y軸上的差之和。
最終效果(綠色代表起點,紅色代表終點,黑色代表障礙,灰色代表路徑)
尋路前
尋路結果
最后#
A星尋路有相當多可以擴展的地方,只要抓住核心,就是不斷計算周圍點的代價,找出花費最小代價到達終點的路徑,這個代價可以針對各種復雜的情況采取不同的計算方法,比如說一個FPS游戲的AI,游戲中玩家肯定會向火力范圍內的敵人攻擊,這時候如果為了走最短的路徑而暴露在玩家的槍口下就得不償失了,這時可以加大處在玩家攻擊范圍內的點的代價值,讓AI在更短路徑和受到攻擊的風險之間做出權衡,或者某個地方有獎勵道具,這時可以減少獎勵道具附近的點的代價值,讓AI更傾向於繞一些路去獲取道具,總之理解了算法思想,就能靈活運用於各種尋路情境。
轉載地址:https://www.cnblogs.com/LiveForGame/p/10528393.html