简介
Astar是一种深度优先启发式寻路算法,广泛运用在游戏领域。提起Astar不得不提一下迪杰特斯拉算法,它是一种广度优先启发式寻路算法,俗称阉割版的Astar。因为大部分情况下Astar的效率都比迪杰特斯拉高,我们只需要稍作了解就可以。
Astar的原理,有一篇博客写的很详细,萌新推荐一下https://www.cnblogs.com/zhoug2020/p/3468167.html
代码
1.先定义一个地图上的节点
public class Node
{
public int x { get; }
public int y { get; }
public int g;
public int h;
public int f;
public bool walkable;//该节点是否可移动
public Node parent;
public Node(int x, int y)
{
this.x = x;
this.y = y;
}
/// <summary>
/// 清空节点上的消耗计算数据
/// </summary>
public void Clear()
{
g = 0;
h = 0;
f = 0;
parent = null;
}
}
2.获取一个节点周围节点的方法
这里是4方向战棋作为例子,如果8方向的话,4个顶点消耗应该高一些。
4方向战棋4个方向消耗都是1。
/// <summary>
/// 获取周围节点
/// </summary>
/// <param name="neighbourNode"></param>
/// <param name="startPoint"></param>
public void GetNeighbour(List<Node> neighbourNode, Node startPoint)
{
if (startPoint.x - 1 >= 0)
{
neighbourNode.Add(nodes[startPoint.x - 1, startPoint.y]);
}
if (startPoint.x + 1 < length)
{
neighbourNode.Add(nodes[startPoint.x + 1, startPoint.y]);
}
if (startPoint.y - 1 >= 0)
{
neighbourNode.Add(nodes[startPoint.x, startPoint.y - 1]);
}
if (startPoint.y + 1 < width)
{
neighbourNode.Add(nodes[startPoint.x, startPoint.y + 1]);
}
}
3.预估h值 曼哈顿算法
/// <summary>
/// 预估两个点间的消耗 曼哈顿算法
/// </summary>
/// <param name="startPoint"></param>
/// <param name="endPoint"></param>
/// <returns></returns>
public int GetH(Node startPoint, Node endPoint)
{
int x = startPoint.x >= endPoint.x ? startPoint.x - endPoint.x : endPoint.x - startPoint.x;
int y = startPoint.y >= endPoint.y ? startPoint.y - endPoint.y : endPoint.y - startPoint.y;
return x + y;
}
4.每次从开放列表取新节点计算之前 排序
因为数据少,所以用冒泡排序会更快一些
/// <summary>
/// 按F值冒泡排序
/// </summary>
/// <param name="list"></param>
public void SortListByF(List<Node> list)
{
if (list == null || list.Count == 0)
{
return;
}
for (int i = 0; i < list.Count - 1; i++)
{
for (int j = 0; j < list.Count - 1 - i; j++)
{
if (list[j].f > list[j + 1].f)
{
Node temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
}
}
}
}
5.核心的寻路算法
/// <summary>
/// 寻路
/// </summary>
/// <param name="startPoint"></param>
/// <param name="endPoint"></param>
/// <returns></returns>
public List<Node> FindPath(Vector3Int startPoint, Vector3Int endPoint)
{
List<Node> openList = new List<Node>();//开放列表
List<Node> closeList = new List<Node>();//关闭列表
List<Node> neighbourNodes = new List<Node>();//周围4个节点
Node startNode = nodes[startPoint.x, startPoint.y];//起始点
Node endNode = nodes[endPoint.x, endPoint.y];//目标点
if (startNode == endNode)
{
return GetPath(startNode);
}
openList.Add(startNode);
while (openList.Count > 0)
{
SortListByF(openList);//重新排序
startNode = openList[0];//获取当前最小F值的点
if (startNode == endNode) {
Debug.Log("get path");
return GetPath(startNode);
}
neighbourNodes.Clear();//清空
GetNeighbour(neighbourNodes, startNode); //获取周边4个节点
foreach (Node n in neighbourNodes)
{
//已经在关闭列表 本次不进行处理
if (closeList.Contains(n))
{
continue;
}
//如果该点不可移动
if (!n.walkable)
{
closeList.Add(n);
continue;
}
//消耗计算
int g = startNode.g + 1;
int h = GetH(n, endNode);
int f = g + h;
//检测是否已经探索过
if (openList.Contains(n))
{
//存在更优路线
if (n.g > g)
{
n.g = g;
n.f = g + n.h;
n.parent = startNode;
}
}
else
{
n.g = g;
n.h = h;
n.f = f;
n.parent = startNode;
openList.Add(n);
}
}
openList.Remove(startNode);
closeList.Add(startNode);//将该点加入关闭列表
}
Debug.Log("no path");
return null;
}
6.储存路径
寻路方法结束时,如果存在路径的话,路径保存在节点node的链表里,我们还需要一个方法取出路径。
另外,取完路径一定记得清空节点缓存的fgh值,不然下次调用数据发生错乱可能会死机233
/// <summary>
/// 得到路径
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public List<Node> GetPath(Node path)
{
List<Node> pathList = new List<Node>();
if (path != null)
{
pathList.Add(path);
}
if (path.parent != null)
{
while (path.parent != null)
{
pathList.Add(path.parent);
path = path.parent;
}
}
pathList.Reverse();//逆向排序 这里用栈去储存路径好了,栈结构刚好逆向输出,用list反而麻烦了,这个优化就留给读者好了
NodesClear(pathList);//清空计算过程中缓存的数据
return pathList;
}
/// <summary>
/// 清理节点上的数据
/// </summary>
/// <param name="paths"></param>
public void NodesClear(List<Node> paths)
{
foreach (var n in paths)
{
n.Clear();
}
}
7.A*的拓展
战棋游戏中,角色的移动范围搜索就是Astar的一个变种。
有大佬和我讨论说,如果角色移动力是3,就创建一个7*7的范围,然后去除掉曼哈顿值大于3的节点,就是移动范围。这样时间复杂度O(n)就能搞定。
实际上这么考虑不全面,角色有可能被围在一个四面是墙的空间,即便墙外的曼哈顿值小于等于3也没有办法走过去。
所以,移动范围还是考虑用Astar去搜索,攻击范围是穿透型的,直接用曼哈顿值计算就可以。
下面是代码示例
/// <summary>
/// 寻找移动范围
/// </summary>
/// <param name="startPoint"></param>
/// <param name="movePower"></param>
/// <returns></returns>
public List<Node> FindMove(Vector3Int startPoint, int movePower)
{
List<Node> openList = new List<Node>();//开放列表
List<Node> closeList = new List<Node>();//关闭列表
List<Node> neighbourNodes = new List<Node>();//周围4个节点
List<Node> moveRange = new List<Node>();
Node startNode = nodes[startPoint.x, startPoint.y];//初始节点
openList.Add(startNode);
moveRange.Add(startNode);
do
{
startNode = openList[0];//取g值最小的节点作为本次探索节点
neighbourNodes.Clear();//清空
GetNeighbour(neighbourNodes, startNode); //获取周边4个节点
foreach (Node n in neighbourNodes)
{
//已经在障碍物列表 本次不进行处理
if (closeList.Contains(n))
{
continue;
}
SrpgTile tile = terrain.GetTile<SrpgTile>(new Vector3Int(n.x, n.y, 0));
//如果该点不可移动
if (!tile.walkable || n.unit != null || n.obj != null)
{
closeList.Add(n);
continue;
}
int g = startNode.g + 1;
//检测是否已经探索过
if (openList.Contains(n))
{
if (n.g > g)
{
n.g = g;
n.parent = startNode;
}
continue;
}
n.g = g;
n.parent = startNode;
if (n.g <= movePower)
{
if (!moveRange.Contains(n))
{
moveRange.Add(n);
}
}
if (n.g < movePower)
{
openList.Add(n);
}
}
openList.Remove(startNode);//探索完毕 从开放列表中移除本次探索点
closeList.Add(startNode);//将该点加入关闭列表
} while (openList.Count > 0);
NodesClear(moveRange);
return moveRange;
}
8.优化
Astar寻路 起点与终点距离太远的时候,Astar的计算量会非常大,可以考虑用四叉树把地图分割一下,结合Astar寻路
四叉树的介绍链接https://blog.csdn.net/jxw167/article/details/81869949
9.效果
完美收工