A*算法是最好优先算法的一种,是解决最短路径的有效方法。只要理解A*的工作原理,算法就不难实现。
1. A* 的工作原理
什么是寻路?寻路就是从有限的点中找出一条从起点到终点的路径。但是,寻路的过程中我们不知道下一个节点是什么,有可能下一个节点是一个墙,也可能是一个陷阱等等不能经过的点。所以我们需要不断的试探,直到我们从起点正确的走到了终点,这个试探的过程就结束了,如果四周都是不能经过的点封闭的,这个试探过程也要结束。
我们说了,A*是找出最短路径的算法,那么我们就需要在寻路的过程中考虑继续下一步的消耗,每次都是向消耗最少的点走,直到走到终点。所以A* = 试探+权重。
如图,左边的蓝色矩形表示起点,右边的蓝色矩形表示终点,紫色矩形表示墙,蓝色圆点表示试探路线上的路点,绿色圆点表示可选路点,红色圆点表示最优路线上的路点。我们从图中可以看出根据算法的设置,寻路会有一定的试探过程,但是保证每一步最优路线都是正确的,虽然可能有其他的走法(根据算法的设置不同,产生的最优路线有可能不同)。
我们要做的关键是找出每个点的权重,然后尽可能的绕过墙,找出最佳路线。那么这些点的权重是什么?
首先每个点到终点都有一个距离,根据曼哈顿算法,我们可以知道不能走斜线地情况下这两个点的距离是多少,我们假设这个值是H。然后每个点周围有8个点(4条边+4个顶点),这个点到周围的点又产生了一个距离,我们假设这段距离为G。现在这个点的权重F就变成了G+H,而这个权重是我们对点到终点的估计值,这个估计值如果接近点到终点的实际值,得到的路线越准确并且效率更高。
2. A*寻路的过程
首先我们准备一个关闭列表,表示我们不再检测的点,一开始这个列表中只有墙。
然后准备一个队列,描述进行下一步的点,并把起点放入队列中。
1. 从队列中取出权重最小的点(选择点),并找出它周围的8个点。对于刚开始,我们权重最小的点就是起点。
2. 对它周围的8个点计算出它们的权重F=G+H,让这8个点指向选择点,并放入到队列中。
3. 把取出的点放入进关闭列表,这个已经取出的点就不在检测了。
4. 继续回到第1步重复,直到队列为空时或者走到终点后结束。
PS:如果遇到了墙怎么办?按照上面的4步已经足够我们找出一个最简单的最优路径了。
如图,但是我们发现,在坐标(2,1)的位置找到的路径直接穿过了两个墙,如果这个墙是一个直线方向上的我们这样做还说的过去,但是现在这样子貌似会直接让我们直接撞在墙上。所以遇到墙时,需要有一个特殊的处理,让我们绕过这个墙。而这一个操作应该在我们找出周围的8个点之前进行,那就是找出当前选择点的上下左右四个方向是否有墙,因为斜方向上就算有墙也可以直接通过所以没有必要找斜方向的墙。找到这四个方向上的墙后,我们需要对这些墙周围的点根据选择点的斜方向做一次清理,因为这些墙上下左右方向上不会存在选择点斜方向产生的点。这个操作决定了寻路的路径是否会在遇到障碍时斜着穿过去。
3.A* Unity源码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PointStruct {
public PointStruct(int x, int y) {
this._x = x;
this._y = y;
}
private int _x;
private int _y;
public int x {
get { return _x; }
}
public int y {
get { return _y; }
}
private int _weight;
public int Weight {
get { return _weight; }
}
public void CalculateWeight() {
this._weight = this.G + this.H;
}
public int G = 0;
public int H = 0;
public int CalculateConsumeH(int x, int y) {
return Mathf.Abs(this._x - x) + Mathf.Abs(this._y - y);
}
public PointStruct parentPoint;
public int tag; // 0, 1表示点为墙壁, 2表示点在开放列表, 3表示点在关闭列表
}
首先需要准备这样一个结构,它保存了一个点的信息。parentPoint实现了每个点的链式存储。tag则表示这个点的状态。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AStarAlgorithm : MonoBehaviour {
public Transform aStarShownTransform;
public int SquareCount = 7; // 绘制 n*n 的方格
public Color SquareColor = Color.black;
private List<PointStruct> openList = new List<PointStruct>();
private List<PointStruct> closeList = new List<PointStruct>();
[System.Serializable]
public struct Point {
public int x;
public int y;
}
public Point[] wallPoints;
public Color WallColor;
public Point startPoint;
public Color StartPointColor;
public Point endPoint;
public Color EndPointColor;
public PointStruct startPointStruct;
public PointStruct[] points;
public Color BestPointColor;
public Color BestLocationColor;
public Color ForecastLocationColor;
private void Start() {
if (aStarShownTransform == null) {
throw new Exception("Transform is NULL.");
}
}
private void DrawSquare(Vector3 center, float edge) {
Vector3 topLeft = new Vector3(center.x - edge / 2, center.y - edge / 2);
Vector3 topRight = new Vector3(center.x + edge / 2, center.y - edge / 2);
Vector3 bottomLeft = new Vector3(center.x - edge / 2, center.y + edge / 2);
Vector3 bottomRight = new Vector3(center.x + edge / 2, center.y + edge / 2);
Gizmos.DrawLine(topLeft, topRight);
Gizmos.DrawLine(topLeft, bottomLeft);
Gizmos.DrawLine(topRight, bottomRight);
Gizmos.DrawLine(bottomLeft, bottomRight);
}
// 绘制Map
private void DrawMap() {
Gizmos.color = SquareColor;
int row = SquareCount;
int col = SquareCount;
int index = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
points[index] = new PointStruct(i, j);
points[index].tag = 0;
DrawSquare(new Vector3(i, j), 1);
index++;
}
}
}
// 绘制障碍点
private void DrawWall() {
if (wallPoints == null) {
Debug.LogWarning("wall is null");
return;
}
Gizmos.color = WallColor;
for (int i = 0; i < wallPoints.Length; i++) {
foreach (var v in points) {
if (v.x == wallPoints[i].x && v.y == wallPoints[i].y) {
v.tag = 1;
closeList.Add(v);
Gizmos.DrawCube(new Vector3(wallPoints[i].x, wallPoints[i].y), new Vector3(1, 1, 0));
}
}
}
}
// 绘制起点和终点
private void DrawStart() {
Gizmos.color = StartPointColor;
Gizmos.DrawCube(new Vector3(startPoint.x, startPoint.y), new Vector3(1, 1, 0));
foreach (var v in points) {
if (v.x == startPoint.x && v.y == startPoint.y) {
v.tag = 2;
startPointStruct = v;
startPointStruct.parentPoint = null;
openList.Add(v);
break;
}
}
}
private void DrawEnd() {
Gizmos.color = EndPointColor;
Gizmos.DrawCube(new Vector3(endPoint.x, endPoint.y), new Vector3(1, 1, 0));
}
// 绘制预测路径
private void DrawForecastLocation() {
// 所有试探的部分
foreach (var v in closeList) {
if (v.x == startPoint.x && v.y == startPoint.y) continue;
if (v.tag == 1) continue;
Gizmos.color = ForecastLocationColor;
Gizmos.DrawSphere(new Vector3(v.x, v.y), 0.2f);
}
}
// 绘制最优路径
private void DrawBestPoint(PointStruct best) {
Gizmos.color = BestPointColor;
Gizmos.DrawSphere(new Vector3(best.x, best.y), 0.2f);
Gizmos.color = Color.cyan;
Gizmos.DrawLine(new Vector3(best.x, best.y), new Vector3(best.parentPoint.x, best.parentPoint.y));
}
private void DrawBestLocation(PointStruct best) {
while (best.parentPoint != null) {
if (best.x != endPoint.x || best.y != endPoint.y) {
Gizmos.color = BestLocationColor;
Gizmos.DrawSphere(new Vector3(best.x, best.y), 0.2f);
} else {
Gizmos.color = BestLocationColor;
Gizmos.DrawSphere(new Vector3(best.x, best.y), 0.2f);
}
best = best.parentPoint;
}
if (best.parentPoint == null) {
Gizmos.color = BestLocationColor;
Gizmos.DrawSphere(new Vector3(best.x, best.y), 0.2f);
}
}
// 检测周围的墙
private List<PointStruct> DetectWalls(PointStruct p) {
List<PointStruct> psArray = new List<PointStruct>();
for (int i = -1; i < 2; i += 2) {
int x = p.x + i;
PointStruct temp1 = new PointStruct(x, p.y);
int y = p.y + i;
PointStruct temp2 = new PointStruct(p.x, y);
foreach (var v in points) {
if (v.x == temp1.x && v.y == temp1.y) temp1 = v;
else if (v.y == temp2.y && v.x == temp2.x) temp2 = v;
}
if (temp1.tag == 1) psArray.Add(temp1);
if (temp2.tag == 1) psArray.Add(temp2);
}
return psArray;
}
// 打印开放列表
private void PrintOpenList() {
foreach (var v in openList) {
Debug.Log($"{{{v.x}:{v.y}}}, Weight={v.Weight} G={v.G} H={v.H}");
}
}
// 计算最优点
private PointStruct CalculateBestPoint(PointStruct p, List<PointStruct> detectWalls) {
PointStruct best = p;
foreach (var v in points) {
int deltaX = Mathf.Abs(v.x - p.x);
int deltaY = Mathf.Abs(v.y - p.y);
float sqrt = Mathf.Sqrt(deltaX * deltaX + deltaY * deltaY);
if ((deltaX == 1 || deltaY == 1) && (int)sqrt == 1 && v.tag == 0) {
if (openList.Contains(v)) continue;
if (closeList.Contains(v)) continue;
bool enable = true;
if (detectWalls.Count > 0) {
foreach (var wall in detectWalls) {
if ((wall.x == v.x && Mathf.Abs(wall.y - v.y) == 1)
|| (wall.y == v.y && Mathf.Abs(wall.x - v.x) == 1)) {
enable = false;
}
}
}
if (!enable) continue;
v.G = (int)(sqrt * 10);
v.H = v.CalculateConsumeH(endPoint.x, endPoint.y);
v.CalculateWeight();
v.parentPoint = p;
v.tag = 2;
openList.Add(v);
DrawBestPoint(v);
if (v.x == endPoint.x && v.y == endPoint.y) { best = v; break; }
} else if (v.tag == 2) {
int sumG1 = p.parentPoint.G + p.G + v.G;
int sumG2 = p.parentPoint.G + v.G;
if (sumG1 > sumG2) {
continue;
}
}
}
return best;
}
private void SearchAllEnabledPoints(PointStruct p) {
if (openList.Contains(p)) {
openList.Remove(p);
p.tag = 3;
closeList.Add(p);
DrawForecastLocation();
}
// 斜穿过墙
// List<PointStruct> detectWalls = new List<PointStruct>();
List<PointStruct> detectWalls = DetectWalls(p);
PointStruct best = CalculateBestPoint(p, detectWalls);
// 最优路径
if (best.x == endPoint.x && best.y == endPoint.y) {
DrawBestLocation(best);
return;
}
// 递归回调
if (openList.Count > 0) BFS(openList[0]);
}
private void BFS(PointStruct p) {
PointStruct temp = p;
for (int i = 0; i < openList.Count; i++) {
if (temp.Weight > openList[i].Weight) {
temp = openList[i];
} else if (temp.Weight == 0) {
temp = openList[i];
}
}
SearchAllEnabledPoints(temp);
}
private void OnDrawGizmos() {
if (aStarShownTransform == null) return;
// restart per draw gizmos
Matrix4x4 defaultMatrix = Gizmos.matrix;
Gizmos.matrix = aStarShownTransform.localToWorldMatrix;
Color defaultColor = Gizmos.color;
openList = new List<PointStruct>();
closeList = new List<PointStruct>();
points = new PointStruct[SquareCount * SquareCount];
DrawMap();
DrawWall();
DrawStart();
DrawEnd();
BFS(openList[0]);
DrawMap();
Gizmos.color = defaultColor;
Gizmos.matrix = defaultMatrix;
}
}
这里我用List来实现存储找到的点,是为了找出最小权重的点时方便。