首先,A*寻路算法完全不是很难,可以说就是深度遍历算法的一个升级版本
深度便利算法是什么?就是从起点开始,判断每一个可以走的位置,然后再将每个得到的可以走的位置作为起点,继续往后走,直到找到终点为止
就像把水倒在桌子上,每滴水都会向着周围的每个方向四散开来,直到有水滴触碰到想要接触的东西,这个水滴经过的路径就是寻路的结果
而A*算法,就是在此基础之上,每次判断出可以走的位置之后,不去判断每个新得到的位置后面的路径,而是判断最有可能以最短路径达到终点的路径
这就是A*算法,对比深度遍历只是单纯的新增了一个对于新增路径的筛选,不是每次遍历所有新增路径,而是每次判断距离最优的路径
那么这个用来筛选路径点的最优值要怎么得到呢?非常简单,就是走过的距离加上终点预估距离
走过的距离顾名思义,就是从起点到当前位置走过了多少距离
终点预估距离有很多种算法,比较常用的就是直线距离和xyz轴的差值的和
float GetEndDistance(Vector3 start,Vector3 end)
{
//第一种
return Vector3.Distance(start, end);
//第二种
return Mathf.Abs(start.x - end.x) + Mathf.Abs(start.y - end.y) + Mathf.Abs(start.z - end.z);
}
虽然第一种写法更短,但是其实内部逻辑要更复杂,所以推荐第二种写法
还有要说的一点是,A*算法必须要有一个方法,来获取指定一个路径点周围所有路径点,这个方法就要自己想办法写出来了
所以A*寻路算法的流程就是:
1.将起点定义为点A
2.寻找点A周围相邻的所有点ListB
3.遍历ListB,其中每个具体点为B
4.记录从点A走到这个B的路径,以及走过的距离以及预估的剩余距离,直到遍历结束
5.寻找走过距离和预估距离合计最小的点视为最有可能是最优路径的点,视为点A
6.寻找点A周围相邻的所有点ListB(同2)
7.遍历ListB,其中每个具体点为B(同3)
8.判断点B是否为终点,如果是终点就结束,寻找最优路径,如果不是终点就继续
9.记录从点A走到这个B的路径,以及走过的距离以及预估的剩余距离,直到遍历结束(同4)
10.寻找走过距离和预估距离合计最小的点视为最有可能是最优路径的点,视为点A(同5)
(随后5,6,7,8,9无限循环,直到通过8找到终点结束循环为止)....
下面就是涉及到代码的部分:
cs
public interface IPathData
{
public Vector3 WorldPos { get; set; }
public IPathData[] GetNearPath();
}
先介绍一下IPathData,这个接口需要每个路径点继承,继承接口的脚本要实现两个功能:
WorldPos:字段,用来保存这个路径的坐标点
GetNearPath:方法,用来获取和这个路径点相邻的所有路径点
首先定义一个代表路径点的类,保存每个路径点相关的属性:
cs
public class PathNode
{
public IPathData pathData;
public PathNode parent;
public float moveNow;
public float targetDistance;
public float value => moveNow + targetDistance;
public PathNode(IPathData pathData,PathNode parent, float moveNow, float targetDistance)
{
this.pathData = pathData;
this.parent = parent;
this.moveNow = moveNow;
this.targetDistance = targetDistance;
}
}
pathData:路径脚本
parent:表示来到这个点的上一个点,如果这个值为null就证明这个点是起点,通过这个值找路径
moveNow:从起点到当前点走过的距离
targetDistance:距离终点的预期距离
value:走过的距离和预期距离的综合,根据这个值判断是不是最优路径点
在寻路主主逻辑之前,先讲一下主逻辑用到的两个工具方法
cs
public List<IPathData> GetParentPathList(PathNode endNode,IPathData endCell)
{
List<IPathData> cellDatas = new List<IPathData>();
cellDatas.Add(endCell);
while (endNode.parent != null)
{
cellDatas.Add(endNode.parent.pathData);
endNode = endNode.parent;
}
cellDatas.Reverse();
return cellDatas;
}
float GetEndDistance(Vector3 start,Vector3 end)
{
return Mathf.Abs(start.x - end.x) + Mathf.Abs(start.y - end.y) + Mathf.Abs(start.z - end.z);
}
GetParentPathList方法:
第一个参数为最后找到的最邻近终点的节点
第二个参数为终点的路径脚本
返回值为从起点到终点的全部路径点
先加入终点,然后根据最后一个节点,不断往前找,直到找到起点为止,然后反转,就变成从起点开始,逐步找到终点
GetEndDistance方法:
判断两个点的预期距离,上面提过,用的是对比xyz的差值的方式
寻路主逻辑:
cs
public List<IPathData> FindPath(IPathData startPath,IPathData endPath, HashSet<IPathData> allCell)
{
PathNode bestNode;
IPathData[] nearCell;
float moveNow;
Dictionary<IPathData,PathNode> openList = new Dictionary<IPathData,PathNode>();
HashSet<IPathData> closeHesh = new HashSet<IPathData>();
float distance = GetEndDistance(startPath.WorldPos, endPath.WorldPos);
openList.Add(startPath,new PathNode(startPath,null,0,distance));
while (openList.Count > 0)
{
bestNode = null;
foreach (var node in openList)
{
if (bestNode == null) { bestNode = node.Value; }
if (node.Value.value < bestNode.value)
{
bestNode = node.Value;
}
}
openList.Remove(bestNode.pathData);
closeHesh.Add(bestNode.pathData);
if (bestNode.pathData == endPath)
{
return GetParentPathList(bestNode, endPath);
}
nearCell = bestNode.pathData.GetNearPath();
if(nearCell == null){continue;}
foreach (IPathData cellData in nearCell)
{
if(cellData == null){continue;}
if(closeHesh.Contains(cellData)){continue;}
if(!allCell.Contains(cellData)){continue;}
moveNow = bestNode.moveNow + GetEndDistance(bestNode.pathData.WorldPos, cellData.WorldPos);
if (openList.ContainsKey(cellData))
{
if (openList[cellData].moveNow > moveNow)
{
PathNode node = openList[cellData];
node.moveNow = moveNow;
node.parent = bestNode;
}
}
else
{
openList[cellData] = new PathNode(cellData, bestNode, moveNow,
GetEndDistance(cellData.WorldPos, endPath.WorldPos));
}
}
}
return FindPath(startPath, endPath);
}
其实一共就50行左右,逻辑和上面的思路也差不多类似
参数一为起点的路径脚本
参数二为终点的路径脚本
参数三为所有可以通行的点,不在这个参数里面的点不可以通行
方法里面分为openList和closeList
openList代表当前可能是最优路径的路径点
closeList是已经计算过周围点不再进入计算的路径点
在计算周围路径点的时候,如果出现更短的距离到达某个点的时候,就更新这个路径点的到达最短距离
方法的最后一句是return FindPath(startCell, endCell);
这是另一个方法,和这个方法基本相同,唯一的区别就是不再判断路径点是否可移动,默认所有点都可移动,避免没有可用路径会卡死的情况,属于备用方案
cs
public List<IPathData> FindPath(IPathData startCell,IPathData endCell)
{
PathNode bestNode;
IPathData[] nearCell;
float moveNow;
Dictionary<IPathData,PathNode> openList = new Dictionary<IPathData,PathNode>();
HashSet<IPathData> closeHesh = new HashSet<IPathData>();
float distance = GetEndDistance(startCell.WorldPos, endCell.WorldPos);
openList.Add(startCell,new PathNode(startCell,null,0,distance));
while (openList.Count > 0)
{
bestNode = null;
foreach (var node in openList)
{
if (bestNode == null) { bestNode = node.Value; }
if (node.Value.value < bestNode.value)
{
bestNode = node.Value;
}
}
openList.Remove(bestNode.pathData);
closeHesh.Add(bestNode.pathData);
if (bestNode.pathData == endCell)
{
return GetParentPathList(bestNode, endCell);
}
nearCell = bestNode.pathData.GetNearPath();
if(nearCell == null){continue;}
foreach (IPathData cellData in nearCell)
{
if(cellData == null){continue;}
if(closeHesh.Contains(cellData)){continue;}
moveNow = bestNode.moveNow + GetEndDistance(bestNode.pathData.WorldPos, cellData.WorldPos);
if (openList.ContainsKey(cellData))
{
if (openList[cellData].moveNow > moveNow)
{
PathNode node = openList[cellData];
node.moveNow = moveNow;
node.parent = bestNode;
}
}
else
{
openList[cellData] = new PathNode(cellData, bestNode, moveNow,
GetEndDistance(cellData.WorldPos, endCell.WorldPos));
}
}
}
Debug.LogError("寻路系统未找到路径,请检查代码");
return null;
}
如果不存在无法移动的点的情况下,寻路还是失败,证明CellDate当中就无法从起点一直到终点,这个时候就需要检查CellData保存周围点的代码了
最后附上全部的寻路脚本的代码:
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IPathData
{
public Vector3 WorldPos { get; set; }
public IPathData[] GetNearPath();
}
public class PathNode
{
public IPathData pathData;
public PathNode parent;
public float moveNow;
public float targetDistance;
public float value => moveNow + targetDistance;
public PathNode(IPathData pathData,PathNode parent, float moveNow, float targetDistance)
{
this.pathData = pathData;
this.parent = parent;
this.moveNow = moveNow;
this.targetDistance = targetDistance;
}
}
public class FindPathManager : MonoBehaviour
{
private static FindPathManager instance;
public static FindPathManager Instance
{
get
{
if (instance == null)
{
instance = new GameObject("FindPathManager").AddComponent<FindPathManager>();
DontDestroyOnLoad(instance.gameObject);
}
return instance;
}
}
public List<IPathData> FindPath(IPathData startPath,IPathData endPath, HashSet<IPathData> allCell)
{
PathNode bestNode;
IPathData[] nearCell;
float moveNow;
Dictionary<IPathData,PathNode> openList = new Dictionary<IPathData,PathNode>();
HashSet<IPathData> closeHesh = new HashSet<IPathData>();
float distance = GetEndDistance(startPath.WorldPos, endPath.WorldPos);
openList.Add(startPath,new PathNode(startPath,null,0,distance));
while (openList.Count > 0)
{
bestNode = null;
foreach (var node in openList)
{
if (bestNode == null) { bestNode = node.Value; }
if (node.Value.value < bestNode.value)
{
bestNode = node.Value;
}
}
openList.Remove(bestNode.pathData);
closeHesh.Add(bestNode.pathData);
if (bestNode.pathData == endPath)
{
return GetParentPathList(bestNode, endPath);
}
nearCell = bestNode.pathData.GetNearPath();
if(nearCell == null){continue;}
foreach (IPathData cellData in nearCell)
{
if(cellData == null){continue;}
if(closeHesh.Contains(cellData)){continue;}
if(!allCell.Contains(cellData)){continue;}
moveNow = bestNode.moveNow + GetEndDistance(bestNode.pathData.WorldPos, cellData.WorldPos);
if (openList.ContainsKey(cellData))
{
if (openList[cellData].moveNow > moveNow)
{
PathNode node = openList[cellData];
node.moveNow = moveNow;
node.parent = bestNode;
}
}
else
{
openList[cellData] = new PathNode(cellData, bestNode, moveNow,
GetEndDistance(cellData.WorldPos, endPath.WorldPos));
}
}
}
return FindPath(startPath, endPath);
}
public List<IPathData> FindPath(IPathData startCell,IPathData endCell)
{
PathNode bestNode;
IPathData[] nearCell;
float moveNow;
Dictionary<IPathData,PathNode> openList = new Dictionary<IPathData,PathNode>();
HashSet<IPathData> closeHesh = new HashSet<IPathData>();
float distance = GetEndDistance(startCell.WorldPos, endCell.WorldPos);
openList.Add(startCell,new PathNode(startCell,null,0,distance));
while (openList.Count > 0)
{
bestNode = null;
foreach (var node in openList)
{
if (bestNode == null) { bestNode = node.Value; }
if (node.Value.value < bestNode.value)
{
bestNode = node.Value;
}
}
openList.Remove(bestNode.pathData);
closeHesh.Add(bestNode.pathData);
if (bestNode.pathData == endCell)
{
return GetParentPathList(bestNode, endCell);
}
nearCell = bestNode.pathData.GetNearPath();
if(nearCell == null){continue;}
foreach (IPathData cellData in nearCell)
{
if(cellData == null){continue;}
if(closeHesh.Contains(cellData)){continue;}
moveNow = bestNode.moveNow + GetEndDistance(bestNode.pathData.WorldPos, cellData.WorldPos);
if (openList.ContainsKey(cellData))
{
if (openList[cellData].moveNow > moveNow)
{
PathNode node = openList[cellData];
node.moveNow = moveNow;
node.parent = bestNode;
}
}
else
{
openList[cellData] = new PathNode(cellData, bestNode, moveNow,
GetEndDistance(cellData.WorldPos, endCell.WorldPos));
}
}
}
Debug.LogError("寻路系统未找到路径,请检查代码");
return null;
}
public List<IPathData> GetParentPathList(PathNode endNode,IPathData endCell)
{
List<IPathData> cellDatas = new List<IPathData>();
cellDatas.Add(endCell);
while (endNode.parent != null)
{
cellDatas.Add(endNode.parent.pathData);
endNode = endNode.parent;
}
cellDatas.Reverse();
return cellDatas;
}
float GetEndDistance(Vector3 start,Vector3 end)
{
return Mathf.Abs(start.x - end.x) + Mathf.Abs(start.y - end.y) + Mathf.Abs(start.z - end.z);
}
}