前言
在上一篇文章,介绍了网格地图的实现方式,基于该文章,我们来实现一个A星寻路的算法,最终实现的效果为:
项目源码已上传Github:AStarNavigate
在阅读本篇文章,如果你对于里面提到的一些关于网格地图的创建方式的一些地图不了解的话,可以先阅读了解一下下面的这篇文章:
文章链接:
1、简单做一些背景介绍
在介绍A星寻路算法前,先介绍另外一种算法:Dijkstra
寻路算法,简单的来说是一种A星寻路的基础版。Dijkstra
作为一种无启发的寻路算法,通过围绕起始点向四周扩展遍历,一直到找到目标点结束,简单来说就是暴力破解,由近到远遍历所有可能,从而找到目标点
很明显,这种寻路方式是很的消耗性能的,非常的不高效,有没有更好的解决方式呢
从实际生活中出发,如果你要到达某地,却不知道具体的路该怎么办呢,是不是先大概确定方向,边靠近目标点边问路呢
A星寻路算法也是基于这样的思路,通过一定的逻辑找到可以靠近物体的方向,然后一步步的走进目标点,直到到达目的地。
二、A星寻路算法的基本原理
整个理解过程是一个线性结构,只需要一步步完整的走下去,基本就可以对于A星有一个大概的了解。
确定直角斜角权重:
本质上来讲,A星寻路是基于一种网格的地图来实现的寻路的方式,在网格中,一个点可以到达的位置为周围的八个方向。而由于水平与垂直和倾斜的方向距离不一样,所以我们在寻路时需要设置不同的长度:
通过图片可以看出,直线距离与斜线距离是分别等腰直角三角形直角边与斜边。根据勾股定理我们可以得知两者的比例关系约为1.41:1
,为了方便计算,我们就将斜边权重为14
,而直角边权重为10
,这样的话,要得到最短的路径,可以按照下面的思路去考虑:
遍历移动格子可能性:
接下来需要考虑第二个问题,在对起始点周围的可移动格子遍历完成后,如何找到最短路径上的那个格子呢,即下一步该走哪一个格子,这里就是整个A星寻路算法的核心:
如图,当我们第一步对起始点A周围所有的格子遍历后,从A出发有八个可以移动的方向可以到达下一个格子。如果你作为一个人类,当然一眼可以看出下一步向绿色箭头方向移动产生的路径是最短的。
我们人类可以根据经验很快的判断出方向,但是机器不能,计算机需要严谨的程序逻辑来实现这样的效果,需要我们赋予他基本的执行程序。通过重复的执行这样的逻辑,得到最终的效果。因此,接下来,需要思考如何让计算机在一系列点位中找到方向最正确的那个点位
计算某一格子期望长度:
到目前,我们的目的就是使计算机可以找到找到所有可以走的格子中产生路径最短的格子。接下来以你的经验来思考,比较长短往往是依据什么。嘿嘿,别想歪,确实是数字的大小。所以我们需要给每一个格子一个数值来作为路径通过该格子的代价。
当程序进行到现在,要解决的问题是如何求得一个数字来代表该格子。实现方式是通过计算一个通过格子路径长度的对比来找到最短的路径。而任一格子记录路径长度标记为All,并可以将其分为两部分:已走路径与预估路径(不理解没关系,接着往下看):
如图(灵魂画手,顺便加个防伪标志嘿嘿)求从A到B点的路径,当前已经寻路到C点,如何求得经过该点的一个期望路径的长度呢:
- 到达该格子已经走过的路径长度
G
:G
值的计算是基于递推的思想,根据上一个格子的G再加上上一个格子到这个格子的距离即可 - 当前格子到达终点预估路径长度H:该距离是一个估计的距离,至于如何估计的,接下来会进行介绍
然后就可以求出该点的整个期望路径长度All,对G和H进行一个简单的加法:
这样我们就可以通过下一步所有可能的移动的格子中找到最短的格子
关于预估路径长度H的计算:
- 实现对于H的计算的估计有很多,由于本来就是预估,换句话就是不是一定准确的结果,所以我们可以通过计算当前节点到目标点的直线距离或者水平加垂直距离来获得
在本文章的后面演示案例中,是基于水平加垂直距离来计算预估路径长度H,即在上面的图中,从C到B的预估路径计算方式为:
Hcb = 水平格子差 * 10 + 垂直格子差 * 10
上述步骤总结升级:
假设我们走到了C点,并且接下来只能从C点向下一步移动,可以在下面的图中看出接下来格子的所有可能性:
下面我们来手动计算一下4号
和5号
的预估路径长度来帮助你理解这个过程,开始前我们要知道一条斜边长14
,直边长度为10
:
则AC的长度为:
Lac=4*14=56
4号:
H = Lac + 1 * 14 = 70
G = 2 * 10 + 2 * 10 = 40
All = H + G = 110
5号:
H = Lac + 1 * 10 = 66
G = 2 * 10 + 3 * 10 = 50
All = H + G = 116
经过对比,5号
格子的期望路径长度长于4号
,在计算机运行程序时,会对1到7号都进行这样的计算,然后求得其中的一个最小值并作为下一步的移动目标
注意:
- 如过有两个或者多个相同的最小值,会根据程序的写法选择任意一个,这不影响整个程序的运行思路
进一步升级
我们发现,上述步骤是有一些问题,因为场景中没有障碍物,所以物体会一直走直线。但是在实际情况中,假若寻路走进了死胡同,最后的C点周围没有可以移动的点位怎么办呢。
事实上在前面为了便于理解,我们在A星寻路上将问题简化了,一直以最终点作为下一次寻路的起始点,这种方式是没有办法保证最短的路径的,而在实际的A星寻路中,在每一步中,都会记录新的可以移动的路径加入到列表中,我们命名这个列表为开放列表,找到最短的一个节点后,将该点移除,并加入另外一个节点,命名为关闭列表,具体的可以这么说
- 开放列表:用来在其中选择预估路径长度最短的点
- 封闭列表:用来表示已经计算过该点,以后不再进行索引
图中信息注解:
- 红色格子:障碍物
- 白色格子:可以移动区域
- 黄色格子:起始点与终点
- 蓝色格子:代表开放列表中的格子,用来标识下一步所有可以移动的区域
- 绿色格子:所有走过的格子,同时代表闭合列表中的格子
- 黑色格子:最终的路径
通过反复的观看这张动图,相信你应该对于A星寻路有一个完整的理解,接下来,就需要通过编程来实现该寻路算法
三、编程实现
1、制作格子预制体模板
如果你之前看过Unity 制作一个网格地图生成组件这篇文章,你应该很清楚接下来要做什么,如果你不了解也没有关系,我这里再演示一遍:
创建一个Cube
,并调整其缩放,挂载一个脚本Grid
,然后编辑该脚本:
由于是作为寻路的基本格子,因此需要其记录一些信息,我们定义一些变量:
//格子的坐标位置
public int posX;
public int posY;
//格子是否为障碍物
public bool isHinder;
public Action OnClick;
//计算预估路径长度三个值
public int G = 0;
public int H = 0;
public int All = 0;
//记录在寻路过程中该格子的父格子
public Grid parentGrid;
同时在本项目中格子模板需要一个可以改变其颜色的方法用来标识当前模板所处于的状态(障碍、起始点、终点、路径等等),以及一个注册点击事件的委托方法,所以最后完整的代码为:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.UI;
public class Grid : MonoBehaviour
{
public int posX;
public int posY;
public bool isHinder;
public Action OnClick;
//计算预估路径长度三个值
public int G = 0;
public int H = 0;
public int All = 0;
//记录在寻路过程中该格子的父格子
public Grid parentGrid;
public void ChangeColor(Color color)
{
gameObject.GetComponent<MeshRenderer>().material.color = color;
}
//委托绑定模板点击事件
private void OnMouseDown()
{
OnClick?.Invoke();
}
}
完成代码的编写后,就可以将其拖入我们的资源管理窗口Project面板做成一个预制体,或者直接隐藏也可以
注意:
- 如果你不理解我在干什么或者不懂代码的内容,一定要去查看这篇文章:Unity 制作一个网格地图生成组件
2、地图创建
为了提升代码的通用性,在这篇文章中,对于网格地图创建的脚本做出了一些修改,主要在于替换掉脚本中的Grid
变量的定义,转换为GameObject
,由于之前对该脚本有了详细的介绍,所以只贴出了代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class GridMeshCreate : MonoBehaviour
{
[Serializable]
public class MeshRange
{
public int horizontal;
public int vertical;
}
[Header("网格地图范围")]
public MeshRange meshRange;
[Header("网格地图起始点")]
private Vector3 startPos;
[Header("创建地图网格父节点")]
public Transform parentTran;
[Header("网格地图模板预制体")