我们先来了解一下A*寻路的思路:
1.每个块需要一个数据类(PointTest.cs),这个类中要存的数据方法有:
1.每个移动方块都有三个值FGH,其中G (代表起点到当前点的距离)、H(当前点到终点的距离)F=G+H。
2.并且每个移动方块都会存储自己上一个块(Parent)
2.首先 创建一个开放列表(OpenList:存放已经找到并计算好FGH值的周围块)和一个关闭列表(closeList:不可移动的块,如起点、障碍物和已经走过的块)
3.将起点放入开放列表(openList)中,只要开放列表(openList)中存在可移动的点就执行While循环while(openList.Count>0)。其中while循环中要执行的方法如下:
3.1 我们先在开放列表(openList)中找到最小F值的块为当前块。
3.2将找到的最小F值的块放入到关闭列表(closeList)中。
3.3 找寻当前块周围的八个可移动块放入周围块列表(surroundPointsList)中
3.4 剔除周围块列表(surroundPointsList)中存在的 如:起点,障碍物,走过的块等不可移动的块,这样周围块列表(surroundPointsList)中就只剩下可以走的块了
3.5然后我们遍历这些可移动的周围块,每个周围块执行两个判断:
(1)当这个周围块不存在开放列表中时,计算这个块的FGH值,并且将父节点赋值成当前节点
(2)当这个周围块存在开放列表中时,我们计算新的G值(计算方式:当前点的G值+当前点到这个周围块的节点所消耗的G值)是否小于周围块的原G值,如果小于就要更新这个周围块的GF值(为什么不更新H值呢?因为H值是块到终点的距离是不会变的。所以不用修改),并且修改这个周围块的父节点为当前节点。
3.6 最后判断一下开放列表(openList)中是否存在终点这个块,如果存在就跳出While循环
4.就这样我们根据终点块的父节点,一级一级的往后倒推就找到了我们想要的最短路径。
看案例图之前先了解一下每个颜色块的作用吧
(1)浅蓝色代表在开放列表中的块(存放已经找到并计算好FGH值的周围块)
(2)橙色代表找到的最小F值块
(3)箭头的指向表示块的上一级父节点
(4) 蓝色表示关闭列表(closeList)中的点(其中绿色起点和红色终点也都在关闭列表中,为了区别就没有标成蓝色)
好的接下来我们就开始根据上面的思路对照来看图了解:
1.开放列表中只有一个起点,将起点放入到关闭列表中并获取起点周围的8个周围块(上下左右,左上 左下 右上 右下),由于一开始这8个周围块都不在关闭列表)中,所以计算他们的FGH值并对父节点赋值,然后放入到开放列表中。如下图
2.再次执行循环,先找到开放列表中的最小F值块(当前橙色块),找寻当前块周围的8个点,剔除不可移动的周围块,遍历剩下的可移动周围块点进行判断,根据上图我们可以找到橙色块,根据橙色当前块来找周围可移动的块共有四个,并且这四个块都在开放列表中了(都有自己的FGH值和父节点了),所以我们要进行一个计算,从当前块(橙色)到这个周围块所花费的G值加橙色块的原G值是否小于周围块原来G值,小于就代表从当前块(橙色)这条路比你上一次走的路要近,就要重新计算这个周围块的FG值,并且修改这个周围块的父节点。这里呢这四个周围块原路径都比走当前块(橙色)近,所以不做修改,执行下一次循环
3.上一步橙色点不是已经走过了嘛,所以这里我就把走过的橙色块改成蓝色代表已经走过的块不能再走了,这一步还是和上一次一样从开放列表中找到最小F值点,这时候你会发现有两个最小值F为4.576492的块这时候怎么选呢?其实我们一开搜寻周围块的时候是有顺序的(上下左右,左上 左下 右上 右下),我们按照这个顺序将块存入到列表,所以当遇到多个最小F值的块相谁最先在列表中就选择那个块为当前块,所以这里程序选的是以起点为中心的右上角的块为当前块(橙色块)。然后就是遍历周围块(注意这里我设置了周围有墙不可以贴墙斜着走方法),没有在开放列表中的就计算FGH值,设置父节点,放到开放列表中即可,在开放列表中时,计算新路径G值与原路径G值谁大谁小,小就修改FG值,并替换父节点,否则就啥也不用动。
4.好的接下来继续找最小点,对周围点进行逻辑判断
5.继续
6.继续
7.继续
8.继续
9.继续
10. 下一步
11.下一步
12.下一步
13.下一步
14.下一步
15.下一步
16.下一步
17.下一步
18.最后一步,可以看到周围块包含了结束点,这证明我们已经找到了最优路线
19.最后找到的路线
程序运行效果:
1.地图块脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
///
/// 地图块
///
public class PointTest : MonoBehaviour
{
public Text ftxt;
public Text gtxt;
public Text htxt;
//当A*计算出最近路径点时,用于记录上一节点
public PointTest Parent { get; set; }
public float F { get; set; }//总消耗 F=G+H
public float G { get; set; }//表示从起点移动到网格上指定方格的移动耗费 (可沿斜方向移动)
public float H { get; set; }//当前点到终点的消耗
//位置下标(具体位置要根据图片宽度进行计算)
public Vector2 PosIndex { get; set; }
//是否是障碍物
public bool IsWall { get; set; }
RectTransform m_rect;//记录自身Rect组件
public void InitData(Vector2 origin, Vector2 space, Vector2 posIndex)
{
Parent = null; //创建地图 上一路径默认为空
PosIndex = posIndex; //位置下标
//当前rect为空 赋值
if (m_rect == null)
{
m_rect = GetComponent();
ftxt = m_rect.Find("F").GetComponent();
gtxt = m_rect.Find("G").GetComponent();
htxt = m_rect.Find("H").GetComponent();
}
//根据原点 间距 图片宽高计算位置
m_rect.anchoredPosition = (posIndex * new Vector2(m_rect.sizeDelta.x, m_rect.sizeDelta.y)) + origin + PosIndex * space;
}
//修改图片颜色
public void SetColor(Color color)
{
GetComponent().color = color;
}
//更新父节点、g值计算总消耗F值
public void UpdateParent(PointTest parent, float g)
{
Parent = parent;
G = g;
F = G + H;
gtxt.text = G.ToString();
ftxt.text = F.ToString();
}
}
2.A*寻路脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class AStarTest : MonoBehaviour
{
[Header("颜色")]
public Color startColor = Color.green; //起始点颜色
public Color endColor = Color.red; //结束点颜色
public Color wallColor = Color.blue; //障碍物颜色
public Color pathColor = Color.gray; //路径颜色
public Color mapColor = Color.white; //地图块颜色
[Header("原点位置/间距")]
public Vector2 origin = Vector2.zero; //原点位置
public Vector2 space = Vector2.zero; //间距
[Header("起点/终点")]
public Vector2 start = Vector2.zero; //起点
public Vector2 end = Vector2.zero; //终点
[Header("障碍物")]
public List wallList; //障碍物列表
[Header("地图宽高")]
public Vector2 mapWidthAndHeight; //地图宽高
private PointTest[,] map; //地图块容器
//预设 容器
public PointTest prefab;
public RectTransform content;
PointTest startPoint; //起点
PointTest endPoint; //终点
List openList = new List(); //闭合地图块列表 不可移动 如:障碍物 起点/终点、已走过的路径
List closeList = new List(); //闭合地图块列表 不可移动 如:障碍物 起点/终点、已走过的路径
void Start()
{
//初始化地图块
InitMap();
//查找最近路径
FindPath(startPoint, endPoint);
//显示最优路线
ShowPath(startPoint, endPoint);
}
#region 初始化地图 设置障碍物 起点终点
PointTest obj;
int pointName = 0; //地图块名称
private void InitMap()
{
map = new PointTest[(int)mapWidthAndHeight.x, (int)mapWidthAndHeight.y];
for (int x = 0; x < (int)mapWidthAndHeight.x; x++)
{
for (int y = 0; y < (int)mapWidthAndHeight.y; y++)
{
//创建地图块并显示
obj = Instantiate(prefab, content).GetComponent();
obj.gameObject.SetActive(true);
//数据初始化
obj.InitData(origin, space, new Vector2(x, y));
//将地图块添加到地图列表
map[x, y] = obj;
pointName++;
obj.name = pointName.ToString();
}
}
//起点 终点初始化 并将其添加到闭合列表中
startPoint = map[(int)start.x, (int)start.y];
endPoint = map[(int)end.x, (int)end.y];
//障碍物创建并将其添加到闭合列表中
for (int i = 0; i < wallList.Count; i++)
{
PointTest point = map[(int)wallList[i].x, (int)wallList[i].y];
point.IsWall = true;
point.SetColor(wallColor);
//添加到闭合列表中
closeList.Add(point);
}
}
#endregion
#region 查找路径
private void FindPath(PointTest start, PointTest end)
{
openList.Add(start); //将开始路径添加到开放路径中
//循环开启列表中的块
while (openList.Count > 0)
{
PointTest point = FindMinFOfPoint(openList);//找到总消耗值F最小的路径块,作为当前块
//Debug.Log(point.name);
//清除当前块 放入闭合列表中
openList.Remove(point);
closeList.Add(point);
List surroundPointsList = GetSurroundPoints(point);//找寻围绕当前块四周可移动的地图块
PointsFilter(surroundPointsList, closeList);//从四周可移动块中过滤已经走过的路径块
//遍历四周点可移动点
foreach (PointTest surroundPoint in surroundPointsList)
{
//判断当前点在开放列表中时,要重新计算G值
if (openList.IndexOf(surroundPoint) > -1)
{
//重新计算G值
float nowG = CalculateG(surroundPoint, point);//计算两点的G值 两点距离
//如果当前G点距离小于原来G值
if (nowG < surroundPoint.G)
{
//更新父节点 G和F值
surroundPoint.UpdateParent(point, nowG);
}
}
else
{
//当前块没有在开放列表中时,初始化父块,FGH值 并将此块放入开放列表中
surroundPoint.Parent = point;
CalculateF(surroundPoint, end);
openList.Add(surroundPoint);
}
}
//判断一下是否到达了目标点
if (openList.IndexOf(end) > -1)
{
break;
}
}
}
#endregion
#region 1.查找开启列表中的最小F值块
private PointTest FindMinFOfPoint(List openList)
{
float f = float.MaxValue;
PointTest temp = null;
//遍历开放路径,判断f值
foreach (PointTest p in openList)
{
if (p.F < f)
{
temp = p;
f = p.F;
}
}
return temp;
}
#endregion
#region 2.获取环绕可移动路径点
private List GetSurroundPoints(PointTest point)
{
PointTest up = null, down = null, left = null, right = null;//上下左右块
PointTest lu = null, ld = null, ru = null, rd = null;//左上左下右上右下块
//取得上下左右的Point (因为使用二维数组,所以高度范围在0~mapWidthAndHeight.y-1,同理宽度为0~mapWidthAndHeight.x-1)
if (point.PosIndex.y < mapWidthAndHeight.y - 1)
{
up = map[(int)point.PosIndex.x, (int)point.PosIndex.y + 1];
}
if (point.PosIndex.y > 0)
{
down = map[(int)point.PosIndex.x, (int)point.PosIndex.y - 1];
}
if (point.PosIndex.x > 0)
{
left = map[(int)point.PosIndex.x - 1, (int)point.PosIndex.y];
}
if (point.PosIndex.x < mapWidthAndHeight.x - 1)
{
right = map[(int)point.PosIndex.x + 1, (int)point.PosIndex.y];
}
/*取得左上、左下、右上、右下的Point
* 左上:左边和上边不为空 就有左上角,其他同理
*/
if (left != null && up != null)
{
//以当前点为中间点,左上角在它(x-1,y+1)位置上
lu = map[(int)point.PosIndex.x - 1, (int)point.PosIndex.y + 1];
}
if (left != null && down != null)
{
//以当前点为中间点,左下角在它(x-1,y-1)位置上
ld = map[(int)point.PosIndex.x - 1, (int)point.PosIndex.y - 1];
}
if (right != null && up != null)
{
//以当前点为中间点,右上角在它(x+1,y+1)位置上
ru = map[(int)point.PosIndex.x + 1, (int)point.PosIndex.y + 1];
}
if (right != null && down != null)
{
//以当前点为中间点,右下角在它(x+1,y-1)位置上
rd = map[(int)point.PosIndex.x + 1, (int)point.PosIndex.y - 1];
}
//筛选可以移动的块(筛选障碍物、起点、终点)
List pointList = new List();
//周围的Point(上下左右)如果不是墙就可以走
if (up != null && up.IsWall == false)
{
pointList.Add(up);
}
if (down != null && down.IsWall == false)
{
pointList.Add(down);
}
if (left != null && left.IsWall == false)
{
pointList.Add(left);
}
if (right != null && right.IsWall == false)
{
pointList.Add(right);
}
//两边也不是墙才可以走(斜对角)
if (lu != null && lu.IsWall == false && left.IsWall == false && up.IsWall == false)
{
pointList.Add(lu);
}
if (ld != null && ld.IsWall == false && left.IsWall == false && down.IsWall == false)
{
pointList.Add(ld);
}
if (ru != null && ru.IsWall == false && right.IsWall == false && up.IsWall == false)
{
pointList.Add(ru);
}
if (rd != null && rd.IsWall == false && right.IsWall == false && down.IsWall == false)
{
pointList.Add(rd);
}
return pointList;
}
#endregion
#region 3.从四周可移动块中过滤已经走过的路径块
///
/// 从四周可移动块中过滤已经走过的路径块
///
/// 四周可移动的地图块
/// 闭合路径块
private void PointsFilter(List src, List closePoint)
{
//遍历闭合列表
foreach (PointTest p in closePoint)
{
/*判断是否找到
* IndexOf(T):表示从列表0位置匹配到最后,直到找到第一个重复值并返回他的下标 没找到会返回-1
* IndexOf(T, Int32):从第n个下标开始到最后
* IndexOf(T, Int32,Int32):从第n个下标开始到m个下标结束的范围查找,如果只想查询一个可以是IndexOf(T, 1,1)从1~1的范围即只查找下标为1的数
*/
if (src.IndexOf(p) > -1)
{
src.Remove(p);
}
}
}
#endregion
#region 4.计算G的值 起点到当前点的消耗
private float CalculateG(PointTest now, PointTest parent)
{
return (Vector2.Distance(new Vector2(now.PosIndex.x, now.PosIndex.y), new Vector2(parent.PosIndex.x, parent.PosIndex.y)) + parent.G);
}
#endregion
#region 5.计算F的值
///
/// 计算F的值
///
/// 当前位置
/// 目标位置
private void CalculateF(PointTest now, PointTest end)
{
//当前位置到 终点位置
//float h = Mathf.Abs(end.PosIndex.x - now.PosIndex.x) + Mathf.Abs(end.PosIndex.y - now.PosIndex.y);//绝对值
float h = (Vector2.Distance(new Vector2(now.PosIndex.x, now.PosIndex.y), new Vector2(end.PosIndex.x, end.PosIndex.y)));
//当前位置到上一点位置
float g = Vector2.Distance(new Vector2(now.PosIndex.x, now.PosIndex.y), new Vector2(now.Parent.PosIndex.x, now.Parent.PosIndex.y)) + now.Parent.G;
float f = g + h;
now.F = f;
now.G = g;
now.H = h;
now.ftxt.text = f.ToString();
now.gtxt.text = g.ToString();
now.htxt.text = h.ToString();
}
#endregion
#region 显示路径坐标
private void ShowPath(PointTest start, PointTest end)
{
PointTest temp = end;
while (true)
{
Color color = pathColor;
if (temp == start)
{
color = startColor;
}
if (temp == end)
{
color = endColor;
}
temp.SetColor(color);
if (temp.Parent == null)
break;
temp = temp.Parent;
}
}
#endregion
}