基于Unity的A星寻路算法(绝对简单完整版本)

本文详细介绍了基于Unity的A星寻路算法实现过程,包括算法的基本原理、编程实现和能效问题。通过理解直线与斜线权重、计算期望路径长度以及处理障碍物等关键步骤,展示了如何在Unity中实现一个简单而完整的A星寻路系统。
摘要由CSDN通过智能技术生成

前言

在上一篇文章,介绍了网格地图的实现方式,基于该文章,我们来实现一个A星寻路的算法,最终实现的效果为:

请添加图片描述

项目源码已上传Github:AStarNavigate

在阅读本篇文章,如果你对于里面提到的一些关于网格地图的创建方式的一些地图不了解的话,可以先阅读了解一下下面的这篇文章:

文章链接:

1、简单做一些背景介绍

在介绍A星寻路算法前,先介绍另外一种算法:Dijkstra寻路算法,简单的来说是一种A星寻路的基础版。Dijkstra作为一种无启发的寻路算法,通过围绕起始点向四周扩展遍历,一直到找到目标点结束,简单来说就是暴力破解,由近到远遍历所有可能,从而找到目标点

很明显,这种寻路方式是很的消耗性能的,非常的不高效,有没有更好的解决方式呢

从实际生活中出发,如果你要到达某地,却不知道具体的路该怎么办呢,是不是先大概确定方向,边靠近目标点边问路呢

A星寻路算法也是基于这样的思路,通过一定的逻辑找到可以靠近物体的方向,然后一步步的走进目标点,直到到达目的地。

二、A星寻路算法的基本原理

整个理解过程是一个线性结构,只需要一步步完整的走下去,基本就可以对于A星有一个大概的了解。

确定直角斜角权重:

本质上来讲,A星寻路是基于一种网格的地图来实现的寻路的方式,在网格中,一个点可以到达的位置为周围的八个方向。而由于水平与垂直和倾斜的方向距离不一样,所以我们在寻路时需要设置不同的长度:

在这里插入图片描述
通过图片可以看出,直线距离与斜线距离是分别等腰直角三角形直角边与斜边。根据勾股定理我们可以得知两者的比例关系约为1.41:1,为了方便计算,我们就将斜边权重为14,而直角边权重为10,这样的话,要得到最短的路径,可以按照下面的思路去考虑:

遍历移动格子可能性:

接下来需要考虑第二个问题,在对起始点周围的可移动格子遍历完成后,如何找到最短路径上的那个格子呢,即下一步该走哪一个格子,这里就是整个A星寻路算法的核心:
在这里插入图片描述

如图,当我们第一步对起始点A周围所有的格子遍历后,从A出发有八个可以移动的方向可以到达下一个格子。如果你作为一个人类,当然一眼可以看出下一步向绿色箭头方向移动产生的路径是最短的。

我们人类可以根据经验很快的判断出方向,但是机器不能,计算机需要严谨的程序逻辑来实现这样的效果,需要我们赋予他基本的执行程序。通过重复的执行这样的逻辑,得到最终的效果。因此,接下来,需要思考如何让计算机在一系列点位中找到方向最正确的那个点位

计算某一格子期望长度:

到目前,我们的目的就是使计算机可以找到找到所有可以走的格子中产生路径最短的格子。接下来以你的经验来思考,比较长短往往是依据什么。嘿嘿,别想歪,确实是数字的大小。所以我们需要给每一个格子一个数值来作为路径通过该格子的代价。

当程序进行到现在,要解决的问题是如何求得一个数字来代表该格子。实现方式是通过计算一个通过格子路径长度的对比来找到最短的路径。而任一格子记录路径长度标记为All,并可以将其分为两部分:已走路径与预估路径(不理解没关系,接着往下看):
在这里插入图片描述

如图(灵魂画手,顺便加个防伪标志嘿嘿)求从A到B点的路径,当前已经寻路到C点,如何求得经过该点的一个期望路径的长度呢:

  • 到达该格子已经走过的路径长度GG值的计算是基于递推的思想,根据上一个格子的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面板做成一个预制体,或者直接隐藏也可以

注意:

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("网格地图模板预制体")
A*寻路算法是一种常用的路径规划算法,它在地图上寻找两个点之间的最短路径。在Unity,我们可以使用以下步骤实现A*寻路算法: 1. 创建地图:我们需要创建一个网格地图,其每个格子都代表着地图上的一个点。每个点都有一个坐标、可通过性和一些其他属性。 2. 创建节点类:我们需要创建一个节点类,用于保存每个点的信息。每个节点都有一个父节点、位置、代价和其他属性。 3. 创建Open和Close列表:我们需要创建两个列表,一个是Open列表,用于存储待搜索的节点,另一个是Close列表,用于存储已搜索过的节点。 4. 初始化起点和终点:我们需要初始化起点和终点,并将起点加入到Open列表。 5. 搜索路径:我们需要重复以下步骤直到找到终点或者Open列表为空: a. 从Open列表选取代价最小的节点作为当前节点。 b. 将当前节点从Open列表删除,并将其加入到Close列表。 c. 检查当前节点是否为终点,如果是则返回路径。 d. 遍历当前节点的相邻节点,计算它们的代价,并将它们加入到Open列表。 6. 返回路径:如果找到了终点,则从终点开始沿着父节点一直往回走,直到回到起点。这样就得到了一条最短路径。 以下是示例代码: ```csharp using System.Collections; using System.Collections.Generic; using UnityEngine; public class AStar : MonoBehaviour { public LayerMask wallMask; public Vector2 gridSize; public float nodeRadius; Node[,] grid; float nodeDiameter; int gridSizeX, gridSizeY; void Start() { nodeDiameter = nodeRadius * 2; gridSizeX = Mathf.RoundToInt(gridSize.x / nodeDiameter); gridSizeY = Mathf.RoundToInt(gridSize.y / nodeDiameter); CreateGrid(); } void CreateGrid() { grid = new Node[gridSizeX, gridSizeY]; Vector3 worldBottomLeft = transform.position - Vector3.right * gridSize.x / 2 - Vector3.forward * gridSize.y / 2; for (int x = 0; x < gridSizeX; x++) { for (int y = 0; y < gridSizeY; y++) { Vector3 worldPoint = worldBottomLeft + Vector3.right * (x * nodeDiameter + nodeRadius) + Vector3.forward * (y * nodeDiameter + nodeRadius); bool walkable = !Physics.CheckSphere(worldPoint, nodeRadius, wallMask); grid[x, y] = new Node(walkable, worldPoint, x, y); } } } public List<Node> GetNeighbours(Node node) { List<Node> neighbours = new List<Node>(); for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { if (x == 0 && y == 0) continue; int checkX = node.gridX + x; int checkY = node.gridY + y; if (checkX >= 0 && checkX < gridSizeX && checkY >= 0 && checkY < gridSizeY) { neighbours.Add(grid[checkX, checkY]); } } } return neighbours; } public Node NodeFromWorldPoint(Vector3 worldPosition) { float percentX = (worldPosition.x + gridSize.x / 2) / gridSize.x; float percentY = (worldPosition.z + gridSize.y / 2) / gridSize.y; percentX = Mathf.Clamp01(percentX); percentY = Mathf.Clamp01(percentY); int x = Mathf.RoundToInt((gridSizeX - 1) * percentX); int y = Mathf.RoundToInt((gridSizeY - 1) * percentY); return grid[x, y]; } public List<Node> FindPath(Vector3 startPos, Vector3 targetPos) { Node startNode = NodeFromWorldPoint(startPos); Node targetNode = NodeFromWorldPoint(targetPos); List<Node> openSet = new List<Node>(); HashSet<Node> closedSet = new HashSet<Node>(); openSet.Add(startNode); while (openSet.Count > 0) { Node currentNode = openSet[0]; for (int i = 1; i < openSet.Count; i++) { if (openSet[i].fCost < currentNode.fCost || (openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost)) { currentNode = openSet[i]; } } openSet.Remove(currentNode); closedSet.Add(currentNode); if (currentNode == targetNode) { return RetracePath(startNode, targetNode); } foreach (Node neighbour in GetNeighbours(currentNode)) { if (!neighbour.walkable || closedSet.Contains(neighbour)) { continue; } int newCostToNeighbour = currentNode.gCost + GetDistance(currentNode, neighbour); if (newCostToNeighbour < neighbour.gCost || !openSet.Contains(neighbour)) { neighbour.gCost = newCostToNeighbour; neighbour.hCost = GetDistance(neighbour, targetNode); neighbour.parent = currentNode; if (!openSet.Contains(neighbour)) { openSet.Add(neighbour); } } } } return null; } List<Node> RetracePath(Node startNode, Node endNode) { List<Node> path = new List<Node>(); Node currentNode = endNode; while (currentNode != startNode) { path.Add(currentNode); currentNode = currentNode.parent; } path.Reverse(); return path; } int GetDistance(Node nodeA, Node nodeB) { int distX = Mathf.Abs(nodeA.gridX - nodeB.gridX); int distY = Mathf.Abs(nodeA.gridY - nodeB.gridY); if (distX > distY) { return 14 * distY + 10 * (distX - distY); } return 14 * distX + 10 * (distY - distX); } public class Node { public bool walkable; public Vector3 worldPosition; public int gridX; public int gridY; public int gCost; public int hCost; public Node parent; public Node(bool _walkable, Vector3 _worldPos, int _gridX, int _gridY) { walkable = _walkable; worldPosition = _worldPos; gridX = _gridX; gridY = _gridY; } public int fCost { get { return gCost + hCost; } } } } ``` 在上面的代码,我们首先创建了一个网格地图,并在其创建了节点类。然后,我们实现了A*算法的核心部分,并将其用于在网格地图上搜索路径。在搜索结束后,我们返回了一条最短路径。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心之凌儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值