F(n) = G(n) + H(n)
G(n): 从起始位置到n位置所需的实际消耗
H(n): 从n位置到目标位置的预计消耗
所以F(n)就是从起始位置经过n位置到目标位置的路径总消耗
A*寻路的过程就是不断找最小F(n)的过程
1.加入“开启列表”
先将起点作为当前检查点加入“开启列表”,“开启列表”是一个等待检查方格的列表,起点格子是最开始的检查点
2.检查周围
将当前检查点周围可到达的点加入“开启列表”,并设置它们的"父节点"为当前的检查点,并且计算出这些点的G(n)、H(n)、F(n)值
如果周围的点已经有在“开启列表” 中,则我们需要对比新旧路径的G值,如果新路径的G值更小,则使用新路径,需要更新“父节点”
如果是障碍物,则忽略
3.移入“关闭列表”
将这个检查点从“开启列表”移到“关闭列表”,"关闭列表"中存放的都是不需要再次检查的格子点
4.继续找直到找不到或者找到为止
再从“开启列表”中找出F值最小的格子,然后从上述的第1步开始重复继续,直到找到的F值最小的格子为终点或者“开启列表”中没有格子了,即为结束
简单做了个示例演示A*寻路的过程,以辅助理解
地图随机生成指定数量的障碍物,随机指定起始位置和终点位置
Game视图 Esc重置地图 Space 执行一步寻路步骤
白色:未检查过的格子
黑色:障碍物
黄色:在“开启列表”中的格子
灰色:在“关闭列表”中的格子
红色:目标格子
绿色:当前正在检查的格子
绿色链:最终的寻路结果
格子左上角:G值
格子右上角:H值
格子左下角:F值
格子右下角:坐标
箭头:指向寻路过程中的父格子
完整工程:
Unity版本:2020.3.10
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AStarGrid
{
public int X { get; set; } //坐标X
public int Y { get; set; } //坐标Y
public float F { get { return G + H; } }
public float G { get; set; }
public float H { get; set; }
public bool IsObstacle { get; set; }
public AStarGrid ParentGrid { get; set; }
public AStarGrid(int x, int y)
{
this.X = x;
this.Y = y;
}
public void Reset()
{
G = 0;
H = 0;
IsObstacle = false;
ParentGrid = null;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AStarMap
{
AStarGrid[,] map;
int mapWidth;
int mapHeight;
//开启列表
Dictionary<int, AStarGrid> openList = new Dictionary<int, AStarGrid>();
//关闭列表
Dictionary<int, AStarGrid> closeList = new Dictionary<int, AStarGrid>();
public AStarMap(int mapWidth, int mapHeight)
{
this.mapWidth = mapWidth;
this.mapHeight = mapHeight;
map = new AStarGrid[mapWidth, mapHeight];
for (int w = 0; w < mapWidth; w++)
{
for (int h = 0; h < mapHeight; h++)
{
map[w, h] = new AStarGrid(w, h);
}
}
}
public void SetObstacle(int x, int y, bool isObstacle)
{
map[x, y].IsObstacle = isObstacle;
}
public AStarGrid GetGrid(int x, int y)
{
return map[x, y];
}
///
/// 直接寻路,返回寻路结果
///
///
///
///
///
///
public List<AStarGrid> FindPath(int startX, int startY, int targetX, int targetY)
{
openList.Clear();
closeList.Clear();
var startGrid = map[startX, startY];
var targetGrid = map[targetX, targetY];
var grid = startGrid;
AddToOpenList(grid, null, targetGrid); //先将起点加入“开启列表”,“开启列表”是一个等待检查方格的列表,起点格子是最开始的检查点
while (grid != null) //null时意味着找不到路径
{
if (grid == targetGrid) //找到了最终目标点
{
break;
}
//将当前检查点周围可到达的点加入“开启列表”,并设置它们的"父节点"为当前的检查点
//在这个过程中我们需要更新格子的F、G、H的值,F(n) = G(n) + H(n)
//G(n): 从起始点到n点所需的消耗
//H(n): 从n点到目标点预计的消耗
//F(n): 当前经过n点的这条路径的总消耗
//至于如何计算G、H值,需要自己定,G值是确定的,而H值是预计的,因此H值的计算准确直接会影响到寻路的效率(预计耗费有很多计算方法,我们要尽量找出最适合的计算方法,这样子才能更快速的找出“最短”路径)
AddAroundGridToOpenList(grid, targetGrid);
//将这个检查点移至“关闭列表”, "关闭列表"中存放的都是不需要再次检查的格子点
MoveGridToCloseFromOpen(grid);
//从“开启列表”中找出F值最低的格子作为下一个检查点,F值最低代表着可能是最快到达的路径
grid = FindMinFGrid();
}
List<AStarGrid> path = new List<AStarGrid>();
while (grid != null)
{
//通过不断的连接父节点,这样子列表的格子就是最终的路径
path.Add(grid);
grid = grid.ParentGrid;
}
return path;
}
#region 一步一步寻路演示
AStarGrid pathStartGrid, pathTargetGrid, pathCurrentGrid;
List<AStarGrid> pathResult = new List<AStarGrid>();
public void SetFindPath(int startX, int startY, int targetX, int targetY)
{
openList.Clear();
closeList.Clear();
pathResult.Clear();
pathStartGrid = map[startX, startY];
pathTargetGrid = map[targetX, targetY];
pathCurrentGrid = pathStartGrid;
AddToOpenList(pathCurrentGrid, null, pathTargetGrid);
}
///
/// 执行一次寻路步骤
///
/// 寻路是否完成
public bool DoFindPathStep()
{
if (pathCurrentGrid == null || pathCurrentGrid == pathTargetGrid)
return true;
AddAroundGridToOpenList(pathCurrentGrid, pathTargetGrid);
MoveGridToCloseFromOpen(pathCurrentGrid);
pathCurrentGrid = FindMinFGrid();
if (pathCurrentGrid == null)
return true;
if (pathCurrentGrid == pathTargetGrid)
{
while (pathCurrentGrid != null)
{
pathResult.Add(pathCurrentGrid);
pathCurrentGrid = pathCurrentGrid.ParentGrid;
}
return true;
}
return false;
}
public List<AStarGrid> GetPathResult()
{
return pathResult;
}
public bool IsCurrentCheckGrid(AStarGrid grid)
{
return grid == pathCurrentGrid;
}
public bool IsTargetGrid(AStarGrid grid)
{
return grid == pathTargetGrid;
}
#endregion
public bool IsInOpenList(AStarGrid grid)
{
return openList.ContainsKey(GetGridIndex(grid));
}
public bool IsInCloseList(AStarGrid grid)
{
return closeList.ContainsKey(GetGridIndex(grid));
}
void AddToOpenList(AStarGrid grid, AStarGrid parentGrid, AStarGrid targetGrid)
{
int index = GetGridIndex(grid);
if (openList.ContainsKey(index))
{
float newG = ComputeG(grid, parentGrid);
if (newG < grid.G)
{
grid.ParentGrid = parentGrid;
grid.G = newG;
}
}
else
{
openList.Add(index, grid);
grid.ParentGrid = parentGrid;
UpdateGridGH(grid, targetGrid);
}
}
int GetGridIndex(AStarGrid grid)
{
return grid.Y * mapWidth + grid.X;
}
int GetGridIndex(int x, int y)
{
return y * mapWidth + x;
}
void AddAroundGridToOpenList(AStarGrid grid, AStarGrid targetGrid)
{
int cx, cy;
AStarGrid g;
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
cx = grid.X + x;
cy = grid.Y + y;
if (cx >= 0 && cx < mapWidth && cy >= 0 && cy < mapHeight)
{
g = map[cx, cy]; if (!g.IsObstacle && !closeList.ContainsKey(GetGridIndex(g)))
{
AddToOpenList(g, grid, targetGrid);
}
}
}
}
}
void MoveGridToCloseFromOpen(AStarGrid grid)
{
int index = GetGridIndex(grid);
openList.Remove(index);
if (!closeList.ContainsKey(index))
{
closeList.Add(index, grid);
}
}
void UpdateGridGH(AStarGrid grid, AStarGrid targetGrid)
{
var parentGrid = grid.ParentGrid;
grid.G = ComputeG(grid, parentGrid);
grid.H = Mathf.Sqrt((float)((targetGrid.X - grid.X) * (targetGrid.X - grid.X) + (targetGrid.Y - grid.Y) * (targetGrid.Y - grid.Y)));
}
float ComputeG(AStarGrid grid, AStarGrid parentGrid)
{
float g = 0;
if (parentGrid != null)
{
g = parentGrid.G + Mathf.Sqrt((float)((parentGrid.X - grid.X) * (parentGrid.X - grid.X) + (parentGrid.Y - grid.Y) * (parentGrid.Y - grid.Y)));
}
return g;
}
AStarGrid FindMinFGrid()
{
AStarGrid minFGrid = null;
foreach (var grid in openList.Values)
{
if (minFGrid == null)
{
minFGrid = grid;
}
else
{
if (grid.F < minFGrid.F)
{
minFGrid = grid;
}
}
}
return minFGrid;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GridCtrl : MonoBehaviour
{
public Text txtF;
public Text txtG;
public Text txtH;
public Text txtCoordinate;
public RectTransform directionObj;
public Image bg;
AStarMap map;
AStarGrid grid;
public void SetGrid(AStarGrid grid, AStarMap map)
{
this.map = map;
this.grid = grid;
var rect = (transform as RectTransform).rect;
transform.position = new Vector3((rect.width + 5) * grid.X , (rect.height + 5) * grid.Y + 5, 0);
}
public void UpdateShow()
{
txtF.text = grid.F.ToString("f2");
txtG.text = grid.G.ToString("f2");
txtH.text = grid.H.ToString("f2");
txtCoordinate.text = string.Format("({0},{1})", grid.X, grid.Y);
if (grid.ParentGrid != null)
{
Vector2 parentGridDir = new Vector2(grid.ParentGrid.X - grid.X, grid.ParentGrid.Y - grid.Y);
float angle = Vector2.SignedAngle(parentGridDir, new Vector2(1, 0));
directionObj.rotation = Quaternion.Euler(0, 0, -angle);
directionObj.gameObject.SetActive(true);
}
else
{
directionObj.gameObject.SetActive(false);
}
if(grid.IsObstacle)
{
bg.color = Color.black;
}
else if(map.IsCurrentCheckGrid(grid))
{
bg.color = Color.green;
}
else if(map.IsTargetGrid(grid))
{
bg.color = Color.red;
}
else if(map.IsInOpenList(grid))
{
bg.color = Color.yellow;
}
else if(map.IsInCloseList(grid))
{
bg.color = Color.grey;
}
else
{
bg.color = Color.white;
}
}
public void SetBgColor(Color color)
{
bg.color = color;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AStarMapGenerator : MonoBehaviour
{
public int mapWidth = 5;
public int mapHeight = 5;
public GameObject GridPrefab;
public Vector2 startGrid;
public Vector2 targetGrid;
public bool isRandomMap;
public int randomObstacleCount;
public Vector2[] obstacles;
GridCtrl[,] gridCtrlMap;
AStarMap map;
void Start()
{
GenerateMap();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
DoPathFindStep();
}
if (Input.GetKeyDown(KeyCode.Escape))
{
ResetMap();
}
}
public void GenerateMap()
{
map = new AStarMap(mapWidth, mapHeight);
gridCtrlMap = new GridCtrl[mapWidth, mapHeight];
for (int y = 0; y < mapHeight; y++)
{
for (int x = 0; x < mapWidth; x++)
{
var grid = map.GetGrid(x, y);
var gridCtrl = GameObject.Instantiate<GameObject>(GridPrefab, transform).GetComponent<GridCtrl>();
gridCtrl.SetGrid(grid, map);
gridCtrlMap[x, y] = gridCtrl;
}
}
InitMap();
}
void ResetMap()
{
for (int y = 0; y < mapHeight; y++)
{
for (int x = 0; x < mapWidth; x++)
{
var grid = map.GetGrid(x, y);
grid.Reset();
}
}
InitMap();
}
void InitMap()
{
if (isRandomMap)
{
int obsCount = 0;
int rx, ry;
while (obsCount < randomObstacleCount)
{
rx = Random.Range(0, mapWidth);
ry = Random.Range(0, mapHeight);
if (!map.GetGrid(rx, ry).IsObstacle)
{
map.SetObstacle(rx, ry, true);
obsCount++;
}
}
AStarGrid sGrid, tGrid;
while (true)
{
rx = Random.Range(0, mapWidth);
ry = Random.Range(0, mapHeight);
if (!map.GetGrid(rx, ry).IsObstacle)
{
sGrid = map.GetGrid(rx, ry);
break;
}
}
while (true)
{
rx = Random.Range(0, mapWidth);
ry = Random.Range(0, mapHeight);
tGrid = map.GetGrid(rx, ry);
if (!tGrid.IsObstacle && tGrid != sGrid)
{
break;
}
}
map.SetFindPath(sGrid.X, sGrid.Y, tGrid.X, tGrid.Y);
Debug.Log(sGrid.X + " " + sGrid.Y + " " + tGrid.X + " " + tGrid.Y);
}
else
{
for (int i = 0; i < obstacles.Length; i++)
{
map.SetObstacle((int)obstacles[i].x, (int)obstacles[i].y, true);
}
map.SetFindPath((int)startGrid.x, (int)startGrid.y, (int)targetGrid.x, (int)targetGrid.y);
}
UpdateShow();
}
void DoPathFindStep()
{
bool isComplete = map.DoFindPathStep();
UpdateShow();
if (isComplete)
{
var path = map.GetPathResult();
if (path.Count > 0)
{
foreach (var grid in path)
{
gridCtrlMap[grid.X, grid.Y].SetBgColor(Color.green);
}
}
}
}
void UpdateShow()
{
for (int y = 0; y < mapHeight; y++)
{
for (int x = 0; x < mapWidth; x++)
{
gridCtrlMap[x, y].UpdateShow();
}
}
}
}