1. 程式人生 > 其它 >Unity中實現A*尋路

Unity中實現A*尋路

前言:最近沒事兒沒工作,計劃每天寫一篇部落格,防止對Unity生疏,也可以記錄學習的點點滴滴。

A*尋路在很多面試裡都會問到,但實際工作中根本用不著自己寫,網上有成熟的外掛,不容易錯還方便。

思路:我們將地塊切成大小均勻的格子,格子分成普通(可通行)、起點、終點、阻擋型別(不可通行)。每次迴圈時,查詢open列表中綜合代價最低的為當前格子,查詢當前格子的八個方向(也可以查詢四個方向)的鄰格,計算綜合代價並加入到open列表中去,當前格子就加入到close列表裡,並從open列表中移除。當open列表內容為空時,或者當前格子已經為結束點時,結束迴圈。F代表綜合代價,也就是起始距離 + 結束距離 = 綜合距離;H代表結束距離(忽視阻擋);G代表起始距離

示例:

第一步,先實現Cell物件

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

public class Cell : MonoBehaviour, IComparable
{
    private CellType m_type;
    public CellType myType
    {
        get => m_type;
        set
        {
            m_type = value;
            
switch (m_type) { case CellType.Normal: SetColor(Color.white); break; case CellType.Start: SetColor(Color.green); break; case CellType.End: SetColor(Color.red);
break; case CellType.Block: SetColor(Color.black); break; } } } public Vector2Int pos;//座標 public int F, G, H;//綜合代價、起始代價、結束代價 public Cell parent;//父物體,為方便查詢上一個節點,類似連結串列 private MeshRenderer render; private void Awake() { render = GetComponent<MeshRenderer>(); } public void SetColor(Color color) { render.material.color = color; } /// <summary> /// 兩個cell物件排序,需要實現icompareable介面,指定它倆對比,是指對比F /// </summary> /// <param name="obj"></param> /// <returns></returns> public int CompareTo(object obj) { Cell cell = (Cell)obj; if (cell.F > F) { return -1; } else if (cell.F == F) { return 0; } else { return 1; } } }

第二步,實現查詢具體方法

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

/// <summary>
/// 這就是格子型別
/// </summary>
public enum CellType
{
    Normal = 1,
    Start = 2,
    End = 3,
    Block = 4
}
public class AStar : MonoBehaviour
{
    /// <summary>
    /// 這是生成地圖資料
    /// </summary>
    public int[,] map =
    {
        {2, 1, 4, 1, 1 },
        {1, 1, 4, 1, 1 },
        {1, 1, 4, 1, 1 },
        {1, 1, 4, 1, 1 },
        {1, 1, 1, 1, 3 }
    };
    /// <summary>
    /// 預製體
    /// </summary>
    public GameObject prefab;

    /// <summary>
    /// 所有格子物件
    /// </summary>
    private Cell[,] cells;

    /// <summary>
    /// 開始點座標和結束點座標
    /// </summary>
    private Vector2Int startPos, endPos;

    /// <summary>
    /// open列表和close列表
    /// </summary>
    private List<Cell> openList, closeList;
    /// <summary>
    /// 儲存路徑的棧
    /// </summary>
    private Stack<Cell> path;


    private void Start()
    {
        cells = new Cell[map.GetLength(0), map.GetLength(1)];
        openList = new List<Cell>();
        closeList = new List<Cell>();
        path = new Stack<Cell>();

        //生成地圖
        Vector3 pos = Vector3.zero;
        for (int i = 0; i < map.GetLength(0); i++)
        {
            for (int j = 0; j < map.GetLength(1); j++)
            {
                pos.Set(i * 1.5f, 0, j * 1.5f);
                GameObject obj = GameObject.Instantiate<GameObject>(prefab);
                obj.transform.parent = transform;
                obj.transform.position = pos;
                Cell cell = obj.AddComponent<Cell>();
                CellType temp = (CellType)map[i, j];
                if (temp == CellType.Start)
                {
                    startPos.Set(i, j);
                }
                else if (temp == CellType.End)
                {
                    endPos.Set(i, j);
                }
                cell.myType = temp;
                cell.pos.Set(i, j);
                cells[i, j] = cell;
            }
        }
    }

    private void Update()
    {
        if (Input.GetMouseButton(0))
        {
            Find();
            StartCoroutine(Draw());
        }
    }

    private void Find()
    {
        openList.Add(cells[startPos.x, startPos.y]);
        Cell currentCell = openList[0];
        while (openList.Count > 0 && currentCell.myType != CellType.End)
        {
            //通過排序找到綜合代價最小的
            openList.Sort();
            currentCell = openList[0];

            //這裡已經找到了
            if (currentCell.myType == CellType.End)
            {
                while (currentCell.parent != null)
                {
                    if (currentCell.parent.myType != CellType.Start)
                    {
                        path.Push(currentCell.parent);
                    }
                    currentCell = currentCell.parent;
                }
                return;
            }

            //查詢八個鄰格
            for (int i = -1; i <= 1; i++)
            {
                for (int j = -1; j <= 1; j++)
                {
                    //增量為0,代表這個座標指自己,所以直接跳過
                    if (i == 0 && j == 0)
                    {
                        continue;
                    }
                    //如果想只獲取四個方向的鄰格,就需要排除增量i = 增量j的情況
                    //if (i == j)
                    //{
                    //    continue;
                    //}
                    int x = currentCell.pos.x + i;
                    int y = currentCell.pos.y + j;
                    if (x < 0 || y < 0 || x >= cells.GetLength(0) || y >= cells.GetLength(1) || cells[x,y].myType == CellType.Block || closeList.Contains(cells[x,y]))
                    {
                        //這裡判斷當前鄰格的座標是否合法?是否為阻塞格子?是否已經存在於close列表中?
                        continue;
                    }
                    //重新計算起始距離,乘10為了方便計算。當前座標的格子的起始距離 = CurrenCell的起始距離 + 當前座標的格子與CurrenCell的距離
                    int g = (int)(currentCell.G + Mathf.Sqrt(Mathf.Abs(i) + Mathf.Abs(j))* 10);
                    if (cells[x,y].G == 0 || g < cells[x,y].G)
                    {
                        //若當前座標的格子並未被查詢過,或者當前座標的格子的起始代價大於新算的起始代價,則更新
                        cells[x, y].G = g;
                        cells[x, y].parent = currentCell;
                    }
                    cells[x, y].H = (Mathf.Abs(x - endPos.x) + Mathf.Abs(y - endPos.y)) * 10;//計算結束距離
                    cells[x, y].F = cells[x, y].G + cells[x, y].H;//綜合代價
                    if (!openList.Contains(cells[x,y]))
                    {
                        openList.Add(cells[x, y]);
                    }
                }
            }
            openList.Remove(currentCell);
            closeList.Add(currentCell);
            if (openList.Count == 0)
            {
                //這裡是指open列表都已經沒有內容了,但是仍未查詢到結束點,因此可認為無路可達
                Debug.LogWarning("窮途末路");
            }
        }
    }

    private IEnumerator Draw()
    {
        while (path.Count > 0)
        {
            Cell cell = path.Pop();
            cell.SetColor(Color.blue);
            yield return new WaitForSeconds(0.2f);
        }
    }
}

第三步,測試並檢視正確性

這是八個方向的

 這是四個方向的

總結:

以前同事做推箱子的時候,就用過A*演算法,還教過我,不過我當時並沒懂,最近又挨著學了一次,發現還是自己動手牢靠,記得比較清晰。當時的我的誤區在於,我以為close列表就是路徑,其實close列表僅僅代表“這個格子我已經檢查過啦,不必再檢查了”,真正的路徑是通過結束點,一步步獲取它的parent直到起始點(起始點的parent為空),這樣才是完整路徑。還需要注意的是,我們每次迴圈都會對open列表排序,選擇綜合代價最小的格子,作為本次迴圈的“CurrentCell”,所以我們必須為Cell物件實現排序介面,指定排序是通過對比F綜合代價。