目录
A星寻路理论知识:
通过寻路消耗最小的点 所连起来的一条到达终点的路径
-
寻路消耗公式:
-
f = g + h; g:当前点离起点的直线距离(四方向就是1,八方向分两种情况1和1.41) , h : 当前点到终点的曼哈顿距离(四方向移动时,八方向移动用对角距离公式)
-
开启列表:
从起点开始,遍历周围的八个点,在这八个点中,如果点不在开启列表或关闭列表中 且 该点没有被标记为障碍物,则把该点放入开启列表,放入的同时计算寻路消耗;然后对开启列表中的元素进行从小到大的排序
3.关闭列表:
找到消耗最小的那个点(排序后,开启列表中的第一个元素),放入关闭列表中;
4.记录到达某一点时该点的父对象:
在把点放入列表的同时,记录该点的上一个点是哪一个点,这样可以帮助我们确定最终路径, 起点的父对象为null
如何确定最终路径: 在关闭列表中,每加入一个点的时候,都判断该点是否是终点,如果是,则根据该点及该点存储的父对象点一路往回找,依次放入列表中,形成一个路径
终点永远无法抵达的时候怎么判断?:
循环会一直执行,从开启列表往关闭列表中加入点,也就是把开启列表中寻路消耗最小的点“移”到关闭列表中,也就是开启列表中的点会不断减少,当开启列表中的点减少到0的时候,说明所有的点都找过一遍了,但就是到达不了终点,所以无法生成一条路径
代码思路
: 在函数的入口处,先清空先前一步操作留下的数据,然后重置一下起点的f、g、h的值,和父对象为null,把起点直接放入关闭列表中,然后开始死循环,从这个点开始,遍历周围八个点,判断是否满足能够加入开启列表的条件,能够加入的话,就顺便找出其父对象(就是找到当前点的起点(前一个点)),然后加入到开启列表中,同时计算其寻路消耗,再遍历完八个方向后,如果没有一个点加入到开启列表,也就是我们在这判断一下开启列表是否为空,若为空,说明永远无法找到终点,即返回null,
然后再开始对开启列表中的元素进行排序,从小到大,然后拿出开启列表中寻路消耗值最小的点,把它加入到关闭列表中并把下一次循环的起点设置为刚加入到关闭列表中的这个点,然后从开启列表中移除该点,然后判断,刚设置为起点的那个点(刚加入到关闭列表中的那个点)是否就是终点,即startNode == endNode,如果是的话,新建一个list表,根据endNode的父对象,一层层往上找,直到父对象为null,说明找到了起点,也就是生成了一条路径,然后对list进行反转,得到起点得到终点的路径,即path.Reverse(); 然后return path;
A*寻路启发函数
一.
四方向移动,当只存在上下左右的四方向移动时,A*寻路的启发式(h)可以换成哈曼顿距离,即当前点到终点的各个方向的差的绝对值之和,且g可以固定为一个单位长度,g的算法有两种
1.直接通过Vector.Distance来计算,不过还需要对单位长度进行转换
2.通过父对象的g值 + 1 个单位长度 得到该点的g值
八方向移动, 启发式h考虑对角距离公式
对角距离的计算方法
对于任意两点 (x1,y1) 和 (x2,y2),对角距离 D 可以这样计算:
-
dx=∣x2−x1∣
-
dy=∣y2−y1∣
-
D=D2⋅min(dx,dy)+D1⋅(max(dx,dy)−min(dx,dy))
其中 D1 是水平或垂直移动的成本(就是一个单位长度的格子在地图中的实际长度),D2 是对角线移动的成本。通常 D2=2×D1,因为在平面几何中,沿对角线移动的距离等于边长的根号2倍。
int dx = Math.Abs(endPos.x - node.x);
int dy = Math.Abs(endPos.y - node.y);
node.h = Math.Max(dx, dy) + (1.41 - 1) * Math.Min(dx, dy);
这里,1.41
大约是根号2,对应于对角线移动的成本(如果你的水平和垂直移动成本是1)。这样的启发式函数可以更准确地估计在8方向移动允许的情况下到达终点的实际成本。
g值可以在上下左右换成1个单位长度,斜角方向换成1.41个单位长度
对开启列表的寻路消耗进行排序
openList.Sort(ComparerMethod); //根据寻路消耗进行排序
private int ComparerMethod(AStarNode a, AStarNode b) { //if (a.f > b.f) // return 1; //else if (a.f == b.f) // return 1; //else // return -1; // 比较a和b的f值 if (a.f < b.f) return -1; // 如果a的f值小于b的f值,返回-1(a应该排在b前面) else if (a.f > b.f) return 1; // 如果a的f值大于b的f值,返回1(a应该排在b后面) else return b.g.CompareTo(a.g); // 寻路消耗相同的情况下,g值大的排前面,因为g越大说明离起点越远,走的就越远 }
在f值相同的情况下,可以根据实际情况考虑根据g值还是h值对开启列表中的寻路消耗进行排序
代码实现:AStarNode:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum E_Node_Type
{
Walk, //可行走点
Stop, //障碍点
}
/// <summary>
/// A星格子类
/// </summary>
public class AStarNode
{
//格子对象的坐标
public int x;
public int y;
//寻路消耗
public float f;
//离起点的距离
public float g;
//离终点的哈曼顿距离
public float h;
//当前点的父对象
public AStarNode father;
//格子的类型
public E_Node_Type type;
/// <summary>
/// 构造函数,传入坐标和格子类型
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="type"></param>
public AStarNode(int x, int y,E_Node_Type type)
{
this.x = x;
this.y = y;
this.type = type;
}
}
AStarMgr:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
/// <summary>
/// A星寻路管理器
/// </summary>
public class AStarMgr
{
private static AStarMgr instance;
public static AStarMgr Instance
{
get
{
if(instance == null)
{
instance = new AStarMgr();
}
return instance;
}
}
//地图的宽高
private int mapW;
private int mapH;
/// <summary>
/// 地图相关所有的格子对象容器
/// </summary>
public AStarNode[,] nodes;
// 走路的方向:
private int[] xPos = new int[] {-1, 0, 1, -1 , 1 , -1, 0, 1};
private int[] yPos = new int[] { -1, -1, -1, 0, 0, 1, 1, 1 };
//开启列表
private List<AStarNode> openList;
//关闭列表
private List<AStarNode> closeList;
/// <summary>
/// 初始化地图
/// </summary>
/// <param name="w"></param>
/// <param name="h"></param>
public void InitMapInfo(int w,int h)
{
mapW = w;
mapH = h;
nodes = new AStarNode[mapW,mapH];
for (int i = 0; i < mapW; i++)
{
for(int j = 0; j < mapH; j++)
{
AStarNode node = new AStarNode(i, j, UnityEngine.Random.Range(0, 100) < 20 ? E_Node_Type.Stop : E_Node_Type.Walk);
nodes[i, j] = node;
// nodes[i,j] = new AStarNode(i,j,E_Node_Type.Walk);
}
}
//临时设置两个障碍点,商业游戏中都是读取外部配置文件的
//nodes[0, 1].type = E_Node_Type.Stop;
//nodes[1,1].type = E_Node_Type.Stop;
openList = new List<AStarNode>();
closeList = new List<AStarNode>();
}
public List<AStarNode> FindPath(Vector2 startPos,Vector2 endPos)
{
if (!JudgeNode((int)startPos.x, (int)startPos.y) || !JudgeNode((int)endPos.x, (int)endPos.y))
{
Debug.Log("传入的起点或终点不合法");
return null;
}
//清空开启和关闭列表,清除上一次的数据
openList.Clear();
closeList.Clear();
AStarNode startNode = nodes[(int)startPos.x, (int)startPos.y];
AStarNode endNode = nodes[(int)endPos.x, (int)endPos.y];
startNode.father = null;
startNode.f = 0;
startNode.g = 0;
startNode.h = 0;
closeList.Add(startNode); //把最开始的起点加入到关闭列表中
while(true)
{
//首先判断传入的两个点是否合法:1.在地图范围内,2.不能是阻挡点
if (!JudgeNode((int)startPos.x, (int)startPos.y) || !JudgeNode((int)endPos.x, (int)endPos.y))
{
Debug.Log("传入的起点或终点不合法");
return null;
}
//得到起点和终点的格子
// startNode = nodes[(int)startPos.x, (int)startPos.y];
//找当前点周围的点
for (int i = 0; i < 8; i++)
{
//如果周围的那个点不合法,就继续找周围的下个点
if (!JudgeNode((int)startNode.x + xPos[i], (int)startNode.y + yPos[i])) continue;
//判断是否在开启列表或关闭列表中,不在的话,就可以放入开启列表
if (!JudgeList((int)startNode.x + xPos[i], (int)startNode.y + yPos[i]))
{
AStarNode node = nodes[(int)startNode.x + xPos[i], (int)startNode.y + yPos[i]];
openList.Add(node);
//放入列表的同时计算寻路消耗
node.father = startNode;
node.g = Vector2.Distance(new Vector2(startNode.x,startNode.y),new Vector2(node.x,node.y));
//node.h = Math.Abs(endPos.y - node.y) + Math.Abs(endPos.x - node.x);
int dx = (int)Math.Abs(endPos.x - node.x);
int dy = (int)Math.Abs(endPos.y - node.y);
node.h = Math.Max(dx, dy) + (1.41f - 1f) * Math.Min(dx, dy);
node.f = node.g + node.h;
}
}
if (openList.Count == 0)
{
Debug.Log("死路");
return null;
}
openList.Sort(ComparerMethod); //根据寻路消耗进行排序
closeList.Add(openList[0]); //放入关闭列表中
startNode = openList[0]; //改变起始位置为新加入到关闭列表中的点
openList.RemoveAt(0); //从开启列表中移除该点
/*startPos = new Vector2(closeList[closeList.Count-1].x, closeList[closeList.Count-1].y); */ //改变起始位置为新加入到关闭列表中的点
//判断该点是不是终点,是的话,就可以开始返回路径了,不是的话,就继续寻找下一个点
if (startNode == endNode)
{
List<AStarNode> path = new List<AStarNode>();
AStarNode currentNode = endNode;
path.Add(currentNode);
while (currentNode.father != null)
{
path.Add(currentNode.father);
currentNode = currentNode.father;
}
path.Reverse(); //反转一下,得到正确的路径
return path;
}
}
}
/// <summary>
/// 根据寻路消耗对开启列表中的格子进行排序,找到最小的那个
/// </summary>
private int ComparerMethod(AStarNode a, AStarNode b)
{
//if (a.f > b.f)
// return 1;
//else if (a.f == b.f)
// return 1;
//else
// return -1;
// 比较a和b的f值
if (a.f < b.f)
return -1; // 如果a的f值小于b的f值,返回-1(a应该排在b前面)
else if (a.f > b.f)
return 1; // 如果a的f值大于b的f值,返回1(a应该排在b后面)
else
return b.g.CompareTo(a.g); // 寻路消耗相同的情况下,g值大的排前面,因为g越大说明离起点越远,走的就越远
}
/// <summary>
/// 判断点是否合法
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
private bool JudgeNode(int x,int y)
{
if (x < 0 || y < 0 || x >= mapW || y >= mapH || nodes[x, y]?.type == E_Node_Type.Stop) return false;
return true;
}
/// <summary>
/// 判断一个点是否在开启列表或关闭列表中
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="index">移动方向的索引</param>
/// <returns></returns>
private bool JudgeList(int x,int y)
{
if(openList.Contains(nodes[x, y])|| closeList.Contains(nodes[x, y]))
{
return true;
}
return false;
}
}
测试代码:TestAStar
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestAStar : MonoBehaviour
{
//左上角第一个立方体的位置
public int beginX = -3;
public int beginY = 5 ;
//之后每一个立方体之间的偏移位置
public int offsetX = 2;
public int offsetY = 2;
//地图格子的宽高
public int mapW = 5;
public int mapH = 5;
//开始点,给一个负的
private Vector2 beginPos = Vector2.right * 1;
public Material red;
public Material green;
public Material yellow;
public Material Normal;
private List<AStarNode> list;
private Dictionary<string,GameObject> cubes = new Dictionary<string,GameObject>();
private void Start()
{
AStarMgr.Instance.InitMapInfo(mapW, mapH);
for (int i = 0; i < mapW; i++)
{
for(int j = 0; j < mapH; j++)
{
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj.transform.position = new Vector3(beginX + i * offsetX, beginY + j * offsetY, 0);
obj.name = i + "_" + j;
cubes.Add(obj.name, obj);
AStarNode node = AStarMgr.Instance.nodes[i, j];
if(node.type == E_Node_Type.Stop)
{
obj.GetComponent<MeshRenderer>().material = red;
}
}
}
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
RaycastHit info;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out info, 1000))
{
if (beginPos == Vector2.right * -1)
{
//先清理上一次的路径
if (list != null)
{
for(int i = 0;i < list.Count; i++)
{
cubes[list[i].x + "_" + list[i].y].GetComponent<MeshRenderer>().material = Normal;
}
}
string[] strs = info.collider.gameObject.name.Split('_');
//切割后得到行列位置,就是开始点的位置
beginPos = new Vector2(int.Parse(strs[0]), int.Parse(strs[1]));
info.collider.gameObject.GetComponent<MeshRenderer>().material = yellow;
}
else
{
//已经有起点了,开始点出终点
string[] strs = info.collider.gameObject.name.Split('_');
//得到终点
Vector2 endPos = new Vector2(int.Parse(strs[0]), int.Parse(strs[1]));
//开始寻路
list = AStarMgr.Instance.FindPath(beginPos, endPos);
if (list != null)
{
//找到路径了
for (int i = 0; i < list.Count; i++)
{
cubes[list[i].x + "_" + list[i].y].GetComponent<MeshRenderer>().material = green;
}
}
cubes[beginPos.x + "_" + beginPos.y].GetComponent<MeshRenderer>().material = Normal;
beginPos = Vector2.right * -1;
}
}
}
}
/*
private List<AStarNode> path;
void Start()
{
AStarMgr.Instance.InitMapInfo(10, 10);
path = AStarMgr.Instance.FindPath(new Vector2(0, 0), new Vector2(1, 2));
if (path != null)
{
for (int i = 0; i < path.Count; i++)
{
Debug.Log(path[i].x + " " + path[i].y);
Debug.Log("----");
}
}
else
{
print("找不到路径");
}
}
*/
}