Unity A*寻路算法

前言:为什么要使用A*寻路算法,不直接使用unity自带的Navigation组件呢?

  1. 灵活性高

    • A*算法允许开发者根据具体游戏需求调整和优化算法实现,比如通过改变启发式函数来适应不同的地图和寻路条件。
    • Unity的Navigation组件虽然强大,但在一些特殊场景或需要高度定制的路径计算中可能不够灵活。
  2. 效率高

    • A*算法结合了Dijkstra算法的最短路径搜索和贪心算法的启发式搜索,能有效减少不必要的搜索,从而提高寻路效率。
    • 尽管Unity的Navigation系统也进行了优化,但在某些复杂场景下,自定义的A*算法可能会提供更好的性能表现。
  3. 可拓展性强

    • A*算法易于扩展和维护,开发者可以根据项目的实际需要添加新的功能或者进行调试。
    • Unity的Navigation系统虽然提供了可视化工具和诸多功能,但在进行特定扩展时可能需要更多的工作。
  4. 支持二维与三维环境

    • Unity的Navigation组件主要针对3D环境设计,对于2D游戏的路径寻找支持不如A*算法直接和高效。
  5. 适应多种场景的需求

    在不同的游戏开发项目中,地图和场景的设计差异较大,A*算法因其灵活性和适应性被广泛采用

Navigation组件的局限性:

  1. 烘焙时间和资源消耗:对于大型或复杂场景,Navigation组件的烘焙过程可能会非常耗时且消耗大量资源。

  2. 动态环境的处理:尽管新版Unity Navigation系统支持动态烘焙,但在处理频繁变化的场景时仍可能面临性能挑战。

  3. 易用性与控制权衡:Navigation组件虽然简化了寻路设置,但同时也牺牲了某些高级功能和自定义选项,这在需要精确控制的游戏设计中可能是一个缺点

A*寻路的算法估价

在A算法中核心的寻路依据就是估量代价,在A*寻路算法中通常用F表示。

F=G+H
其中G表示当前点到起始点的估量代价,表示当前点到终点的代价。

G的计算方式:最开始,以起点为中心开始计算周边的八个格子,然后在以这算出来的八个
格子为中心,继续计算他们周边的八个格子的G值,在计算的时候区域有所覆盖,如果计算出来的值小于格子目前的G值,该格子的G值就要更新为较小的G值。以此类推,直到找到终点,遍历完所有的格子。G值的计算结果为:中心点的G值+距离值【10or14】

设定每个格子之间的距离为10,那么从中心的格子到对角线之间的距离就是两个格子中心之间的连线,也就是14,G值为左下角的数值,H值为右下角的数值,F值为左上角数值,默认起点的G值为0

 H的计算方式:H值是从当前点到终点的预估代价。这个值的计算通常基于某种启发式方法,以估算剩余距离。一个简单的启发式方法是使用欧几里得距离,即在二维空间中,两点间的直线距离可以通过勾股定理来计算。

A*寻路算法的具体步骤及代码

每次计算G和H的时候建立下面这样的表格,找到F值最小的格子,然后再通过该格子找到周围的8个格子再次建立,这样的表格,找到F值最小的格子,以此递归,直到找到终点为止,每次当过中心点的格子会被记录起来来防止下次再次经过这个格子。A*寻路算法的本质就是通过终点回溯来找到最短路径。

序号GHF
1101020
2101424
3101020

步骤

  1. 设置开放列表OpenList和关闭列表CloseList
  2. 将起点放到OpenList
  3. 开启循环While(OpenList.count > 0)

        3.1 将OpenList按照F值从小到大排序

        3.2 OpenList[0]的值必是最小的,命名为center

                3.2.1发现Centeri就是终点,回溯找到导航路径
 

        3.3 以这个点为中心去计算周边的8个格子的三个值

        3.4 如果这个格子没有被计算过或者原来的G值比这次计算的还要大

                3.4.1 此时设置新的FGH值给该格子,并设置该格子的发现者为center

        3.5 如果这个格子被计算过,且原G值比这次计算的要小

                3.5.1 此时就不能替换原来的FGH值

        3.6 将发现的每个格子放入OpenList

                3.6.1 放入的时候要检测【该格子不在OpenList,该格子不在CloseList】

        3.7 将此次的发现者Center放入CloseList中

        3.8 判断OpenList为空

                3.8.1 说明所有的可发现的格子都被遍历过了,始终没有找到中,说明无法到达终点

代码

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Serialization;
using Random = UnityEngine.Random;

public class AStar : MonoBehaviour
{
    [Header("要生成的小方格")] 
    public GameObject cubePrefab;
    [Header("小方格的边长")] 
    public float cubeLength = 0.4f;
    [Header("地面缩放的实际长度")] 
    public int gridScale = 10;

    [Header("地形网格的长度")] 
    public int gridLength;
    [Header("地形网格的宽度")] 
    public int gridWidth;
    [Range(0,100)]
    [Header("障碍物出现的比例")]
    public int ObstacleScale = 30;
    [Space]
    [Header("起点坐标")]
    public int startX;
    public int startY;
    [Header("终点坐标")]
    public int endX;
    public int endY;
    //存储所有的格子
    public GridItem[,] _gridItems;
    //路径总长度
    public int pathCount;

    public Camera mainCamera;
    public void GridInit()
    {
        //计算gridLength和gridWidth,即地形网格的行数和列数。这个计算基于地面缩放比例和小方格的边长
        gridLength = (int)(transform.localScale.x * gridScale/cubeLength); 
        gridWidth = (int)(transform.localScale.z * gridScale/cubeLength);
        //初始化数组
        _gridItems = new GridItem[gridLength, gridWidth];
        //生成所有的格子
        for (int i = 0; i < gridLength; i++)
        {
            for (int j = 0; j < gridWidth; j++)
            {
                
                //由于在生成的时候是从Plane的中心生成所以需要一个偏移量确保从左下角生成
                Vector3 gridOffset = new Vector3(-gridScale / 2 * transform.localScale.x, 0,
                    -gridScale / 2 * transform.localScale.z);
                
                Vector3 cubeOffset = new Vector3(cubeLength / 2, 0,
                    cubeLength / 2);
                
               GameObject cube = Instantiate(cubePrefab,
                    new Vector3((float)i * cubeLength, 0, (float)j * cubeLength) +
                    gridOffset + cubeOffset + transform.position,
                    Quaternion.identity);
                
                cube.transform.SetParent(transform);
                //获取脚本组件
                GridItem item = cube.GetComponent<GridItem>();
                //存储格子到数组中
                _gridItems[i, j] = item;
                //设置坐标
                item.x = i;
                item.y = j;
                //设置物体类型
                int ran = Random.Range(1, 101);
                
                //如果在范围之内
                if (ran < ObstacleScale)
                {
                    //设置为障碍物
                    item.SetItemType(ItemType.Obstacle);
                }
            }
        }
        //设置起点和终点格子
        try
        {
          _gridItems[startX, startY].SetItemType(ItemType.Start);
           _gridItems[endX, endY].SetItemType(ItemType.End);
        }
        catch (IndexOutOfRangeException e)
        {
            startX = 0;
            startY = 0;
            endX = gridLength - 1;
            endY = gridWidth - 1;
            _gridItems[startX, startY].SetItemType(ItemType.Start);
            _gridItems[endX, endY].SetItemType(ItemType.End);
            Debug.LogWarning("起点坐标或终点坐标设置错误");
        }
    }

    private void Awake()
    {
        mainCamera = Camera.main;
        
    }

    private void Start()
    {
        GridInit();
        AStarFinding();
    }

    #region A Star Need Feild
    //开启列表->存储所有FGH待计算的格子
    private List<GridItem> openList;
    //关闭列表->存储所有发现者的格子
    private List<GridItem> closeList;
    //路径栈
    private Stack<GridItem> pathStack;
        
    #endregion
    private void AStarFinding()
    {
        //初始化列表
        openList = new List<GridItem>();
        closeList = new List<GridItem>();
        pathStack = new Stack<GridItem>();
        //将起点放置到开启列表
        openList.Add(_gridItems[startX,startY]);
        //开启循环
        while (true)
        {
            //按照F值从小到大排序
            openList.Sort();
            //找到F值最小的格子
            GridItem center = openList[0];

            if (center.itemType == ItemType.End)
            {
                //TODO:回溯找到导航路径
                GeneratePath(center);
                break;
            }
            //以Center格子为中心去发现周边的8个格子
            for (int i = -1; i <= 1; i++)
            {
                for (int j = -1; j <= 1; j++)
                {
                    //如果是中心格子,略过
                    if (i == 0 && j == 0)
                        continue;
                    //真正的格子坐标
                    int x = center.x + i;
                    int y = center.y + j;
                    
                    //判断下标是否越界,掠过
                    if (x < 0 || x > gridLength - 1)
                        continue;
                    if(y < 0 || y > gridWidth - 1)
                        continue;
                    //临时存储当前的格子
                    GridItem crtItem = _gridItems[x, y];
                    //判断格子是否为障碍物,掠过
                    if(crtItem.itemType == ItemType.Obstacle)
                        continue;
                    //如果该格子已经作为中心被计算过,掠过
                    if(closeList.Contains(crtItem))
                        continue;
                    int H = CountH(x, y);
                   
                    int G = CountOffsetG(i, j) + center.G;
                    //如果该格子从未计算过G值或原G值比新算的G值要大
                    if (crtItem.G == 0 || crtItem.G > G)
                    {
                       
                        crtItem.G = G;
                        //更新发现者
                        crtItem.parent = center;
                        
                        crtItem.H = H;

                        
                        //计算F值
                        crtItem.F = crtItem.G + crtItem.H;
                    }
                   

                    if (!openList.Contains(crtItem))
                    {
                        openList.Add(crtItem);
                    }
                }
            }
            //for循环结束
            //将当前中心从openList中移除
            openList.Remove(center);
            //将当前中心添加到closeList
            closeList.Add(center);
            //遍历到了尽头还是没有找到终点
            if (openList.Count == 0)
            {
                //TODO:无法找到路径
                Debug.Log("无法找到路径");
                break;
            }
        }
    }
    /// <summary>
    /// 生成路径
    /// </summary>
    /// <param name="Item"></param>
    private void GeneratePath(GridItem item)
    {
        pathStack.Push(item);
        //先看一下是否有发现者
        if (item.parent != null)
        {
            //递归查找
            GeneratePath(item.parent);
        }
        else
        {
            //找到起点,路径点存储完毕
            //生成路径
            pathCount = pathStack.Count;
            StartCoroutine(ShowPath());
        }

        
    }

    IEnumerator ShowPath()
    {
        int i = 1;
        while (pathStack.Count > 0)
        {
            i++;
            yield return new WaitForSeconds(.2f);
            GridItem item = pathStack.Pop();
            if (item.itemType == ItemType.Normal)
            {
                item.SetColor(Color.Lerp(Color.red, Color.green, (float)(pathCount - pathStack.Count) / pathCount));
                mainCamera.transform.position = Vector3.Lerp(new Vector3(mainCamera.transform.position.x, 2, mainCamera.transform.position.z),
                    new Vector3(item.transform.position.x, 2, item.transform.position.z),
                    (float)(pathCount - pathStack.Count) / pathCount);
            }
        }
       
    }
    /// <summary>
    /// 计算H值
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <returns></returns>
    private int CountH(int x, int y)
    {
        //计算水平方向的步数
        int newX = x - endX;
        newX = newX > 0 ? newX : -newX;
        int newY = y - endX;
        newY = newY > 0 ? newY : -newY;
        return 10 * (newX + newY);
    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            UnityEngine.SceneManagement.SceneManager.LoadScene(0);
        }
    }
    /// <summary>
    /// 计算G值
    /// </summary>
    /// <param name="i"></param>
    /// <param name="j"></param>
    /// <returns></returns>
    private int CountOffsetG(int i, int j)
    {
        if (i == 0 || j == 0)
            return 10;
        return 14;
    }
    [Obsolete]
    private int OldCountOffsetG(int i, int j)
    {
        return (int)Mathf.Sqrt(i * i + j * j) * 10;
    }
}

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum ItemType
{
    Normal,
    Obstacle,
    Start,
    End
}
public class GridItem : MonoBehaviour, IComparable<GridItem>
{
    //设置行和列的坐标
    public int x;
    public int y;
    //格子类型
    public ItemType itemType;

    public MeshRenderer _meshRenderer;
    //FGH
    public int F;
    public int G;
    public int H;
    private IComparable<GridItem> _comparableImplementation;

    //发现者
    public GridItem parent;
    private void Awake()
    {
        _meshRenderer = GetComponent<MeshRenderer>();
    }

    public void SetColor(Color color)
    {
        _meshRenderer.material.color = color;
    }
    public void SetItemType(ItemType itemType)
    {
        this.itemType = itemType;
        switch (itemType)
        {
            case ItemType.Obstacle:
                _meshRenderer.material.color = Color.blue;
                break;
            case ItemType.Start:
                _meshRenderer.material.color = Color.red;
                break;
            case ItemType.End:
                _meshRenderer.material.color = Color.green;
                break;
                
        }
    }



    public int CompareTo(GridItem other)
    {
        if (this.F < other.F)
        {
            //当前格子在List中的下标-1
            return -1;
        }
        if (this.F > other.F)
        {
            //当前格子在List中的下标+1
            return 1;
        }
        //当前格子在List中的下标+0
        return 0;
        
    }
}

  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我会尝试回答你的问题。 首先,要使用Unity脚本实现A*寻路算法,需要在Unity中创建一个脚本,并将其添加到场景中的游戏对象上。 以下是实现A*寻路算法Unity脚本示例代码: ```csharp using UnityEngine; using System.Collections; using System.Collections.Generic; public class AStarPathfinding : MonoBehaviour { public Transform seeker, target; //起点和终点 Grid grid; //寻路所需的网格 void Awake() { grid = GetComponent<Grid>(); } void Update() { FindPath(seeker.position, target.position); } void FindPath(Vector3 startPos, Vector3 targetPos) { Node startNode = grid.NodeFromWorldPoint(startPos); Node targetNode = grid.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) { RetracePath(startNode, targetNode); return; } foreach (Node neighbour in grid.GetNeighbours(currentNode)) { if (!neighbour.walkable || closedSet.Contains(neighbour)) { continue; } int newMovementCostToNeighbour = currentNode.gCost + GetDistance(currentNode, neighbour); if (newMovementCostToNeighbour < neighbour.gCost || !openSet.Contains(neighbour)) { neighbour.gCost = newMovementCostToNeighbour; neighbour.hCost = GetDistance(neighbour, targetNode); neighbour.parent = currentNode; if (!openSet.Contains(neighbour)) { openSet.Add(neighbour); } } } } } void 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(); grid.path = path; } int GetDistance(Node nodeA, Node nodeB) { int dstX = Mathf.Abs(nodeA.gridX - nodeB.gridX); int dstY = Mathf.Abs(nodeA.gridY - nodeB.gridY); if (dstX > dstY) { return 14 * dstY + 10 * (dstX - dstY); } return 14 * dstX + 10 * (dstY - dstX); } } ``` 该脚本中的A*寻路算法会在每次Update()函数调用时寻找从起点到终点的最短路径,并将其保存在网格的路径中。 实现A*寻路算法需要一个网格,该网格由一系列节点组成。每个节点包含了该节点在网格中的位置、该节点到起点的距离(gCost)、
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值