Unity 之 手把手教你实现自己Unity2D游戏寻路逻辑 【文末源码】

  • 前言
  • 一,效果展示
  • 二,场景搭建
  • 三,代码逻辑
  • 四,完善场景
  • 五,使用小结

前言

还在看别人的寻路逻辑?保姆级教程,一步步教你实现网格寻路逻辑。 超级详细的代码注释,图文步骤详解。写文不易,有帮助的话三连支持下吧~

一,效果展示


二,场景搭建

以一个 9 * 9 的地图为例:

  1. 新建工程,设置屏幕分辨率为: [1080 * 1920],如下图:

  2. 在自带场景下创建Image作为背景(在Hierarchy右键 --> UI --> Image); 修改其名字为”GridManager“,坐标为: [0, 0, 0],大小为:[1000 * 1000] 并将颜色设置为黑色,完成后效果如下图:

  3. 在"GridManager"下面创建一个空物体,命名为"Grid",将其锚点设置为铺满,并为其添加组件Grid Layout Group,完成后效果如下图:

  4. 在"Grid"下面添加一个Image,并为其添加Button组件,作为点击格子使用,然后Ctrl + D 复制80个,实现效果如下:

  5. 调整"Grid"的Grid Layout Group组件属性值,Left,Top,SpacingX,Y 均设置为10,意思为左边距为10,上边距为10,物体间隔为10,实现后效果如下:

现在这样就已经模拟搭建出类似棋盘场景了,下面看下代码改如何是实现的吧。


三,代码逻辑

  • 寻路二维数组的物体:
/// <summary>
/// 移动方向
/// </summary>
public enum Direction
{up, down, left, right
}public class RoutingObject : MonoBehaviour
{/// <summary>/// x坐标/// </summary>public int x;/// <summary>/// y坐标/// </summary>public int y;/// <summary>/// 目标距离/// </summary>public int targetDistance;/// <summary>/// 移动距离/// </summary>public int moveDistance;/// <summary>/// A*和值(目标距离+移动距离)/// </summary>public int moveSum;/// <summary>/// 是否可以移动/// </summary>public bool isCanMove;/// <summary>/// 移动方向/// </summary>public Direction direction;
}
  • 寻路逻辑:根据传入参数(起始点,结束点,地图),进行查找可移动路线,最后筛选出最短路线。部分代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;/// <summary>
/// 寻路
/// </summary>
public class Routing
{#region 单例Routing() { }static Routing instance;public static Routing Instance{get{if (instance == null){instance = new Routing();}return instance;}}#endregion/// <summary>/// 二维数组的地图/// </summary>RoutingObject[,] map;/// <summary>/// 存储被考虑来寻找最短路径的点/// </summary>List<RoutingObject> open = new List<RoutingObject>();/// <summary>/// 存储不再被考虑寻找最短路径的点/// </summary>List<RoutingObject> closed = new List<RoutingObject>();/// <summary>/// 存储路线点的列表/// </summary>List<RoutingObject> route = new List<RoutingObject>();/// <summary>/// 初始化/// </summary>void Init(RoutingObject[,] mapArray){open.Clear();closed.Clear();route.Clear();map = mapArray;}/// <summary>/// 判断从起始点是否能到达目标点/// </summary>/// <param name="start_x">起始点x坐标</param>/// <param name="start_y">起始点y坐标</param>/// <param name="end_x">目标点x坐标</param>/// <param name="end_y">目标点y坐标</param>/// <param name="map"></param>/// <returns></returns>public bool IsRouting(RoutingObject start, RoutingObject end, RoutingObject[,] mapArray){Init(mapArray);Explore(start, end, start);// 判断存储路线点的列表里是否存有点return route.Count > 0;}/// <summary>/// 探索中心点上下左右四个方向点/// </summary>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("没有找到目标点");}}}/// <summary>/// 根据传进来的方向去获取和值/// </summary>/// <param name="center"></param>/// <param name="start"></param>/// <param name="end"></param>/// <param name="direction"></param>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 + " 不能移动");}}/// <summary>/// 判断这个点是否属于未来被考虑前进的点/// </summary>/// <param name="ro"></param>/// <returns></returns>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;}}}/// <summary>/// 获取距离/// </summary>/// <param name="start"></param>/// <param name="end"></param>int GetDistance(RoutingObject start, RoutingObject end){// 定义目标距离返回值, --> 谁大,谁减谁return Mathf.Abs(start.x - end.x) + Mathf.Abs(start.y - end.y);}/// <summary>/// 是否找到目标点/// </summary>/// <returns></returns>bool IsGetEnd(RoutingObject end){return closed.Contains(end);}/// <summary>/// 在被考虑的列表中获取和值最小的点/// </summary>/// <returns></returns>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;}/// <summary>/// 返回路线/// </summary>/// <param name="center"></param>/// <param name="start"></param>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);}}/// <summary>/// 路线排序(将起始点从存储路线点的列表中移除,并从起始点到目标点重新排序)/// </summary>void RouteSort(RoutingObject start){List<RoutingObject> list = new List<RoutingObject>(route);route.Clear();for (int i = list.Count - 1; i >= 0; i--){if (list[i] != start){route.Add(list[i]);}}}/// <summary>/// 返回最短路线/// </summary>/// <returns></returns>public List<RoutingObject> GetRoute(){return route;}
}
  • 创建GridManager脚本,将其挂载到GridManager物体上,
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class GridManager : MonoBehaviour
{/// <summary>/// 单例/// </summary>public static GridManager Instance;/// <summary>/// 地图行数/// </summary>public int mapColumnCount = 9;/// <summary>/// 地图列数/// </summary>public int mapRowCount = 9;/// <summary>/// 当前可移动最短路径存储集合/// </summary>public List<RoutingObject> routeList = new List<RoutingObject>();/// <summary>/// 已被占领的格子集合 -- 格子上有障碍物/// </summary>public List<GameObject> OccupyGridList = new List<GameObject>();/// <summary>/// 存储地图格子/// </summary>private GameObject[,] GridArray;/// <summary>/// 当前所选格子/// </summary>private GameObject selectGrid;private void Awake(){Instance = this;GridArray = new GameObject[mapRowCount, mapColumnCount];}void Start(){}/// <summary>/// 每个格子初始化时 调用赋值/// </summary>/// <param name="go">格子</param>/// <param name="x">所在X</param>/// <param name="y">所在Y</param>public void SetGridArray(GameObject go, int x, int y){GridArray[x, y] = go;}/// <summary>/// 根据(x,y)获取 格子物体/// </summary>/// <param name="x"></param>/// <param name="y"></param>/// <returns></returns>GameObject GetGridArray(int x, int y){return GridArray[x, y];}/// <summary>/// 获取一个准备移动球的二维数组(每个坐标点上记录着是否可以移动)/// </summary>/// <returns></returns>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<Grid>().isCanMove = false;}else{GridArray[i, j].GetComponent<Grid>().isCanMove = true;}array[i, j] = GridArray[i, j].GetComponent<Grid>();}}return array;}/// <summary>/// 点击格子的调用/// </summary>/// <param name="selectObj"></param>public void OnClickGrid(GameObject selectObj){if (selectGrid == null){selectGrid = selectObj;}else{// 获取当前地图(地图记录每个点是否能移动)RoutingObject[,] map = GetMoveMap();// 获取起始点RoutingObject start = selectGrid.GetComponent<RoutingObject>();RoutingObject end = selectObj.GetComponent<RoutingObject>();// 判断是否可以通过if (Routing.Instance.IsRouting(start, end, map)){Debug.Log("判断可以通过");// 标识为起点start.gameObject.GetComponent<Image>().color = Color.cyan;// 最短路径 添加到管理器routeList.AddRange(Routing.Instance.GetRoute());MoveBall();}else{// TODO... 提示Debug.LogError("不能移动到当前位置...");}selectGrid = null;}}/// <summary>/// 执行移动逻辑/// </summary>void MoveBall(){// 模拟移动StartCoroutine(MoveGrid());// todo... 按需修改实际逻辑//for (int i = 0; i < routeList.Count; i++)//{//    GameObject go = GetGridArray(routeList[i].x, routeList[i].y);//    go.GetComponent<Image>().color = Color.green;//}////routeList.Clear();}/// <summary>/// 模拟移动/// </summary>/// <returns></returns>IEnumerator MoveGrid(){for (int i = 0; i < routeList.Count; i++){GameObject go = GetGridArray(routeList[i].x, routeList[i].y);go.GetComponent<Image>().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<Button>().onClick.AddListener(OnClick);}void OnClick(){Debug.Log("点击格子:    " + x + "," + y);GridManager.Instance.OnClickGrid(gameObject);}}

四,完善场景

完成上面步骤就已经完成运行测试了,随便点击两个点警徽得到如下结果:

重复运行即可测试无障碍物的情况了,下面我们手动添加下障碍物。

思路:

  • 添加Toggle,勾选Toggle,添加/删除障碍物;
  • 添加Button,还原当前表格状态,方便测试时不用重新运行

GridManager 脚本中添加如下代码:

/// <summary>
/// 添加障碍物
/// </summary>
public Toggle AddOccupyToggle;/// <summary>
/// 清空按钮
/// </summary>
public Button ClearButton;void Start()
{ClearButton.onClick.AddListener(ClearFun);
}/// <summary>
/// 加入或移除障碍物
/// </summary>
/// <param name="go"></param>
void AddOccupyGridList(GameObject go)
{Debug.Log("加入或移除障碍物");if (OccupyGridList.Contains(go)){go.GetComponent<Image>().color = Color.white;OccupyGridList.Remove(go);}else{go.GetComponent<Image>().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<Image>().color = Color.white;}}
}

OnClickGrid() 中添加点击格子触发逻辑

代码处理完成后,在Inspector面板给AddOccupyToggle 和 ClearButton 赋值如下图:

运行测试:


五,使用小结

经过上的一系列操作就完成了2D网格寻路逻辑,使用时你只需要拷贝RoutingObjectRouting两个代码即可。

使用方式:调用Routing.Instance.IsRouting(start, end, map)校验是否能通过,若能通过则通过Routing.Instance.GetRoute() 获取最短路径,提供个实际逻辑使用即可。


说好的:文末源码;没有积分的童鞋,可以关注下面

Unity 之 手把手教你实现自己Unity2D游戏寻路逻辑 【文末源码】相关推荐

  1. 零基础教你Unity制作像素鸟游戏 【文末源码】

    爆肝三天终于写完了,一文教你从零开启Unity制作像素鸟游戏 前言 一,新建目录 二,制作材质 三,场景搭建 四,创建地图 五,制作管道 六,创建主角 七,小鸟动起来 八,游戏状态控制 九,摄像机跟随 ...

  2. java计算机毕业设计幼儿早教系统软件设计与实现MyBatis+系统+LW文档+源码+调试部署

    java计算机毕业设计幼儿早教系统软件设计与实现MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计幼儿早教系统软件设计与实现MyBatis+系统+LW文档+源码+调试部署 本源码技 ...

  3. paddle 标注_一看就会,手把手教你编程,批量文章标注拼音(附源码)

    文/IT可达鸭 图/IT可达鸭.网络 前言 是不是学了Python之后,苦于没有项目练手?是不是看了很多关于编程视频,等到自己动手时,却怎么也做不出一个项目? 工作在一线的老程序员告诉你,别慌,让我手 ...

  4. 手把手教 | 使用Bert预训练模型文本分类(内附源码)

    作者:GjZero 标签:Bert, 中文分类, 句子向量 本文约1500字,建议阅读8分钟. 本文从实践入手,带领大家进行Bert的中文文本分类和作为句子向量进行使用的教程. Bert介绍 Bert ...

  5. 手把手教你调试构建一个Vue/小程序商城项目源码

    下面将详细的介绍weiphp5.0商城项目的调试打包上线的流程: 安装NodeJs/NPM 安装CNPM(可忽略) 运行项目 打包上线项目 1. 安装NodeJs 推荐到NodeJS的官网下载安装包 ...

  6. 手把手教你使用LabVIEW OpenCV dnn实现图像分类(含源码)

    文章目录 前言 一.什么是图像分类? 1.图像分类的概念 2.MobileNet简介 二.使用python实现图像分类(py_to_py_ssd_mobilenet.py) 1.获取预训练模型 2.使 ...

  7. Unity 简单手机小游戏 - 3D重力滚球(文末源码)

    游戏效果图: 目前做了5个关卡 通过陀螺仪使得小球有运动的力 public class groy : MonoBehaviour {float x;float y;Gyroscope go;void ...

  8. python手机版做小游戏代码大全-Python大牛手把手教你做一个小游戏,萌新福利!...

    原标题:Python大牛手把手教你做一个小游戏,萌新福利! 引言 最近python语言大火,除了在科学计算领域python有用武之地之外,在游戏.后台等方面,python也大放异彩,本篇博文将按照正规 ...

  9. 手把手教你如何用Python制作一个电子相册?末附python教程

    这里简单介绍一下python制作电子相册的过程,主要用到tkinter和pillow这2个库,tkinter用于窗口显示照片,pillow用来处理照片,照片切换分为2种方式,一种是自动切换(每隔5秒) ...

最新文章

  1. spring Bean自动装配
  2. [转]cocos2d-js 3.0 屏幕适配方案 分辨率适应
  3. Scrapy框架模拟Github网站登陆
  4. 亚马逊AWS:用AI和机器学习让所有人受益
  5. pagehelper分页
  6. shell整理(41)====判断输入是不是ip
  7. uiautomator环境搭建所遇问题汇总
  8. 软件版本命名规范(转载)
  9. legend3---Homestead中Laravel项目502 Bad Gateway
  10. 【Visual Studio】Visual Studio 2019 创建 Windows 控制台程序 ( 安装 ‘使用 C++ 的桌面开发‘ 组件 | 创建并运行 Windows 控制台程序 )
  11. 【javascript知识点】javascript 额外篇
  12. Swift开发图解入门
  13. VirtualBox在win10下安装一个国产深度os桌面系统的操作教程
  14. 【AI视野·今日Robot 机器人论文速览 第二十四期】Thu, 30 Sep 2021
  15. 《BI那点儿事》数据挖掘初探
  16. 高效的JavaScript.
  17. ppt课堂流程图_4个超实用的PPT制作技巧:开学提升备课质量,资深老师都在用
  18. Docker系列(三)容器的基本操作
  19. ApacheCN 活动汇总 2019.7.19
  20. Matlab常用命令汇总

热门文章

  1. 2017阿里校招内推面试回忆
  2. 计算机考研高数试卷答案,考研数学试卷大全(全国各高校历年试卷)
  3. 去叶剂行业调研报告 - 市场现状分析与发展前景预测
  4. 计算机辐射测试,测试计算机的电磁辐射
  5. 如何通过OPENROWSET函数向SQL Server导入带工作组(mdw)保护的Access数据库数据(转)...
  6. mouse without borders 两台主机共用一套鼠标键盘
  7. 区块链女侠杨霞:为区块链代码提供军事级的安全检测丨蚂蚁区块链大赛成都站火热报名...
  8. 关于Deepin商店没有应用的解决办法
  9. Java程序 switch语句
  10. Weisfeiler-Lehman(WL)算法