UnityA星寻路算法获取最短路径
~最后效果
1. 场景的搭建
2. 说明
A星寻路公式 F = G + H
F: 寻路消耗(越小说明距离终点越近)
G: 起点距离当前点的代价
H: 当前点距离终点的代价
脚本 | 说明 |
---|---|
Singleton | 继承该脚本实现单例 |
AStarNode | 存储每个节点的信息 |
AStarManager | 所有节点的管理器 |
Test | 使用节点管理器实现可视化的寻路流程 |
3. Singleton脚本
继承该脚本实现单例
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Singleton<T> where T : class
{
private static T instance;
public static T Instance
{
get
{
if(instance == null)
{
// 通过反射创建实例
instance = (T)Activator.CreateInstance(typeof(T), true);
}
return instance;
}
}
}
4. AStarNode脚本
存储每个节点的信息
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 节点的类型
public enum AStarNodeType
{
Walk, // 可以走的
NotWalk, // 障碍物
Start, // 起点
End, // 终点
}
public class AStarNode
{
// 节点坐标
public int x;
public int y;
// 寻路消耗
public float f;
// 离起点的代价
public float g;
// 离终点的代价
public float h;
// 父对象
public AStarNode father;
// 类型
public AStarNodeType type;
// 构造函数
public AStarNode(int x, int y, AStarNodeType type)
{
this.x = x;
this.y = y;
this.type = type;
}
}
5. AStarManager 脚本
所有节点的管理器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AStarManager : Singleton<AStarManager>
{
private AStarManager()
{
// 实例化开始列表和关闭列表
openList = new List<AStarNode>();
closeList = new List<AStarNode>();
}
// 地图宽高
private int w;
private int h;
// 存储所有节点的二维数组
public AStarNode[,] nodes;
// 开始列表
private List<AStarNode> openList;
// 关闭列表
private List<AStarNode> closeList;
// 初始化地图
public void InitMap(int w, int h)
{
// 获取地图宽高
this.w = w;
this.h = h;
// 实例化数组
nodes = new AStarNode[w, h];
// 遍历存储所有节点,并有百分之20几率是障碍物类型的节点
for (int i = 0; i < w; i++)
{
for(int j = 0; j < h; j++)
{
nodes[i, j] = new AStarNode(i, j, Random.Range(0, 100) < 20 ? AStarNodeType.NotWalk : AStarNodeType.Walk);
}
}
int x = 0;
int y = 0;
bool isDone = false; // 判断起点或者终点有没有生成完成;
// 生成起点,如果没有完成生成
while (!isDone)
{
// 随机生成x,y;
x = Random.Range(0, w);
y = Random.Range(0, h);
// 如果不是障碍物类型,就将这个节点变为起点
if (nodes[x, y].type != AStarNodeType.NotWalk)
{
nodes[x, y].type = AStarNodeType.Start;
// 生成起点完成,结束循环
isDone = true;
}
}
// 生成终点
isDone = false;
while(!isDone)
{
x = Random.Range(0, w);
y = Random.Range(0, h);
// 如果不是障碍物也不是起点, 那么这个节点就可以成为终点
if(nodes[x,y].type != AStarNodeType.NotWalk && nodes[x, y].type != AStarNodeType.Start)
{
nodes[x, y].type = AStarNodeType.End;
// 生成终点完成,结束循环
isDone = true;
}
}
}
public List<AStarNode> FindPath(Vector2 startPos, Vector2 endPos)
{
// 判断起点和终点有没有在地图范围内
if (startPos.x < 0 || startPos.y < 0 || startPos.x >= w || startPos.y >= h ||
endPos.x < 0 || endPos.y < 0 || endPos.x >= w || endPos.y >= h)
{
Debug.Log("起点或终点超出地图范围");
return null;
}
// 获取起点和终点
AStarNode start = nodes[(int)startPos.x, (int)startPos.y];
AStarNode end = nodes[(int)endPos.x, (int)endPos.y];
// 判断起点和终点是不是障碍物方块
if(start.type == AStarNodeType.NotWalk || end.type == AStarNodeType.NotWalk)
{
Debug.Log("起点或终点是障碍物");
return null;
}
// 清空上一轮的开始和关闭列表
openList.Clear();
closeList.Clear();
// 起点的值确保为初始化状态
start.f = 0;
start.g = 0;
start.h = 0;
start.father = null;
// 将起点添加到关闭列表
closeList.Add(start);
// 循环找附近的8个点,直到死路或者找到终点
while (true)
{
// 找到起点附近的8个点
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
float g = 1;
// 如果是自己,就跳过
if (i == 0 && j == 0) continue;
// 如果是斜着的节点, g 就是 1.4
if ((i < 0 ? -i : i) == (j < 0 ? -j : j))
{
g = 1.4f;
}
// 找到附近的节点并判断能不能添加到开始列表
FindNearPos(start.x + i, start.y + j, g, 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;
}
}
}
// 列表排序,找到寻路消耗最小的节点
private int SortOpenList(AStarNode a, AStarNode b)
{
if(a.f >= b.f)
{
return 1;
}
else
{
return -1;
}
}
private void FindNearPos(int x, int y, float g, AStarNode father, AStarNode end)
{
// 判断是不是在地图范围内
if (x < 0 || x >= w || y < 0 || y >= h) return;
// 获取该节点
AStarNode node = nodes[x, y];
// 如果在开始列表中有该节点
if (openList.Contains(node))
{
// 算出该节点距离起点的距离 g
float gCrt = father.g + g; // 父对象离起点的距离 + 我离父对象的距离 = 我离起点的距离
// 如果该节点距离起点的距离小于我离父对象的距离
if (gCrt < node.g)
{
// 那么该点的父节点要获取这个最小值点
node.g = gCrt;
// 算出寻路消耗
node.f = node.g + node.h;
// 父对象赋值
node.father = father;
return;
}
else
{
return;
}
}
// 如果节点
// 1. 不是为空
// 2. 不是障碍物
// 3. 不在开始列表中
// 4. 不在关闭列表中
if (node == null ||
node.type == AStarNodeType.NotWalk ||
closeList.Contains(node) ||
openList.Contains(node))
return;
// 父对象赋值
node.father = father;
node.g = father.g + g;
node.h = Mathf.Abs(end.x - node.x) + Mathf.Abs(end.y - node.y);
node.f = node.g + node.h;
openList.Add(node);
}
}
6. Test脚本
使用节点管理器实现可视化的寻路流程
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Test : MonoBehaviour
{
// 地图宽高
private int w;
private int h;
// 要生成的方块,作为A星格子
private GameObject cube;
// 存储方块的字典
private Dictionary<string, GameObject> dic;
private void Awake()
{
// 加载方法资源
cube = Resources.Load<GameObject>("Prefabs/Cube");
// 实例化字典
dic = new Dictionary<string, GameObject>();
}
private void Start()
{
// 绑定按钮点击事件
GameObject.Find("Start").GetComponent<Button>().onClick.AddListener(StartFindPath);
GameObject.Find("ReStart").GetComponent<Button>().onClick.AddListener(ReStart);
// 获取地图宽高
w = (int)transform.localScale.x * 10;
h = (int)transform.localScale.z * 10;
// 初始化
Init();
}
// 开始寻路
private void StartFindPath()
{
// 获取起点和终点的坐标
Vector2 startPos = Vector2.zero;
Vector2 endPos = Vector2.zero;
foreach (AStarNode item in AStarManager.Instance.nodes)
{
if(item.type == AStarNodeType.Start)
{
startPos = new Vector2(item.x, item.y);
}
if(item.type == AStarNodeType.End)
{
endPos = new Vector2(item.x, item.y);
}
}
// 获取寻路结果
List<AStarNode> list = AStarManager.Instance.FindPath(startPos, endPos);
// 寻路结果不等于null 说明找到路了
if (list != null)
{
// 遍历
foreach (AStarNode item in list)
{
// 如果是起点或者终点就跳过
if (item.type == AStarNodeType.Start || item.type == AStarNodeType.End) continue;
// 将路径变为绿色
dic[item.x + "_" + item.y].GetComponent<MeshRenderer>().material.color = Color.green;
}
}
}
private void ReStart()
{
// 清空字典
dic.Clear();
// 清空子对象
for(int i = 0; i < transform.childCount; i++)
{
Destroy(transform.GetChild(i).gameObject);
}
// 初始化
Init();
}
private void Init()
{
// 初始化地图
AStarManager.Instance.InitMap(w, h);
for (int i = 0; i < w; i++)
{
for (int j = 0; j < h; j++)
{
// 生成方块
GameObject go = Instantiate(cube, transform);
// 修改方块的位置(铺满整个地板)
go.transform.localPosition = new Vector3(i - w * 0.5f + 0.5f, 0.5f, j - h * 0.5f + 0.5f);
// 更改方块的名字,为了获取的方便
go.name = i + "_" + j;
// 将方块添加到字典
dic.Add(go.name, go);
// 获取节点
AStarNode node = AStarManager.Instance.nodes[i, j];
// 判断节点的类型
switch (node.type)
{
// 如果是障碍物就变成红色
case AStarNodeType.NotWalk:
go.GetComponent<MeshRenderer>().material.color = Color.red;
break;
// 如果是起点就变成黄色
case AStarNodeType.Start:
go.GetComponent<MeshRenderer>().material.color = Color.yellow;
break;
// 如果是终点就变成蓝色
case AStarNodeType.End:
go.GetComponent<MeshRenderer>().material.color = Color.blue;
break;
}
}
}
}
}