A*寻路实例项目实践笔记

前言

好久不见!今天使用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。

将这些相邻节点存入一个集合,然后算法看一圈,并拿到F cost最低的节点,对拿到的点重复上诉过程。

直到在相邻节点中找到了B点,算法就完成工作了。在没有障碍加入的情况下,路径向着终点就去了。

当然,如果你的游戏里面没有障碍物,还需要什么寻路算法,接下来我们看看加入障碍物以后的情况。如下图所示,在执行了一步操作以后,出现了三个F cost相同的相邻节点,这时候我们选择H cost,也就是离B点最近的节点

然后,依旧是选择F cost最小的两个中的一个,这一次两个节点从参数上看完全一样,所以随便选一个,反正如果选错的话得到的相邻节点的F cost最后都不可能大于另一个选择。

现在我们可以看到,鼠标指向的54是下一个选择,但是这里有个小细节,他左边相邻节点的G cost为38,而不是最短距离30,这是因为我们第一次将该节点考虑进来是通过鼠标指针上面的48和其相邻。所以说G cost得到的是根据最近一次路径运算得到的最小值,而如果我们马上考虑鼠标指向的54,我们就会发现到达这个左边相邻节点的G cost出现更小值,所以我们进行更新。这个节点现在G cost为更小值30,F cost为60。总结一下:G cost当前的值不一定是最小值,而是当前所以算过的路径下的最小值。其实说到这里,我们就知道节点还需要存储一个父节点,表明目前这个最小的G cost是从与哪个节点相邻得来的。

说句题外话,既然G cost不一定是最短, H cost呢?H cost一定是最短,H cost通过某节点先直线到B节点的同一横向或纵向,再直线到B得来的,所以一定是最短。

我们继续,现在更新后的60是最短,我们选60。

就这样一直选下去,最后我们就能得到这条路径,对了,别忘了前面说的每个节点需要记录自己是通过哪个父节点走到的,这样才能得到路径

保存父节点,就像这样

来看一下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
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值