前言
好久不见!今天使用Unity照着Sebastian Lague大佬的视频做一个A寻路算法的实例项目,包括A寻路的介绍,网格系统的创建,以及一些实际使用案例。
A*的算法原理
简单来说,A是一种寻路算法,通过A可以找到一系列网格中从A到B的最短路线。假如说,我们想得到下图所示A到B的最短路线,第一步我们需要得到关于A节点的所有相邻节点和这些相邻节点的的一些参数。第一个参数是相邻节点到A的距离,叫做G cost。第二个参数是该相邻节点到B节点的距离,叫做H cost,基本上可以说H cost是G cost的反面。最后一个参数为G cost和H cost的和,叫做F cost。
![](https://i-blog.csdnimg.cn/blog_migrate/2a9fed77dcf1db8374863aa4ed862cfa.png)
将这些相邻节点存入一个集合,然后算法看一圈,并拿到F cost最低的节点,对拿到的点重复上诉过程。
![](https://i-blog.csdnimg.cn/blog_migrate/e5269446fe692147acd09952319f55cf.png)
直到在相邻节点中找到了B点,算法就完成工作了。在没有障碍加入的情况下,路径向着终点就去了。
![](https://i-blog.csdnimg.cn/blog_migrate/a913c4c6a1f4760e45093244bc1ac702.png)
当然,如果你的游戏里面没有障碍物,还需要什么寻路算法,接下来我们看看加入障碍物以后的情况。如下图所示,在执行了一步操作以后,出现了三个F cost相同的相邻节点,这时候我们选择H cost,也就是离B点最近的节点
![](https://i-blog.csdnimg.cn/blog_migrate/7cb06363e6e2a86a687e9e9ad7dd7067.png)
然后,依旧是选择F cost最小的两个中的一个,这一次两个节点从参数上看完全一样,所以随便选一个,反正如果选错的话得到的相邻节点的F cost最后都不可能大于另一个选择。
![](https://i-blog.csdnimg.cn/blog_migrate/e64d00631fe96986fe1ee1478671e173.png)
现在我们可以看到,鼠标指向的54是下一个选择,但是这里有个小细节,他左边相邻节点的G cost为38,而不是最短距离30,这是因为我们第一次将该节点考虑进来是通过鼠标指针上面的48和其相邻。所以说G cost得到的是根据最近一次路径运算得到的最小值,而如果我们马上考虑鼠标指向的54,我们就会发现到达这个左边相邻节点的G cost出现更小值,所以我们进行更新。这个节点现在G cost为更小值30,F cost为60。总结一下:G cost当前的值不一定是最小值,而是当前所以算过的路径下的最小值。其实说到这里,我们就知道节点还需要存储一个父节点,表明目前这个最小的G cost是从与哪个节点相邻得来的。
![](https://i-blog.csdnimg.cn/blog_migrate/7cb8086d3d13e605d4607abba88ada18.png)
说句题外话,既然G cost不一定是最短, H cost呢?H cost一定是最短,H cost通过某节点先直线到B节点的同一横向或纵向,再直线到B得来的,所以一定是最短。
![](https://i-blog.csdnimg.cn/blog_migrate/76a7af499d5918668a2729abf68fc855.png)
我们继续,现在更新后的60是最短,我们选60。
![](https://i-blog.csdnimg.cn/blog_migrate/9a91816e2389bcf0de0f735c2dc9b03f.png)
就这样一直选下去,最后我们就能得到这条路径,对了,别忘了前面说的每个节点需要记录自己是通过哪个父节点走到的,这样才能得到路径。
![](https://i-blog.csdnimg.cn/blog_migrate/070d172d8bd61c9bebfa4ef2ad0fe246.png)
保存父节点,就像这样
![](https://i-blog.csdnimg.cn/blog_migrate/e42d6161dbc36d11a45cdf7574c1c283.png)
来看一下A* 算法的伪代码。
可以将OPEN理解为上图中的绿色节点,意味着候选的路径节点,在算法起始时起点为OPEN中的唯一选择,CLOSED理解为红色的点过的节点,意味着算法已经算出了到这个点的最短距离了,以后也不需要再看他了。每次循环开始时从OPEN里面找到F cost最小的作为当前节点,对于当前节点的相邻节点,如果这个节点不可移动,或者已经找到最短路径了,就跳过,否则查看这个相邻节点是否不在OPEN里面或者能得到一个更短的G cost,一个更短的G cost意味着找到了新的到这个相邻节点的更短路径,需要更新这个节点的G cost F cost以及设置当前节点为该相邻节点新的父节点,而不在OPEN意味着该相邻节点从未被考虑,现在要被考虑进来。如此循环往复,当某次循环发现当前节点就是目标节点时,寻路结束。
接下来要做的就是找到目标节点的父节点,再找到这个父节点的父节点,直到找回起始点,得到路径。
实现网格系统
要在项目中使用A*我们首先需要网格,这里大佬直接教我们自制一套网格系统。我们的网格包含一个节点类,一个网格类。节点负责定义网格中的一个位置以及该位置的信息(目前只有unwalkable)。节点将由网格负责创建。可以自定网格整体大小,单个节点大小,可以显示在Scene面板(OnDrawGizmos),可以通过给定(网格中的)某个位置获得网格中的单个节点,由此可以获得玩家所在的网格节点。
节点类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Node
{
// node能走吗?会根据是否与障碍物重合判断
public bool walkable;
// node中心的世界坐标位置
public Vector3 worldPosition;
// 在Grid二维坐标中的位置
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;
}
// fCost为gCost + hCost 所以写个属性就行
public int fCost
{
get {
return gCost + hCost; }
}
}
网格类
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// Grid的GameObject X、Y轴要在场景正中
public class Grid : MonoBehaviour
{
// 用来存储unwalkable的layermask
public LayerMask unwalkableMask;
// grid的大小
public Vector2 gridWorldSize; // Vector2的y对应世界坐标中的z轴
// grid中的node的半径(node立方体边长的一半)
public float nodeRadius;
// 玩家的位置
public Transform player;
// grid是二位的node数组
Node[,] grid;
// grid中的node的直径
float nodeDiameter;
// Grid中的node数量
int gridSizeX, gridSizeY;
// 路径
public List<Node> path;
private void Start()
{
// 根据grid的尺寸和node的尺寸计算node的数量并填入二维数组
nodeDiameter = nodeRadius * 2;
gridSizeX = Mathf.RoundToInt(gridWorldSize.x / nodeDiameter);
gridSizeY = Mathf.RoundToInt(gridWorldSize.y / nodeDiameter);
CreateGrid();
}
// 创建grid实例
private void CreateGrid()
{
grid = new Node[gridSizeX, gridSizeY];
// 计算得到grid(从上往下看)左下角的世界坐标位置
Vector3 worldButtomLeft = transform.position - Vector3.right * gridWorldSize.x / 2 - Vector3.forward * gridWorldSize.y / 2; //forward没错,y对应node的z坐标
for (int x = 0; x < gridSizeX; x++)
{
for (int y = 0; y < gridSizeY; y++)
{
// 计算每一个node的世界坐标位置
Vector3 worldPoint = worldButtomLeft +
Vector3.right * (x * nodeDiameter + nodeRadius) +
Vector3.forward * (y * nodeDiameter + nodeRadius);
// 判断是否有obstacles,如果有就将node设置为unwalkable
bool walkable = !(Physics.CheckSphere(worldPoint, nodeRadius, unwalkableMask));
// 创建每个node实例并给成员赋值
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;
// 结果不能超出Grid的范围
if(checkX >= 0 && checkY < gridSizeY && checkX < gridSizeX && checkY >= 0)
{
neighbours.Add(grid[checkX, checkY]);
}
}
}
return neighbours;
}
// 通过世界坐标获得Node
public Node GetNodeFromWorldPoint(Vector3 worldPosition)
{
// 通过将坐标换算为Grid中的百分比位置来获取Node
float percentX = (worldPosition.x + gridWorldSize.x / 2) / gridWorldSize.x;
float percentY = (worldPosition.z + gridWorldSize.y / 2) / gridWorldSize.y; //grid的y长对应世界坐标系的z
percentX = Mathf.Clamp01(percentX);
percentY = Mathf.Clamp01(percentY);
int x = Mathf.RoundToInt((gridSizeX - 1) * percentX); //减一是因为gridSize是1开始,我们需要index
int y = Mathf.RoundToInt((gridSizeY - 1) * percentY);
return grid[x, y];
}
// 在Scene面板中显示grid
private void OnDrawGizmos()
{
Gizmos.DrawWireCube(transform.position, new Vector3(gridWorldSize.x, 1, gridWorldSize.y));
if (grid != null)
{
Node playerNode = GetNodeFromWorldPoint(player.position);
foreach(Node node in grid)
{
Gizmos.color = node.walkable ? Color.white : Color.red;
if(playerNode == node