A*算法广泛用于MMORPG游戏的寻路功能,本人在B站Up主唐老湿的视频中学习了一段时间,总结出其原理和实现过程。
视频链接
公式:f(寻路消耗)= g(自身离起点距离)+ h(自身离终点距离)
原理:
将地图分成若干个方块(结点),从起点周围的8个结点找到不是位于地图边缘和障碍物的结点,放到开启列表中,再从开启列表中找出f值最小的结点,放入关闭列表中;重复执行该步骤,直到起点等于终点为止。
此时,关闭列表存放的就是最短路径,从最后一个结点开始判断自身有无父结点,有的话就存放到新的列表path中。最后反转path列表,得到的就是由起点到终点的结点路径。
下面是代码以及效果图片展示:
**AStarManager:**用于A*核心算法和主要逻辑的实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AStarManager
{
private static AStarManager instance;
public static AStarManager Instance
{
get
{
if (instance == null)
instance = new AStarManager();
return instance;
}
}
///
/// 地图相关的所有格子对象容器
///
public AStarNode[,] nodes;
///
/// 开启列表
///
private List<AStarNode> openList = new List<AStarNode>();
///
/// 关闭列表
///
private List<AStarNode> closeList = new List<AStarNode>();
///
/// 地图的宽高
///
private int mapW;
private int mapH;
///
/// 初始化地图信息
///
/// 宽
/// 高
public void InitMapInfo(int w,int h)
{
//记录地图的宽高
this.mapW = w;
this.mapH = h;
//根据宽高,创建格子,障碍物可以随机生成
nodes = new AStarNode[w, h];
for (int i = 0; i < w; ++i)
{
for (int j = 0; j < h; ++j)
{
//随机生成的障碍物格子
//实际开发中障碍物需要从配置文件中获取
AStarNode node = new AStarNode(i, j, Random.Range(0, 100) > 20 ? E_Node_Type.Walk : E_Node_Type.Stop);
nodes[i, j] = node;
}
}
}
///
/// 寻路方法,提供给外界使用
///
///
///
///
public List<AStarNode> FindPath(Vector2 startPos,Vector2 endPos)
{
//实际项目中,传入的点是坐标系的点(有可能是小数,需要除以每个格子的宽高
//这里省略换算步骤,直接当作传进来的格子坐标
//判断传入的两个点是否合法
//1.要在地图范围内
if(startPos.x<0||startPos.x>=mapW||
startPos.y<0||startPos.y>=mapH||
endPos.x < 0 || endPos.x >= mapW ||
endPos.y < 0 || endPos.y >= mapH)
{
Debug.Log("开始或结束点在地图外面");
return null;
}
//2.不是障碍物
AStarNode start = nodes[(int)startPos.x, (int)startPos.y];
AStarNode end = nodes[(int)endPos.x, (int)endPos.y];
if (start.type == E_Node_Type.Stop || end.type == E_Node_Type.Stop)
{
Debug.Log("开始或结束点是障碍物");
return null;
}
//清空列表(清空上一次的相关数据,避免影响这一次的寻路计算
openList.Clear();
closeList.Clear();
//把开始点放入关闭列表中
start.father = null;
start.f = 0;
start.g = 0;
start.h = 0;
closeList.Add(start);
//如果不合法,直接返回null,意味着不能寻路
//应该得到起点和终点对应的格子
while (true)
{
//从起点开始,找周围的点,并放入开启列表中
//左上
FindNearlyNodeToOpenList(start.x - 1, start.y - 1, 1.4f, start, end);
//上
FindNearlyNodeToOpenList(start.x, start.y - 1, 1f, start, end);
//右上
FindNearlyNodeToOpenList(start.x + 1, start.y - 1, 1.4f, start, end);
//左
FindNearlyNodeToOpenList(start.x - 1, start.y, 1f, start, end);
//右
FindNearlyNodeToOpenList(start.x + 1, start.y, 1f, start, end);
//左下
FindNearlyNodeToOpenList(start.x - 1, start.y + 1, 1.4f, start, end);
//下
FindNearlyNodeToOpenList(start.x, start.y + 1, 1f, start, end);
//右下
FindNearlyNodeToOpenList(start.x + 1, start.y + 1, 1.4f, start, end);
//判断死路
if (openList.Count == 0)
{
Debug.Log("死路");
return null;
}
//判断这些点是不是边界和阻挡,是否在开启和关闭列表,如果都不是,才放入开启列表
//选出开启列表中,寻路消耗最小的点
openList.Sort(SortOpenList);
//放入关闭列表中,再从开启列表中移除该点
closeList.Add(openList[0]);
//开启列表的第一个点,变成下一次寻路的起点
start = openList[0];
openList.RemoveAt(0);
//如果这个点已经是终点了,那么得到最终的值返回出去
//如果这个点不是终点,那么继续寻路
if (start == end)
{
//找完了,已经找到路径了
List<AStarNode> path = new List<AStarNode>();
//将路径存起来
path.Add(end);
while (end.father!=null)
{
path.Add(end.father);
end = end.father;
}
//列表元素反转,得到由起点到终点的结点列表
path.Reverse();
return path;
}
}
}
///
/// 排序函数,找到开启列表中f值最小的点
///
///
///
///
private int SortOpenList(AStarNode a,AStarNode b)
{
if (a.f > b.f)
return 1;
else if (a.f == b.f)
return 1;
else
return -1;
}
///
/// 找到相邻的点并存放到开启列表中
///
///
///
/// g值
/// 上一个点
/// 终点
private void FindNearlyNodeToOpenList(int x,int y,float g,AStarNode father,AStarNode end)
{
//判断有没有超过地图边界
if (x < 0 || x >= mapW || y < 0 || y >= mapH)
return;
//在地图范围内,再去取点
AStarNode node = nodes[x, y];
//判断这些点是否是边界,是否是障碍物,是否在开启或关闭列表
if (node == null || node.type == E_Node_Type.Stop ||
openList.Contains(node) || closeList.Contains(node))
{
return;
}
//计算f值
//1.记录父对象
node.father = father;
//2.计算g值:自身离起点的距离 = 父节点离起点的距离 + 自身离父节点的距离
node.g = father.g + g;
//3.计算h值
node.h = Mathf.Abs(end.x - node.x) + Mathf.Abs(end.y - node.y);
//计算寻路消耗f
node.f = node.g + node.h;
//如果通过上面的验证,就存到开启列表中
openList.Add(node);
}
}
**AStarNode:**用于定义每个结点的基本属性
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum E_Node_Type
{
//可以走的地方
Walk,
//障碍物
Stop,
}
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;
///
/// 构造函数,传入格子的坐标和类型
///
///
///
///
public AStarNode(int x,int y,E_Node_Type type)
{
this.x = x;
this.y = y;
this.type = type;
}
}
**TestAStar:**用来演示A*寻路的效果
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestAStar : MonoBehaviour
{using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestAStar : MonoBehaviour
{
//实例化cube的起始点位置
public int beginX = -3;
public int beginY = -3;
//每个cube的偏移量
public int offsetX = 2;
public int offsetY = 2;
//地图的大小
public int mapW = 5;
public int mapH = 5;
//结点的开始位置和结束位置
private Vector2 beginPos = Vector2.right * -1;
private Vector2 endPos = Vector2.right * -1;
//存放最短路径
List<AStarNode> list = new List<AStarNode>();
//存放cube
private Dictionary<string, GameObject> cubes = new Dictionary<string, GameObject>();
// Start is called before the first frame update
void Start()
{
//初始化地图信息
AstarManager.Instance.InitMapInfo(mapW, mapH);
//创建cube
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;
//将cube存放到字典中
cubes.Add(obj.name, obj);
//遍历结点列表,把是障碍物的结点变为红色
AStarNode node = AstarManager.Instance.nodes[i, j];
if (node.type == NodeType.Stop)
{
obj.transform.GetComponent<MeshRenderer>().material.color = Color.red;
}
}
}
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0))
{
RaycastHit hitInfo;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if(Physics.Raycast(ray,out hitInfo, 100))
{
//将上一次的路径变回白色
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.color = Color.white;
}
}
//将点击的位置变成黄色
string[] strs = hitInfo.collider.gameObject.name.Split('_');
beginPos = new Vector2(int.Parse(strs[0]), int.Parse(strs[1]));
hitInfo.collider.gameObject.GetComponent<MeshRenderer>().material.color = Color.yellow;
}
else
{
string[] strs = hitInfo.collider.gameObject.name.Split('_');
endPos = new Vector2(int.Parse(strs[0]), int.Parse(strs[1]));
//从结点管理器中获取最短路径
list = AstarManager.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.color = Color.green;
}
}
//下次点击的时候更新起点位置
beginPos = Vector2.right * -1;
}
}
}
}
}