A*寻路算法已经在众多游戏开发中所涉及,目前开发的项目是基于U3D引擎制作的2D像素类型端游,地形在运行之后由脚本创建,其中不同类型的遮挡层级关系依靠生成的不同z轴高度区分,地形整体呈现斜面,地形上的一切位置信息都要先将空间坐标转成格子坐标。故而于地形上的所有移动物体的运动都要通过A*算法实现。如果对A*算法了解不深,可以参考一只探路猫来先汲取一下精华https://www.cnblogs.com/zhoug2020/p/3468167.html
A*算法无非就是用来搜索两点之间最短有效路径。一般来讲我们都是讲地形分割成很多小格子。首尾两点格子信息获取到之后,在走的每一步上即需要进行判断,怎么走,走哪里才是最有效的路径。没走一个最有效的格子,当到达目标点以后,所有的格子串联,既是我们的最短路径。简单总结要点如下:
关键词:
G:从出发点到当前点的已经走过的路程(几个方块)(左下)
H:当前点到目标点的估计路程(几个方块)(右下)
F:F=G+H 每次都走F最小的下一个位置(左上)重点
Closed列表:用来存放不被考虑的路径(路障和当前位置坐标)
Open列表:用来存放当前位置可以被考虑的路径
过程:
1、地形分割:根据地形的大小和障碍物分割成二维数组个小方格 10*10~若干个像素点
2、把初始位置放进Closed列表中,之后查找周围可被考虑的位置,找到后把所有点放进Open列表,计算每一个位置的G/H/F
3、找到当前位置周围最小F值的方块,移动到当前方块,把当前位置放进Closed列表中,之后再查找周围可以被考虑的位置,放进Open列表,计算每一个位置的G/H/F值
4、重复1、2的过程,直到到达目的地,生成路径
H:当前点到目标点的估计路程(几个方块)(右下)
F:F=G+H 每次都走F最小的下一个位置(左上)重点
Closed列表:用来存放不被考虑的路径(路障和当前位置坐标)
Open列表:用来存放当前位置可以被考虑的路径
过程:
1、地形分割:根据地形的大小和障碍物分割成二维数组个小方格 10*10~若干个像素点
2、把初始位置放进Closed列表中,之后查找周围可被考虑的位置,找到后把所有点放进Open列表,计算每一个位置的G/H/F
3、找到当前位置周围最小F值的方块,移动到当前方块,把当前位置放进Closed列表中,之后再查找周围可以被考虑的位置,放进Open列表,计算每一个位置的G/H/F值
4、重复1、2的过程,直到到达目的地,生成路径
说到这里,不再啰嗦,直接上两个脚本:
/// <summary>
/// 这个class是寻路用的 上面的每个单位
/// </summary>
public class Grid
{
public MapGridType MapGridType;
public int x;
public int y;
//格子A*三属性f-g-h f价值 g是 到其实点的代价 H是终点的代价
public int gCost;
// 与目标点的长度
public int hCost;
// 总的路径长度
public int fCost
{
get { return gCost + hCost; }
}
//格子类型
public bool iscanWalk = true;
//格子的归属(父格子)
public Grid parent;
//构造赋值
public Grid(int x, int y, bool iscanWalk,MapGridType mapGridType)
{
this.x = x;
this.y = y;
this.iscanWalk = iscanWalk;
MapGridType = mapGridType;
/// 这个class是寻路用的 上面的每个单位
/// </summary>
public class Grid
{
public MapGridType MapGridType;
public int x;
public int y;
//格子A*三属性f-g-h f价值 g是 到其实点的代价 H是终点的代价
public int gCost;
// 与目标点的长度
public int hCost;
// 总的路径长度
public int fCost
{
get { return gCost + hCost; }
}
//格子类型
public bool iscanWalk = true;
//格子的归属(父格子)
public Grid parent;
//构造赋值
public Grid(int x, int y, bool iscanWalk,MapGridType mapGridType)
{
this.x = x;
this.y = y;
this.iscanWalk = iscanWalk;
MapGridType = mapGridType;
}
}
/// <summary>
/// 由于继承Mono,故挂到一个游戏物体即可
/// </summary>
/// 由于继承Mono,故挂到一个游戏物体即可
/// </summary>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Game.Map
{
public class FindPath : MonoBehaviour
{
public static FindPath mIntance;
private MapGrids mapGrids;
public List<Grid> path;
void Start()
{
mIntance = this;
mapGrids = GetComponent<MapGrids>();
}
public void FindingPath(Vector3 startpos, Vector3 endpos)
{
Grid startgrid = mapGrids.getItem(startpos);
Grid endgrid = mapGrids.getItem(endpos);
if (!endgrid.iscanWalk)
{
path = new List<Grid>();
return;
}
List<Grid> openSet = new List<Grid>();
HashSet<Grid> closeSet = new HashSet<Grid>();
openSet.Add(startgrid);
using System.Collections.Generic;
using UnityEngine;
namespace Game.Map
{
public class FindPath : MonoBehaviour
{
public static FindPath mIntance;
private MapGrids mapGrids;
public List<Grid> path;
void Start()
{
mIntance = this;
mapGrids = GetComponent<MapGrids>();
}
public void FindingPath(Vector3 startpos, Vector3 endpos)
{
Grid startgrid = mapGrids.getItem(startpos);
Grid endgrid = mapGrids.getItem(endpos);
if (!endgrid.iscanWalk)
{
path = new List<Grid>();
return;
}
List<Grid> openSet = new List<Grid>();
HashSet<Grid> closeSet = new HashSet<Grid>();
openSet.Add(startgrid);
while (openSet.Count > 0)
{
Grid curNode = openSet[0];
{
Grid curNode = openSet[0];
for (int i = 0, max = openSet.Count; i < max; i++)
{
if (openSet[i].fCost <= curNode.fCost &&
openSet[i].hCost < curNode.hCost)
{
curNode = openSet[i];
}
}
{
if (openSet[i].fCost <= curNode.fCost &&
openSet[i].hCost < curNode.hCost)
{
curNode = openSet[i];
}
}
openSet.Remove(curNode);
closeSet.Add(curNode);
closeSet.Add(curNode);
// 找到的目标节点
if (curNode == endgrid)
{
generatePath(startgrid, endgrid);
if (curNode == endgrid)
{
generatePath(startgrid, endgrid);
return;
}
}
// 判断周围节点,选择一个最优的节点
foreach (var item in mapGrids.getNeibourhood(curNode))
{
// 如果是墙或者已经在关闭列表中
if (!item.iscanWalk || closeSet.Contains(item))
continue;
// 计算当前相领节点现开始节点距离
int newCost = curNode.gCost + getDistanceNodes(curNode, item);
// 如果距离更小,或者原来不在开始列表中
if (newCost < item.gCost || !openSet.Contains(item))
{
// 更新与开始节点的距离
item.gCost = newCost;
// 更新与终点的距离
item.hCost = getDistanceNodes(item, endgrid);
// 更新父节点为当前选定的节点
item.parent = curNode;
// 如果节点是新加入的,将它加入打开列表中
if (!openSet.Contains(item))
{
openSet.Add(item);
}
}
}
}
generatePath(startgrid,null);
}
public void generatePath(Grid startNode, Grid endNode)
{
path = new List<Grid>();
if (endNode != null)
{
Grid temp = endNode;
while (temp != startNode)
{
path.Add(temp);
temp = temp.parent;
}
// 反转路径
path.Reverse();
}
// 更新路径
}
int getDistanceNodes(Grid a, Grid b)
{
int cntX = Mathf.Abs(a.x - b.x);
int cntY = Mathf.Abs(a.y - b.y);
// 判断到底是那个轴相差的距离更远
if (cntX > cntY)
{
return 14 * cntY + 10 * (cntX - cntY);
}
else
{
return 14 * cntX + 10 * (cntY - cntX);
}
}
foreach (var item in mapGrids.getNeibourhood(curNode))
{
// 如果是墙或者已经在关闭列表中
if (!item.iscanWalk || closeSet.Contains(item))
continue;
// 计算当前相领节点现开始节点距离
int newCost = curNode.gCost + getDistanceNodes(curNode, item);
// 如果距离更小,或者原来不在开始列表中
if (newCost < item.gCost || !openSet.Contains(item))
{
// 更新与开始节点的距离
item.gCost = newCost;
// 更新与终点的距离
item.hCost = getDistanceNodes(item, endgrid);
// 更新父节点为当前选定的节点
item.parent = curNode;
// 如果节点是新加入的,将它加入打开列表中
if (!openSet.Contains(item))
{
openSet.Add(item);
}
}
}
}
generatePath(startgrid,null);
}
public void generatePath(Grid startNode, Grid endNode)
{
path = new List<Grid>();
if (endNode != null)
{
Grid temp = endNode;
while (temp != startNode)
{
path.Add(temp);
temp = temp.parent;
}
// 反转路径
path.Reverse();
}
// 更新路径
}
int getDistanceNodes(Grid a, Grid b)
{
int cntX = Mathf.Abs(a.x - b.x);
int cntY = Mathf.Abs(a.y - b.y);
// 判断到底是那个轴相差的距离更远
if (cntX > cntY)
{
return 14 * cntY + 10 * (cntX - cntY);
}
else
{
return 14 * cntX + 10 * (cntY - cntX);
}
}
}
}
}
需特殊说明的是,项目中于MapGrids脚本做了有关空间坐标转换格子坐标的方法,时间有限就不做展示,后续会跟进。我们在此通过主角来进行A*方法调用。
1、首先在Update每帧执行的方法获取结束点:TargetPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
2
、在外界如果满足人物移动条件,调用下面的
CharacterWalk(TargetPos)此方法同样在Update函数中执行;
3、定义
CharacterWalk方法,参数为Vector3类型目标点;
使用iTween动画实现2D人物行走:
/// <summary>
/// 人物行走
/// </summary>
/// <param name="targetpos"></param>
public void CharacterWalk(Vector3 targetpos)
{
Game.Map.FindPath.mIntance.FindingPath(transform.position, targetpos);
if (mCharacterState == CharacterState.Walk)
{
if (gameObject.GetComponent<iTween>() != null)
Destroy(gameObject.GetComponent<iTween>());
}
CharacterWalkList(Game.Map.FindPath.mIntance.path);
}
/// 人物行走
/// </summary>
/// <param name="targetpos"></param>
public void CharacterWalk(Vector3 targetpos)
{
Game.Map.FindPath.mIntance.FindingPath(transform.position, targetpos);
if (mCharacterState == CharacterState.Walk)
{
if (gameObject.GetComponent<iTween>() != null)
Destroy(gameObject.GetComponent<iTween>());
}
CharacterWalkList(Game.Map.FindPath.mIntance.path);
}
private Vector3[] movePosArr;
private List<GameObject> PathPointArr;//存路径点 运用对象池显示小黑块代表路径
private int MoveIndex;
GameObject go=null;
private List<GameObject> PathPointArr;//存路径点 运用对象池显示小黑块代表路径
private int MoveIndex;
GameObject go=null;
/// <summary>
/// 人物行走动画
/// </summary>
/// <param name="gridlist"></param>
/// <returns></returns>
public void CharacterWalkList(List<Game.Map.Grid> gridlist)
{
if (PathPointArr == null)
{
PathPointArr = new List<GameObject>();
}
/// 人物行走动画
/// </summary>
/// <param name="gridlist"></param>
/// <returns></returns>
public void CharacterWalkList(List<Game.Map.Grid> gridlist)
{
if (PathPointArr == null)
{
PathPointArr = new List<GameObject>();
}
isNearbyTarget = false;
if (gridlist.Count > 1)
{
if (PathPointArr.Count > 0)
{
for (int i = 0; i < PathPointArr.Count; i++)
{
//隐藏路径点
poolManager.Instance.HideObjet(PathPointArr[i]);
if (gridlist.Count > 1)
{
if (PathPointArr.Count > 0)
{
for (int i = 0; i < PathPointArr.Count; i++)
{
//隐藏路径点
poolManager.Instance.HideObjet(PathPointArr[i]);
}
}
PathPointArr.Clear();
MoveIndex = 0;
movePosArr = new Vector3[gridlist.Count];
int r = 0;
foreach (var grid in gridlist)
{
float dt = grid.y * 0.1f - 10.05f;
Vector3 pos = new Vector3(grid.x * 0.48f + 0.24f, grid.y * 0.48f + 0.24f, dt);
movePosArr[r++] = pos;
//显示路径点
go= poolManager.Instance.GetObject("point",null);
PathPointArr.Add(go);
go.transform.position = pos;
go.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
}
PathPointArr.Clear();
MoveIndex = 0;
movePosArr = new Vector3[gridlist.Count];
int r = 0;
foreach (var grid in gridlist)
{
float dt = grid.y * 0.1f - 10.05f;
Vector3 pos = new Vector3(grid.x * 0.48f + 0.24f, grid.y * 0.48f + 0.24f, dt);
movePosArr[r++] = pos;
//显示路径点
go= poolManager.Instance.GetObject("point",null);
PathPointArr.Add(go);
go.transform.position = pos;
go.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
}
StartMove();
}
}
private void StartMove()
{
{
CharacterOrientation er = ChangeOrientation(movePosArr[MoveIndex]);
ChangeOrientation(ChangeOrientation(movePosArr[MoveIndex]));
float duration = Vector3.Distance(transform.position, movePosArr[MoveIndex]) * 0.1f;
Hashtable args = new Hashtable();
args.Add("easeType", iTween.EaseType.linear);
args.Add("time", duration);
args.Add("position", movePosArr[MoveIndex]);
args.Add("oncomplete", "MoveOver");//回调 在此做判断,是否到目标点
args.Add("speed",speed);//每一个格子的时间
iTween.MoveTo(gameObject, args);
}
private void MoveOver()//每走一个格子,进回调来判断是否到达目标点,如果没有,就继续执行StartMove
{
PathPointArr[MoveIndex].SetActive(false);
//PersonPointPool._instance.InitPool(PathPointArr[MoveIndex]);
MoveIndex += 1;
if (MoveIndex < movePosArr.Length)
{
StartMove();
}
else//到达目标点了
{
PathPointArr.Clear();
Debug.Log("移动结束");
mCharacterState = CharacterState.Idel;
switch (mcharacterOrientation)
{
case CharacterOrientation.BackLeft:
case CharacterOrientation.BackRight:
if (isHold)
CharacterObj.GetComponent<Animator>().Play("Back_HoldIdel");
else
CharacterObj.GetComponent<Animator>().Play("Back_Idel");
break;
case CharacterOrientation.FrontLeft:
case CharacterOrientation.FrontRight:
if (isHold)
CharacterObj.GetComponent<Animator>().Play("Front_HoldIdel");
else
CharacterObj.GetComponent<Animator>().Play("Front_Idel");
break;
}
isNearbyTarget = true;
}
}
{
PathPointArr[MoveIndex].SetActive(false);
//PersonPointPool._instance.InitPool(PathPointArr[MoveIndex]);
MoveIndex += 1;
if (MoveIndex < movePosArr.Length)
{
StartMove();
}
else//到达目标点了
{
PathPointArr.Clear();
Debug.Log("移动结束");
mCharacterState = CharacterState.Idel;
switch (mcharacterOrientation)
{
case CharacterOrientation.BackLeft:
case CharacterOrientation.BackRight:
if (isHold)
CharacterObj.GetComponent<Animator>().Play("Back_HoldIdel");
else
CharacterObj.GetComponent<Animator>().Play("Back_Idel");
break;
case CharacterOrientation.FrontLeft:
case CharacterOrientation.FrontRight:
if (isHold)
CharacterObj.GetComponent<Animator>().Play("Front_HoldIdel");
else
CharacterObj.GetComponent<Animator>().Play("Front_Idel");
break;
}
isNearbyTarget = true;
}
}
至此项目中即完整的通过A*来实现主角的移动。