- 前言
- 一,效果展示
- 二,场景搭建
- 三,代码逻辑
- 四,完善场景
- 五,使用小结
一,效果展示还在看别人的寻路逻辑?保姆级教程,一步步教你实现网格寻路逻辑。 超级详细的代码注释,图文步骤详解。写文不易,有帮助的话三连支持下吧~
二,场景搭建
以一个 9 * 9 的地图为例:
-
新建工程,设置屏幕分辨率为: [1080 * 1920],如下图:
-
在自带场景下创建Image作为背景(在Hierarchy右键 --> UI --> Image); 修改其名字为”GridManager“,坐标为: [0, 0, 0],大小为:[1000 * 1000] 并将颜色设置为黑色,完成后效果如下图:
-
在"GridManager"下面创建一个空物体,命名为"Grid",将其锚点设置为铺满,并为其添加组件Grid Layout Group,完成后效果如下图:
-
在"Grid"下面添加一个Image,并为其添加Button组件,作为点击格子使用,然后Ctrl + D 复制80个,实现效果如下:
-
调整"Grid"的Grid Layout Group组件属性值,Left,Top,SpacingX,Y 均设置为10,意思为左边距为10,上边距为10,物体间隔为10,实现后效果如下:
现在这样就已经模拟搭建出类似棋盘场景了,下面看下代码改如何是实现的吧。
三,代码逻辑
- 寻路二维数组的物体:
////// 移动方向 /// public enum Direction { up, down, left, right } public class RoutingObject : MonoBehaviour { ////// x坐标 /// public int x; ////// y坐标 /// public int y; ////// 目标距离 /// public int targetDistance; ////// 移动距离 /// public int moveDistance; ////// A*和值(目标距离+移动距离) /// public int moveSum; ////// 是否可以移动 /// public bool isCanMove; ////// 移动方向 /// public Direction direction; }
- 寻路逻辑:根据传入参数(起始点,结束点,地图),进行查找可移动路线,最后筛选出最短路线。部分代码如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; ////// 寻路 /// public class Routing { #region 单例 Routing() { } static Routing instance; public static Routing Instance { get { if (instance == null) { instance = new Routing(); } return instance; } } #endregion ////// 二维数组的地图 /// RoutingObject[,] map; ////// 存储被考虑来寻找最短路径的点 /// Listopen = new List (); /// /// 存储不再被考虑寻找最短路径的点 /// Listclosed = new List (); /// /// 存储路线点的列表 /// Listroute = new List (); /// /// 初始化 /// void Init(RoutingObject[,] mapArray) { open.Clear(); closed.Clear(); route.Clear(); map = mapArray; } ////// 判断从起始点是否能到达目标点 /// /// 起始点x坐标 /// 起始点y坐标 /// 目标点x坐标 /// 目标点y坐标 /// ///public bool IsRouting(RoutingObject start, RoutingObject end, RoutingObject[,] mapArray) { Init(mapArray); Explore(start, end, start); // 判断存储路线点的列表里是否存有点 return route.Count > 0; } /// /// 探索中心点上下左右四个方向点 /// void Explore(RoutingObject center, RoutingObject end, RoutingObject start) { // 中心点不再考虑寻找路径 closed.Add(center); // 将中心点从寻找列表中移除 if (open.Contains(center)) { open.Remove(center); } // 是否找到目标点 if (IsGetEnd(end)) { // 找到目标点 ReturnRoute(end, start); } else { // 判断中心点上边的点 if (center.y - 1 >= 0) { RoutingObject up = map[center.x, center.y - 1]; GetMoveSumByDirection(up, center, end, Direction.up); } // 判断中心点下边的点 if (center.y + 1 < GridManager.Instance.mapColumnCount) { RoutingObject down = map[center.x, center.y + 1]; GetMoveSumByDirection(down, center, end, Direction.down); } // 判断中心点左边的点 if (center.x - 1 >= 0) { RoutingObject left = map[center.x - 1, center.y]; GetMoveSumByDirection(left, center, end, Direction.left); } // 判断中心点右边的点 if (center.x + 1 < GridManager.Instance.mapRowCount) { RoutingObject right = map[center.x + 1, center.y]; GetMoveSumByDirection(right, center, end, Direction.right); } if (open.Count > 0) { // 没有找到目标点,则在被考虑的列表中找出一个和值最小的 RoutingObject ro = GetMinimumMoveSum(); Explore(ro, end, start); } else { Debug.Log("没有找到目标点"); } } } ////// 根据传进来的方向去获取和值 /// /// /// /// /// void GetMoveSumByDirection(RoutingObject center, RoutingObject start, RoutingObject end, Direction direction) { // 判断这个点是否能移动或者是否被考虑 if (IsForward(center)) { center.direction = direction; // 获取移动距离 center.moveDistance = GetDistance(center, start); // 获取目标距离 center.targetDistance = GetDistance(center, end); // 获取A*和值 center.moveSum = center.moveDistance + center.targetDistance; // 将中心点加入将要被考虑的列表中 open.Add(center); } else { //Debug.Log(center.name + " 不能移动"); } } ////// 判断这个点是否属于未来被考虑前进的点 /// /// ///bool IsForward(RoutingObject ro) { // 判断这个点是否已经在不再考虑的列表中 if (closed.Contains(ro) || open.Contains(ro)) { return false; } else { // 判断这个点是否可以移动 if (ro.isCanMove) { return true; } else { // 不可以移动就加入不再考虑的列表中 closed.Add(ro); return false; } } } /// /// 获取距离 /// /// /// int GetDistance(RoutingObject start, RoutingObject end) { // 定义目标距离返回值, --> 谁大,谁减谁 return Mathf.Abs(start.x - end.x) + Mathf.Abs(start.y - end.y); } ////// 是否找到目标点 /// ///bool IsGetEnd(RoutingObject end) { return closed.Contains(end); } /// /// 在被考虑的列表中获取和值最小的点 /// ///RoutingObject GetMinimumMoveSum() { RoutingObject ro = null; RoutingObject temporary = new RoutingObject(); for (int i = 0; i < open.Count; i++) { //Debug.Log("当前 " + open[i].name + " 的和值为: " + open[i].moveSum); // 列表中的第一个不需要比较,直接赋值 if (i == 0) { ro = open[i]; temporary = open[i]; } else { // 寻找列表中和值最小的点 if (open[i].moveSum < temporary.moveSum) { ro = open[i]; temporary = open[i]; } } } //Debug.Log("最终 " + ro.name + " 的和值为: " + ro.moveSum); return ro; } /// /// 返回路线 /// /// /// void ReturnRoute(RoutingObject center, RoutingObject start) { // 将这个点存储到路线列表中 route.Add(center); // 判断路线列表中是否包含起始点 if (!route.Contains(start)) { // 没有包含 // 返回路线取这个点的反方向 switch (center.direction) { case Direction.up: ReturnRoute(map[center.x, center.y + 1], start); break; case Direction.down: ReturnRoute(map[center.x, center.y - 1], start); break; case Direction.left: ReturnRoute(map[center.x + 1, center.y], start); break; case Direction.right: ReturnRoute(map[center.x - 1, center.y], start); break; } } else { RouteSort(start); } } ////// 路线排序(将起始点从存储路线点的列表中移除,并从起始点到目标点重新排序) /// void RouteSort(RoutingObject start) { Listlist = new List (route); route.Clear(); for (int i = list.Count - 1; i >= 0; i--) { if (list[i] != start) { route.Add(list[i]); } } } /// /// 返回最短路线 /// ///public List GetRoute() { return route; } }
- 创建GridManager脚本,将其挂载到GridManager物体上,
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GridManager : MonoBehaviour
{
///
/// 单例
///
public static GridManager Instance;
///
/// 地图行数
///
public int mapColumnCount = 9;
///
/// 地图列数
///
public int mapRowCount = 9;
///
/// 当前可移动最短路径存储集合
///
public List routeList = new List();
///
/// 已被占领的格子集合 -- 格子上有障碍物
///
public List OccupyGridList = new List();
///
/// 存储地图格子
///
private GameObject[,] GridArray;
///
/// 当前所选格子
///
private GameObject selectGrid;
private void Awake()
{
Instance = this;
GridArray = new GameObject[mapRowCount, mapColumnCount];
}
void Start()
{
}
///
/// 每个格子初始化时 调用赋值
///
/// 格子
/// 所在X
/// 所在Y
public void SetGridArray(GameObject go, int x, int y)
{
GridArray[x, y] = go;
}
///
/// 根据(x,y)获取 格子物体
///
///
///
///
GameObject GetGridArray(int x, int y)
{
return GridArray[x, y];
}
///
/// 获取一个准备移动球的二维数组(每个坐标点上记录着是否可以移动)
///
///
Grid[,] GetMoveMap()
{
// 定义存储地图格子是否可以移动的二维数组
Grid[,] array = new Grid[mapRowCount, mapColumnCount];
for (int i = 0; i < mapRowCount; i++)
{
for (int j = 0; j < mapColumnCount; j++)
{
if (OccupyGridList.Contains(GridArray[i, j]))
{
GridArray[i, j].GetComponent().isCanMove = false;
}
else
{
GridArray[i, j].GetComponent().isCanMove = true;
}
array[i, j] = GridArray[i, j].GetComponent();
}
}
return array;
}
///
/// 点击格子的调用
///
///
public void OnClickGrid(GameObject selectObj)
{
if (selectGrid == null)
{
selectGrid = selectObj;
}
else
{
// 获取当前地图(地图记录每个点是否能移动)
RoutingObject[,] map = GetMoveMap();
// 获取起始点
RoutingObject start = selectGrid.GetComponent();
RoutingObject end = selectObj.GetComponent();
// 判断是否可以通过
if (Routing.Instance.IsRouting(start, end, map))
{
Debug.Log("判断可以通过");
// 标识为起点
start.gameObject.GetComponent().color = Color.cyan;
// 最短路径 添加到管理器
routeList.AddRange(Routing.Instance.GetRoute());
MoveBall();
}
else
{
// TODO... 提示
Debug.LogError("不能移动到当前位置...");
}
selectGrid = null;
}
}
///
/// 执行移动逻辑
///
void MoveBall()
{
// 模拟移动
StartCoroutine(MoveGrid());
// todo... 按需修改实际逻辑
//for (int i = 0; i < routeList.Count; i++)
//{
// GameObject go = GetGridArray(routeList[i].x, routeList[i].y);
// go.GetComponent().color = Color.green;
//}
//
//routeList.Clear();
}
///
/// 模拟移动
///
///
IEnumerator MoveGrid()
{
for (int i = 0; i < routeList.Count; i++)
{
GameObject go = GetGridArray(routeList[i].x, routeList[i].y);
go.GetComponent().color = Color.green;
yield return new WaitForSeconds(0.2f);
}
routeList.Clear();
}
}
- 创建Grid 脚本名并将其继承RoutingObject;此脚本需挂载到场景中物体名为‘Grid’的81一个子物体上,主要逻辑:初始计算自身所在二维数组中的位置(左上角为0,0点),给自身添加点击监听。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Grid : RoutingObject
{
private void Start()
{
// 初始化 在二维数组中的位置
x = transform.GetSiblingIndex() / GridManager.Instance.mapColumnCount;
y = transform.GetSiblingIndex() % GridManager.Instance.mapRowCount;
// 添加到管理器中
GridManager.Instance.SetGridArray(gameObject, x, y);
// 添加按钮监听
GetComponent
四,完善场景
完成上面步骤就已经完成运行测试了,随便点击两个点警徽得到如下结果:
重复运行即可测试无障碍物的情况了,下面我们手动添加下障碍物。
思路:
- 添加Toggle,勾选Toggle,添加/删除障碍物;
- 添加Button,还原当前表格状态,方便测试时不用重新运行
在GridManager 脚本中添加如下代码:
////// 添加障碍物 /// public Toggle AddOccupyToggle; ////// 清空按钮 /// public Button ClearButton; void Start() { ClearButton.onClick.AddListener(ClearFun); } ////// 加入或移除障碍物 /// /// void AddOccupyGridList(GameObject go) { Debug.Log("加入或移除障碍物"); if (OccupyGridList.Contains(go)) { go.GetComponent().color = Color.white; OccupyGridList.Remove(go); } else { go.GetComponent ().color = Color.red; OccupyGridList.Add(go); } } void ClearFun() { Debug.Log("清空所有格子的状态..."); selectGrid = null; OccupyGridList.Clear(); for (int i = 0; i < mapRowCount; i++) { for (int j = 0; j < mapColumnCount; j++) { GameObject go = GridArray[i, j]; go.GetComponent ().color = Color.white; } } }
在OnClickGrid() 中添加点击格子触发逻辑
代码处理完成后,在Inspector面板给AddOccupyToggle 和 ClearButton 赋值如下图:
运行测试:
五,使用小结
经过上的一系列操作就完成了2D网格寻路逻辑,使用时你只需要拷贝RoutingObject和Routing两个代码即可。
使用方式:调用Routing.Instance.IsRouting(start, end, map)校验是否能通过,若能通过则通过Routing.Instance.GetRoute() 获取最短路径,提供个实际逻辑使用即可。
说好的:文末源码;没有积分的童鞋,可以关注下面 卡片公号,回复【2D寻路】获取完整源码吧~



