最近两天刚好有空研究了下游戏中的自动寻路功能,收获颇丰,感觉用相应的算法去解决相应的问题真的非常重要啊!至少比自己想的流水账逻辑流程管用。看来以后得花多点时间研究下算法方面的知识了。
游戏中的自动寻路,顾名思义就是找路,从地图找到从起点到终点的可行最短路径。既然是从地图找路,那么地图就应该是可数据化的,要不怎么找呢。
所以自动寻路的有两个重点是分别是地图数据化和搜索算法。
地图数据化,3D游戏地图有点复杂,还没研究,今天先看看2D游戏的。比如,搞一张九宫格式的地图,那么每个格子至少需要有这样的数据,格子的坐标、是路还是墙(根据具体情况格子还可以拓展其他属性)。然后有了这份数据化的地图后,寻路就变成了从一个格子移动到另一个格子的过程。
搜索算法,深度优先搜索(DFS)和广度优先搜索(BFS)算法,通常我们说DFS用栈方式实现,BFS用队列方式实现,为什么呢,下面我们来看看。
- 队列:起点P,将邻接点A、B、C、D存入队列,标记P为已查找,然后A出队,得邻接点A1、A2、A3、A4, 队列变成B、C、D、A1、A2、A3、A4,继续B出队得新队列C、D、A1、A2、A3、A4、B1、B2、B3、B4…。
栈:起点P,将邻接点A、B、C、D存入栈,标记P为已查找,然后D出栈,得邻接点D1、D2、D3、D4, 栈变成B、C、D1、D2、D3、D4,继续D4出栈得新栈B、C、D1、D2、D3、d1、d2、d3、d4…。
邻接点:不考虑斜角方向,有上下左右四个。粗糙理解讲下就是队列是从根节点开始,搜索所有子节点,然后搜索所有子节点的所有子节点;而栈是从根节点开始,搜索所有子节点,选择某个子节点搜索所有子节点,找不到则回溯父节点继续操作(参考二叉树辅助图理解)。其实从上面来看,无论是栈还是队列,对某个点都是只能访问一次的,访问后立即出栈或出队。如果没处理好,出现重复访问某个点的话,容易出现死循环。
运用到游戏中,那么起点就是二叉树的根节点,终点就是某个子节点,自动寻路就是找到从根节点到子节点的一条连接线。根节点每连接一个子节点时就记录下长度,最后根据长度就可以求出最短路径了。
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.UI;
public class CPathFind : MonoBehaviour {
// 自定义地图,1可行,0障碍
private const int Row = 8, Col = 9;
private int[,] MapDataArr = new int[Row, Col]
{
{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 0, 1, 0, 1, 1, 1},
{1, 1, 1, 1, 1, 0, 0, 1, 1},
{1, 1, 1, 0, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 1, 0, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 1, 1, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1},
};
// 每个地图点数据化
private class CMapPoint
{
public int x;
public int y;
public int abstacle;
public bool isFind = false;
public int moveDis = 0;
public CMapPoint fatherPoint;
public CMapPoint(int x, int y, CMapPoint fp = null)
{
this.x = x;
this.y = y;
fatherPoint = fp;
}
}
public GameObject wBlockPrefab;
public GameObject bBlockPrefab;
public GameObject playerPrefab;
public GameObject blockGroupObj;
private bool isMove = false;
private int leastDis = 10000;
private int findCount = 0;
private CMapPoint endTargetPoint = new CMapPoint(0, 0);
private CMapPoint moveTargetPoint = new CMapPoint(0, 0);
private CMapPoint playerPoint = new CMapPoint(0, 0);
private List<CMapPoint> MapPointList = new List<CMapPoint>();
private List<CMapPoint> MovePointList = new List<CMapPoint>();
private List<CMapPoint> OpenList = new List<CMapPoint>();
private List<CMapPoint> CloseList = new List<CMapPoint>();
void Start () {
CreateMap();
}
void Update () {
PlayerMove();
}
void OnDestroy()
{
for (int i = 0; i < blockGroupObj.transform.childCount; i++)
{
GameObject.Destroy(blockGroupObj.transform.GetChild(i).gameObject);
}
}
void CreateMap()
{
for (int r = 0; r < MapDataArr.GetLength(0); r++)
{
for (int c = 0; c < MapDataArr.GetLength(1); c++)
{
CMapPoint mp = new CMapPoint(c, r) {
abstacle = MapDataArr[r, c]
};
MapPointList.Add(mp);
GameObject blockPrefab = null;
if (MapDataArr[r, c] == 1)
blockPrefab = wBlockPrefab;
else
blockPrefab = bBlockPrefab;
GameObject blockObj = GameObject.Instantiate(blockPrefab, Vector3.zero, Quaternion.identity) as GameObject;
blockObj.name = r + "," + c;
blockObj.transform.SetParent(blockGroupObj.transform);
blockObj.transform.localScale = Vector3.one;
// 左下角为原点
blockObj.transform.localPosition = new Vector3(c * 60, r * 60, 0);
blockObj.SetActive(true);
blockObj.GetComponent<Button>().onClick.AddListener(delegate() { OnClickMapPoint(blockObj.name); });
}
}
}
void OnClickMapPoint(string name)
{
string[] nameArr = name.Split(',');
int row = int.Parse(nameArr[0]);
int col = int.Parse(nameArr[1]);
Debug.Log("click point x=" +col + ", y=" + row);
if (IsPass(col, row))
{
PathFinding(col, row);
}
else
{
Debug.Log("障碍点,无法通过!");
}
}
void PlayerMove()
{
Vector3 targetPos = new Vector3(moveTargetPoint.x * 60, moveTargetPoint.y * 60, 0);
float dValue = Vector3.Distance(playerPrefab.transform.localPosition, targetPos);
if (dValue < 0.1)
{
playerPoint = new CMapPoint(moveTargetPoint.x, moveTargetPoint.y);
GetNextMovePoint();
}
isMove = (dValue < 0.1) ? false : true;
if (isMove)
{
playerPrefab.transform.localPosition = Vector3.MoveTowards(playerPrefab.transform.localPosition, targetPos, 2);
Vector3 targetDir = (targetPos - playerPrefab.transform.localPosition).normalized;
// 方向有变化,由Y轴指向目标方向
if (targetDir != Vector3.zero)
{
playerPrefab.transform.up = targetDir;
}
}
}
void PathFinding(int x, int y)
{
leastDis = 10000;
findCount = 0;
endTargetPoint.x = x;
endTargetPoint.y = y;
endTargetPoint.fatherPoint = null;
endTargetPoint.moveDis = 10000;
MovePointList.Clear();
//ShortestPathDFS();
ShortestPathBFS();
}
void ShortestPathDFS()
{
// 传参数moveTargetPoint表示先移动完当前一格
FindPathByDFS(moveTargetPoint, 0);
GetLeastPath();
Debug.Log("DFS Find Count: " + findCount);
}
// 使用深度优先搜索(利用堆栈)找出所有可能路线,求出最短路线
void FindPathByDFS(CMapPoint curPoint, int dis)
{
int x = curPoint.x, y = curPoint.y;
// (dis < leastDis)加这个条件可以优化计算量
if (IsPass(x, y) && !GetIsFindStatus(curPoint) && dis < leastDis)
{
findCount += 1;
//Debug.Log("find point x=" + x + ", y=" + y);
if (x == endTargetPoint.x && y == endTargetPoint.y)
{
if (dis < leastDis)
{
leastDis = dis;
endTargetPoint.fatherPoint = curPoint.fatherPoint;
//Debug.Log("end point x="+x + ", y="+y);
}
return;
}
// 记录已经查找过的点,避免重复查找导致堆栈溢出
SetIsFindStatus(curPoint, true);
// 利用递归方式搜索
FindPathByDFS(new CMapPoint(x + 1, y, curPoint), dis + 1);
FindPathByDFS(new CMapPoint(x - 1, y, curPoint), dis + 1);
FindPathByDFS(new CMapPoint(x, y + 1, curPoint), dis + 1);
FindPathByDFS(new CMapPoint(x, y - 1, curPoint), dis + 1);
SetIsFindStatus(curPoint, false);
}
}
// 根据终点倒序求出路线所有移动点
void GetLeastPath()
{
CMapPoint mp = endTargetPoint;
while (mp.fatherPoint != null)
{
MovePointList.Add(mp);
mp = mp.fatherPoint;
}
MovePointList.Reverse();
}
//-------------------------------------------------------------//
void ShortestPathBFS()
{
FindPathByBFS();
GetLeastPath();
Debug.Log("BFS Find Count: " + findCount);
}
// 使用广度优先搜索(利用队列),每次从队列取一个点出来,找出其可访问邻接点,直至找到终点
void FindPathByBFS()
{
OpenList.Clear();
CloseList.Clear();
ClearMoveDisOfPoint();
OpenList.Add(moveTargetPoint);
CMapPoint mp = null;
while(OpenList.Count > 0)
{
mp = OpenList[0];
OpenList.RemoveAt(0);
CloseList.Add(mp);
FindAroundPoint(mp);
mp = null;
}
}
// 从访问点找出其可移动邻接点,记录从起点到该点的最短距离,存储该点到队列
void FindAroundPoint(CMapPoint fatherPoint)
{
int x = fatherPoint.x, y = fatherPoint.y;
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 2; j++)
{
int x1 = 0, y1 = 0;
if (i == 0)
{
x1 = x + (2 * j - 1);
y1 = y;
}
else
{
x1 = x;
y1 = y + (2 * j - 1);
}
findCount += 1;
//Debug.Log(findCount+ " find point x=" + x1 + ", y=" + y1);
CMapPoint temp = new CMapPoint(x1, y1, fatherPoint);
if (IsPass(x1, y1) && !InOpenList(temp) && !InCloseList(temp))
{
//Debug.Log(findCount + " find point x=" + x1 + ", y=" + y1);
// 记录和起点之间的距离
temp.moveDis = fatherPoint.moveDis + 1;
if (x1 == endTargetPoint.x && y1 == endTargetPoint.y && (temp.moveDis < leastDis))
{
endTargetPoint.fatherPoint = temp.fatherPoint;
leastDis = temp.moveDis;
continue;
}
OpenList.Add(temp);
}
}
}
}
// 清空所有格子记录的已移动距离
void ClearMoveDisOfPoint()
{
foreach (var p in MapPointList)
{
p.moveDis = 0;
}
}
// 设置搜索到某个格子时已移动距离
void SetMoveDisOfPoint(int x, int y, int dis)
{
foreach (var p in MapPointList)
{
if (p.x == x && p.y == y)
{
p.moveDis = dis;
}
}
}
bool InOpenList(CMapPoint mp)
{
foreach (var p in OpenList)
{
if (p.x == mp.x && p.y == mp.y)
{
return true;
}
}
return false;
}
bool InCloseList(CMapPoint mp)
{
foreach (var p in CloseList)
{
if (p.x == mp.x && p.y == mp.y)
{
return true;
}
}
return false;
}
//-------------------------------------------------------------//
// 获取当前格子查找状态
bool GetIsFindStatus(CMapPoint mp)
{
foreach (var p in MapPointList)
{
if (p.x == mp.x && p.y == mp.y)
{
return p.isFind;
}
}
return true;
}
// 设置当前格子是否已查找过了
void SetIsFindStatus(CMapPoint mp, bool status)
{
foreach(var p in MapPointList)
{
if (p.x == mp.x && p.y == mp.y)
{
p.isFind = status;
}
}
}
// 判断该点是否可通过(非障碍点且在地图内)
bool IsPass(int x, int y)
{
if (0 <= x && x < Col && 0 <= y && y < Row)
{
foreach (var p in MapPointList)
{
if (p.x == x && p.y == y)
{
return (p.abstacle == 1);
}
}
}
return false;
}
// 曼哈顿距离
int ManhattanDistance(int x1, int y1, int x2, int y2)
{
return (Mathf.Abs(x1 - x2) + Mathf.Abs(y1 - y2));
}
// 获取下一个移动点(每次移动一格)
void GetNextMovePoint()
{
if (MovePointList.Count > 0)
{
// 不能直接赋值,注意对象的引用关系
moveTargetPoint = new CMapPoint(MovePointList[0].x, MovePointList[0].y);
MovePointList.RemoveAt(0);
//Debug.Log("next move point x=" + moveTargetPoint.x + ", y=" + moveTargetPoint.y);
}
}
}