前言
A*算法?具体实现?主要步骤?特此学习并记录一下自己的理解
B站:A*算法——讲解细致,推荐观看和理解源码
一、寻路算法:
1)广度优先遍历:
从起点开始,首先遍历起点周围邻近的点,然后再遍历已经遍历过的点邻近的点,逐步的向外扩散,直到找到终点
2)Dijkstra算法
计算每一个节点距离起点的移动代价
f
(
x
)
=
g
(
x
)
\ f(x)=g(x) \,
f(x)=g(x)
3)最佳优先搜索
预先计算出每个节点到终点的预估代价
f
(
x
)
=
h
(
x
)
\ f(x)=h(x) \,
f(x)=h(x)
二、A*算法
1)概念
A*算法是寻路算法的一种,是基于启发式函数改变节点选择规则的搜索。
f
(
x
)
=
g
(
x
)
+
h
(
x
)
\ f(x)=g(x)+h(x) \,
f(x)=g(x)+h(x)
- f(x)是当前节点的总代价,每次遍历总代价最小或者总代价相同但h(x)最小的为新的当前节点
- g(x) 是当前节点距离起点的移动代价。
- h(x)是当前节点距离终点的预估代价,也叫乐观代价。它总是小于等于实际到终点的代价,是A*算法的启发函数
注意:不考虑某些区域权值过大等问题,只考虑路径最短的情况,状态就是节点,代价就是距离
2)距离
代价是根据距离来定的,最常见的距离定义有:
-
曼哈顿距离:不能走斜线,只能按照固定的轴
h ( x ) = ∣ s t a r t . x − e n d . x ∣ + ∣ s t a r t . y − e n d . y ∣ \ h(x)=|start.x-end.x|+|start.y-end.y| \, h(x)=∣start.x−end.x∣+∣start.y−end.y∣ -
欧几里得距离:能走斜线,但计算要复杂一些,造成更大的性能开销
h ( x ) = ( s t a r t . x − e n d . x ) 2 + ( s t a r t . y − e n d . y ) 2 \ h(x)=\sqrt{(start.x-end.x)^{2}+(start.y-end.y)^{2}}\, h(x)=(start.x−end.x)2+(start.y−end.y)2
3)实现
主要步骤
- 制作地图:随机生成或者自定义生成(数组即可)
- 节点定义:定义一个NodeBase class类,为了翻转链表(从终点又找到起点)要设置节点的父节点,为了计算代价要定义G,H,F,为了遍历周围节点要获取距离并找到临近节点。
public class NodeBase{
//每个节点内部保存临近节点,有一定开销
public List<NodeBase> Neighbors { get; protected set; }
//获取距离的方法以及坐标属性
public interface ICoords {
public float GetDistance(ICoords other);
public Vector2 Pos { get; set; }
}
//父节点关联
public NodeBase Connection { get; private set; }
public void SetConnection(NodeBase nodeBase) {
Connection = nodeBase;
}
//定义G代价
public float G { get; private set; }
public void SetG(float g) {
G = g;
}
//定义H代价
public float H { get; private set; }
public void SetH(float h) {
H = h;
}
//定义F代价
public float F => G + H;
}
-
计算代价:
GetDistance() 获取距离的方法可以直接利用坐标进行距离计算,不同的距离公式计算的方式不同
G(x) 直接由起点开始进行+1操作的计算,根据路径进行更新
H(x) 直接利用坐标进行计算 -
搜寻路径(包括找到最佳路径后反转链表):
//具体翻看视频中的源码
public static class Pathfinding {
public static List<NodeBase> FindPath(NodeBase startNode, NodeBase targetNode) {
//开集存储未探索周围节点,遍历前将起点放入开集
var toSearch = new List<NodeBase>() { startNode };
//闭集存储已遍历节点
var processed = new List<NodeBase>();
//Any判断List是否为空,为空或者找到终点会跳出循环
while (toSearch.Any()) {
//查找开集中的F(x)最小或者F(x)相同但H(x)最小
var current = toSearch[0];
foreach (var t in toSearch)
if (t.F < current.F || t.F == current.F && t.H < current.H) current = t;
//遍历后找到开集中F(x)最小的节点加入闭集
processed.Add(current);
toSearch.Remove(current);
//如果当前节点等于终点节点(找到终点)
if (current == targetNode) {
//这里只是用list简单存储路径,并未进行翻转
//提供两种思路,栈存储再弹出或者直接翻转list数组(或者进行索引直接输出)
var currentPathTile = targetNode;
var path = new List<NodeBase>();
//一直查找到起点位置
while (currentPathTile != startNode) {
path.Add(currentPathTile);
currentPathTile = currentPathTile.Connection;
}
return path;
}
//由于节点内部保存了临近节点,所以直接限制这些节点不是障碍并且不在闭集中(闭集默认不会修改,路径)
//两种情况:第一种在开集中,第二种未遍历,不在开集和闭集中
//1,开集中的临近节点要判断是否进行更新
//2,未遍历的节点要添加进开集
foreach (var neighbor in current.Neighbors.Where(t => t.Walkable && !processed.Contains(t))) {
var inSearch = toSearch.Contains(neighbor);
//计算current周围neighbor的距离G(x)
var costToNeighbor = current.G + current.GetDistance(neighbor);
//不管是否在开集中都要更新临近节点的G(x)和父节点
if (!inSearch || costToNeighbor < neighbor.G) {
neighbor.SetG(costToNeighbor);
neighbor.SetConnection(current);
if (!inSearch) {
//如果只是未遍历节点,还要设置H,并把此节点加入开集
neighbor.SetH(neighbor.GetDistance(targetNode));
toSearch.Add(neighbor);
}
}
}
}
return null;
}
核心伪代码
创建开集和闭集,并将起点加入闭集
while循环,只要开集不为空或者当前节点不是终点
获取当前节点为开集中F(x)最小||F(x)相同但H(x)最小,移动到闭集中
如果当前节点为终点,找到最佳路径,回溯并跳出循环
如果不是终点,就遍历周围不在闭集且不是障碍的节点
如果这个节点在开集中,计算G(x)比原来更优,更新父节点和G(x)
否则如果这个节点未加入两个集合中,设置节点状态,将节点加入开集
如果开集为空,说明不存在路径,返回null
三、简单实现A*算法
在unity中创建一个空物体挂载即可运行,手动修改地图
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class easy : MonoBehaviour
{
//1,地图定义,直接二维数组
public static int wide = 5;
public static int height = 5;
//有路的情况
//public static int[,] tail = new int[,]
//{
// {0,0,-1, 0, 0 },
// {0,0, 0, 0, 0 },
// {0,0,-1,-1, 0 },
// {0,0,-1, 0, 0 },
// {0,0,-1, 0, 0 },
//};
//没路的情况
public static int[,] tail = new int[,]
{
{0,0,-1, 0, 0 },
{0,0, -1, 0, 0 },
{0,0,-1,-1, 0 },
{0,0,-1, 0, 0 },
{0,0,-1, 0, 0 },
};
//2,定义节点
public class NodeBase
{
//构造函数
public NodeBase(Vector2 _pos)
{
this.pos = _pos;
Parent = null;
}
//父节点
public NodeBase Parent;
//坐标
public Vector2 pos;
//距离,未定义属性去增加代码复杂度
public float G { get; set; }
public float H { get; set; }
public float F { get; set; }
}
//3,计算代价,曼哈顿距离,坐标格子之间距离为一,没有必要写新方法去获取距离
List<NodeBase> AstarFindWay(NodeBase startNode, NodeBase endNode)
{
//开集存储未探索周围节点,遍历前将起点放入开集
var toSearch = new List<NodeBase>() { startNode };
//闭集存储已遍历节点
var processed = new List<NodeBase>();
//Any判断List是否为空,为空或者找到终点会跳出循环
while (toSearch.Count > 0)
{
//查找开集中的F(x)最小或者F(x)相同但H(x)最小
var current = toSearch[0];
foreach (var t in toSearch)
if (t.F < current.F || t.F == current.F && t.H < current.H) current = t;
//遍历后找到开集中F(x)最小的节点加入闭集
processed.Add(current);
toSearch.Remove(current);
//如果当前节点等于终点节点(找到终点)
if (isEqualNode(current,endNode))
{
//这里只是用list简单存储路径,并未进行翻转
//提供两种思路,栈存储再弹出或者直接翻转list数组(或者进行索引直接输出)
var currentPathTile = current;
var path = new List<NodeBase>();
//一直查找到起点位置
while (!isEqualNode(currentPathTile, startNode))
{
path.Add(currentPathTile);
currentPathTile = currentPathTile.Parent;
}
return path;
}
//拿到节点的临近节点的集合
List<NodeBase> Neighbors = GetNeighbors(current);
//两种情况:第一种在开集中,第二种未遍历,不在开集和闭集中
//1,开集中的临近节点要判断是否进行更新
//2,未遍历的节点要添加进开集
foreach (var neighbor in Neighbors.Where(t => !Contains(processed,t)))
{
var inSearch = Contains(toSearch,neighbor);
//计算current周围neighbor的距离G(x)
var costToNeighbor = current.G + 1;
if (inSearch)
{
if (costToNeighbor < neighbor.G)
{
neighbor.G = costToNeighbor;
neighbor.Parent = current;
neighbor.F = neighbor.G + neighbor.H;
}
}
else
{
neighbor.Parent = current;
neighbor.G = costToNeighbor;
//如果只是未遍历节点,还要设置H,并把此节点加入开集
neighbor.H = Mathf.Abs(neighbor.pos.x - endNode.pos.x) + Mathf.Abs(neighbor.pos.y - endNode.pos.y);
neighbor.F = neighbor.G + neighbor.H;
toSearch.Add(neighbor);
}
}
}
return null;
}
//临近节点只考虑前后左右
List<NodeBase> GetNeighbors(NodeBase currentNode)
{
Vector2 up = new Vector2(currentNode.pos.x, currentNode.pos.y + 1);
Vector2 down = new Vector2(currentNode.pos.x, currentNode.pos.y - 1);
Vector2 left = new Vector2(currentNode.pos.x - 1, currentNode.pos.y);
Vector2 right = new Vector2(currentNode.pos.x + 1, currentNode.pos.y);
List<NodeBase> nodeList = new List<NodeBase>();
if (isValidPos(up))
{
nodeList.Add(new NodeBase(up));
}
if (isValidPos(down))
{
nodeList.Add(new NodeBase(down));
}
if (isValidPos(left))
{
nodeList.Add(new NodeBase(left));
}
if (isValidPos(right))
{
nodeList.Add(new NodeBase(right));
}
return nodeList;
}
//判断这个坐标是否有节点并且可移动
bool isValidPos(Vector2 pos)
{
if (pos.x < 0 || pos.x > wide - 1 || pos.y < 0 || pos.y > height - 1)
{
return false;
}
return tail[(int)pos.x, (int)pos.y] == 0 ? true : false;
}
//判断相等
bool isEqualNode(NodeBase a,NodeBase b)
{
return (int)a.pos.x == (int)b.pos.x && (int)a.pos.y == (int)b.pos.y;
}
//坐标相同说明一样
bool Contains(List<NodeBase> nodeList,NodeBase nodeBase)
{
for(int i = 0; i < nodeList.Count; ++i)
{
if (isEqualNode(nodeBase, nodeList[i]))
{
return true;
}
}
return false;
}
private void Start()
{
//创建起点和终点,调用搜寻路径方法
NodeBase startNode = new NodeBase(new Vector2(0, 0));
NodeBase endNode = new NodeBase(new Vector2(4, 4));
List<NodeBase> AstarNode = AstarFindWay(startNode, endNode);
//输出路径
if (AstarNode != null)
{
string str = "最佳路径:";
for (int i = AstarNode.Count - 1; i >= 0; --i)
{
str += " [" + (int)AstarNode[i].pos.x + "," + (int)AstarNode[i].pos.y + "] ";
}
Debug.Log(str);
}
else
{
Debug.Log("八嘎呀路,死路一条");
}
}
}
有路的情况:
无路的情况:
总结
实现简单A*算法用到的很多方法需要重写,==,List的Contains(),临近节点等都需要判断坐标去实现。