基於Unity的A星尋路演算法(絕對簡單完整版本)

心之凌兒發表於2021-08-12

前言

在上一篇文章,介紹了網格地圖的實現方式,基於該文章,我們來實現一個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("網格地圖模板預製體")]
    public GameObject gridPre;
    [Header("網格地圖模板大小")]
    public Vector2 scale;


    private GameObject[,] m_grids;
    public GameObject[,] grids
    {
        get
        {
            return m_grids;
        }
    }
    //註冊模板事件
    public Action<GameObject, int, int> gridEvent;
    /// <summary>
    /// 基於掛載元件的初始資料建立網格
    /// </summary>
    public void CreateMesh()
    {
        if (meshRange.horizontal == 0 || meshRange.vertical == 0)
        {
            return;
        }
        ClearMesh();
        m_grids = new GameObject[meshRange.horizontal, meshRange.vertical];
        for (int i = 0; i < meshRange.horizontal; i++)
        {
            for (int j = 0; j < meshRange.vertical; j++)
            {
                CreateGrid(i, j);

            }
        }
    }

    /// <summary>
    /// 過載,基於傳入寬高資料來建立網格
    /// </summary>
    /// <param name="height"></param>
    /// <param name="widght"></param>
    public void CreateMesh(int height, int widght)
    {
        if (widght == 0 || height == 0)
        {
            return;
        }
        ClearMesh();
        m_grids = new GameObject[widght, height];
        for (int i = 0; i < widght; i++)
        {
            for (int j = 0; j < height; j++)
            {
                CreateGrid(i, j);

            }
        }
    }

    /// <summary>
    /// 根據位置建立一個基本的Grid物體
    /// </summary>
    /// <param name="row">x軸座標</param>
    /// <param name="column">y軸座標</param>
    public void CreateGrid(int row, int column)
    {
        GameObject go = GameObject.Instantiate(gridPre, parentTran);
        //T grid = go.GetComponent<T>();

        float posX = startPos.x + scale.x * row;
        float posZ = startPos.z + scale.y * column;
        go.transform.position = new Vector3(posX, startPos.y, posZ);
        go.SetActive(true);
        m_grids[row, column] = go;
        gridEvent?.Invoke(go, row, column);
    }
    /// <summary>
    /// 刪除網格地圖,並清除快取資料
    /// </summary>
    public void ClearMesh()
    {
        if (m_grids == null || m_grids.Length == 0)
        {
            return;
        }
        foreach (GameObject go in m_grids)
        {
            if (go != null)
            {
                Destroy(go);
            }
        }
        Array.Clear(m_grids, 0, m_grids.Length);
    }
}

3、實現尋路的過程:

建立一個指令碼命名為AStarLookRode 作為尋路的指令碼

變數定義:

在正式的邏輯程式碼開始前,需要定義一些基本的變數:

  • 開放列表:儲存所有下一步可移動的格子
  • 封閉列表:儲存所有移動過的格子
  • 路徑棧:儲存最終尋路的路徑格子
  • 起始點
  • 終點
  • 場景中的網格地圖

完成變數的定義後,需要在尋路程式開始,對一些變數進行賦值,同時初始化列表,所以我們定義一個初始化的方法:

    public GridMeshCreate meshMap;
    public Grid startGrid;
    public Grid endGrid;

    public List<Grid> openGrids;
    public List<Grid> closeGrids;
    public Stack<Grid> rodes;

    public void Init(GridMeshCreate meshMap, Grid startGrid, Grid endGrid)
    {
        this.meshMap = meshMap;
        this.startGrid = startGrid;
        this.endGrid = endGrid;
        openGrids = new List<Grid>();
        closeGrids = new List<Grid>();
        rodes = new Stack<Grid>();
    }

新增路徑點周圍格子至開放列表:

接下來進行一個功能的程式碼邏輯設計,如何將一個點周圍的格子加入到開放列表。可以觀察場景中的格子,有下面的兩種情況:

  • 位於地圖中心:周圍有八個可以移動的格子:
  • 位於地圖的邊緣:左邊或者右邊,上邊或者下邊沒有格子

這就需要我們從中找到可以取值的範圍,由於格子的位置資訊是一個二維座標,XY,單純的從X軸來考慮,X-1會是格子左邊的格子的座標,但是如果X-1<0則說明其左邊沒有格子,基於這樣的計算方式,來求得當前格子item周圍格子的座標範圍,並剔除一些不需要新增的格子,具體的選擇步驟為:

  • 遍歷周圍的格子grid,如果存在於封閉列表closeGrids內,不處理
  • 如果格子在開放列表openGrids中,計算該點位到目前尋路位置點的期望路徑長度,如果長度更短的話,將當前格子item的父物體替換為該格子的grid
  • 接下來如果grid既不在開放列表openGrids,也不再閉合列表closeGrids內,若判斷不為障礙物,則將其加入開放列表openGrids,並設定其父物體為當前尋路位置item

簡單的從圖中理解:
在這裡插入圖片描述
假定我們現在走到了A點(A代表當前路徑點Item),那麼新增其周圍的格子(用grid代表)範圍限定在紅色框,為了便於區分不同的情況,我做了一些簡單的標識:

  • 紅色格子:障礙物,不處理
  • 綠色格子:已經走過的路徑,在閉合列表closeList內,不處理
  • 標有圓形的格子,未執行過任何操作,新增到openList裡面
  • 橙色小框C:最需要理解的一個格子,首先要明白,該格子已經被其上面的綠色格子遍歷過,簡單的來說是已經在開放列表內,這個時候我們就要判斷A點如果經過C點過來,路徑會不會更短,如果會,則修改該A點的父元素為C點,否則不處理
    public void TraverseItem(int i, int j)
    {
        int xMin = Mathf.Max(i - 1, 0);
        int xMax = Mathf.Min(i + 1, meshMap.meshRange.horizontal - 1);
        int yMin = Mathf.Max(j - 1, 0);
        int yMax = Mathf.Min(j + 1, meshMap.meshRange.vertical - 1);

        Grid item = meshMap.grids[i, j].GetComponent<Grid>();
        for (int x = xMin; x <= xMax; x++)
        {
            for (int y = yMin; y <= yMax; y++)
            {
                Grid grid = meshMap.grids[x, y].GetComponent<Grid>();
                if ((y == j && i == x) || closeGrids.Contains(grid))
                {
                    continue;
                }
                if (openGrids.Contains(grid))
                {
                    if(item.All > GetLength(grid, item))
                    {
                        item.parentGrid = grid;
                        SetNoteData(item);
                    }  
                    continue;
                }                    
                if (!grid.isHinder)
                {
                    openGrids.Add(grid);
                    grid.parentGrid= item;
                }               
            }
        }
    }

求任一格子的期望路徑長度:

接下來就需要計算出一個格子的期望路徑的長度,要基於的父元素的G來計算出該格子的G,同時預估出來該格子到達目標的距離H,計算方式在原理裡面已經介紹過,這裡直接貼出程式碼的執行方式:

    public int SetNoteData(Grid grid)
    {
        Grid itemParent = rodes.Count == 0 ? startGrid : grid.parentGrid;
        int numG = Mathf.Abs(itemParent.posX - grid.posX) + Mathf.Abs(itemParent.posY - grid.posY);
        int n = numG == 1 ? 10 : 14;
        grid.G = itemParent.G + n;

        int numH = Mathf.Abs(endGrid.posX - grid.posX) + Mathf.Abs(endGrid.posY - grid.posY);
        grid.H = numH * 10;
        grid.All = grid.H + grid.G;
        return grid.All;
    }

在前面的程式碼中,有一個開放列表中已經存在,對比期望長度的更改父格子的功能功能。用到了求根據一個格子求下一個格子期望路徑長度的功能。雖然與上面的程式碼功能類似,但是不能直接使用,提升通用性修改起來又麻煩,所以直接再寫一個:

    public int GetLength(Grid bejinGrid,Grid grid)
    {
        int numG = Mathf.Abs(bejinGrid.posX - grid.posX) + Mathf.Abs(bejinGrid.posY - grid.posY);
        int n = numG == 1 ? 10 : 14;
        int G = bejinGrid.G + n;
        
        int numH = Mathf.Abs(endGrid.posX - grid.posX) + Mathf.Abs(endGrid.posY - grid.posY);
        int H = numH * 10;
        int All = grid.H + grid.G;
        return All;
    }

開放列表中尋找期望路徑最短的格子:

在完成對於一個格子的期望路徑長度的計算,我們就需要從開放列表中找出期望路徑長度最短的路徑加入到路徑棧中

但是在這一步有這樣的一個問題,在原理介紹中也有說到,尋路過程中遇到障礙會進行回溯到之前的某一個路徑點,如果在在棧中執行這樣的操作呢

這裡就要用到格子模板Grid中儲存的父格子的資訊,通過對比棧中的資料,查詢到父格子的位置,清除後面的資料,並將該格子插入,具體程式碼為:

     /// <summary>
    /// 在開放列表選中路徑最短的點加入的路徑棧,同時將路徑點加入到閉合列表中
    /// </summary>
    public void Traverse()
    {
        if (openGrids.Count == 0)
        {
            return;
        }
        Grid minLenthGrid = openGrids[0];
        int minLength = SetNoteData(minLenthGrid);
        for (int i = 0; i < openGrids.Count; i++)
        {
            if (minLength > SetNoteData(openGrids[i]))
            {
                minLenthGrid = openGrids[i];
                minLength = SetNoteData(openGrids[i]);
            }
        }
        minLenthGrid.ChangeColor(Color.green);
        Debug.Log("我在尋找人生的方向" + minLenthGrid.posX + "::::" + minLenthGrid.posY);

        closeGrids.Add(minLenthGrid);
        openGrids.Remove(minLenthGrid);               
        rodes.Push(minLenthGrid);
    }

獲取最終路徑:

在完成尋路的步驟後,需要根據路徑棧和格子的父物體來找到最短的路徑,這裡比較功能邏輯比較清晰,直接貼程式碼:

    void GetRode()
    {
        List<Grid> grids = new List<Grid>();
        rodes.Peek().ChangeColor(Color.black);
        grids.Insert(0, rodes.Pop());
        while (rodes.Count != 0)
        {
            if (grids[0].parentGrid != rodes.Peek())
            {
                rodes.Pop();

            }
            else
            {
                rodes.Peek().ChangeColor(Color.black);
                grids.Insert(0, rodes.Pop());               
            }

        }      
    }

封裝方法,對外暴露:

在解決三個關鍵功能的程式碼後,就需要通過一個方法來完成整個尋路的過程,在方法的最後需要通過對終點座標與棧頂物體的座標進行對比,如果相同,則跳出迴圈,執行路徑查詢完成後的操作

同時為了在本案例中為了使得整個尋路過程的步驟視覺化,使用一個協程來完成尋路過程的方法呼叫,這樣,在每一次完成一格的尋路後,可以通過協程來延時執行下一次迴圈:

    public IEnumerator OnStart()
    {

        //Item itemRoot = Map.bolls[0].item;
        rodes.Push(startGrid);
        closeGrids.Add(startGrid);

        TraverseItem(startGrid.posX, startGrid.posY);
        yield return new WaitForSeconds(1);
        Traverse();

        //為了避免無法完成尋路而跳不出迴圈的情況,使用For來規定尋路的最大步數
        for (int i = 0; i < 6000; i++)
        {
            if (rodes.Peek().posX == endGrid.posX && rodes.Peek().posY == endGrid.posY)
            {
                GetRode();
                break;
            }
            TraverseItem(rodes.Peek().posX, rodes.Peek().posY);
            yield return new WaitForSeconds(0.2f);
            Traverse();
        }
    }

四、 執行程式碼

接下來需要建立一個指令碼明命名為MainRun 來執行整個專案,主要部分為建立場景的網格地圖,在前面反覆提到的文章裡面已經有這一部分的介紹。接下來就需要對A星的呼叫:

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

public class MainRun : MonoBehaviour
{
    //獲取網格建立指令碼
    public GridMeshCreate gridMeshCreate;
    //控制網格元素grid是障礙的概率
    [Range(0,1)]
    public float probability;
    bool isCreateMap=false;
    int clickNum=0;
    Grid startGrid;
    Grid endGrid;
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Run();
            isCreateMap = false;
            clickNum = 0;
        }
        if (Input.GetKeyDown(KeyCode.Q))
        {
            AStarLookRode aStarLookRode = new AStarLookRode();
            aStarLookRode.Init(gridMeshCreate,startGrid,endGrid);
            StartCoroutine(aStarLookRode.OnStart());          
        }
    }
    private void Run()
    {        
        gridMeshCreate.gridEvent = GridEvent;
        gridMeshCreate.CreateMesh();
    }

    /// <summary>
    /// 建立grid時執行的方法,通過委託傳入
    /// </summary>
    /// <param name="grid"></param>
    private void GridEvent(GameObject go,int row,int column)
    {
        //概率隨機決定該元素是否為障礙
        Grid grid = go.GetComponent<Grid>();
        float f = Random.Range(0, 1.0f);
        Color color = f <= probability ? Color.red : Color.white;
        grid.ChangeColor(color);
        grid.isHinder = f <= probability;
        grid.posX = row;
        grid.posY = column;
        //模板元素點選事件
        grid.OnClick = () => {
            if (grid.isHinder)
                return;
            clickNum++;
            switch (clickNum)
            {
                case 1:
                    startGrid = grid;
                    grid.ChangeColor(Color.yellow);
                    break;
                case 2:
                    endGrid = grid;
                    grid.ChangeColor(Color.yellow);
                    isCreateMap = true;
                    break;
                default:
                    break;
            }

        };

    }
}

在該指令碼中,主要是用來執行網格地圖建立的方法的,同時寫入A星指令碼的執行介面。

場景執行:

建立一個空物體,並掛載網格地圖建立指令碼GridMeshCreate與執行指令碼MainRun,然後對這兩個指令碼進行賦值:
在這裡插入圖片描述
在兩個指令碼中,我們可以控制一些變數來改變網建立網格地圖大小與障礙物的佔比:

  • MainRunProbability:用來控制地圖中障礙物的數量佔比
  • GridMeshCreateMesh Range:用來控制網格地圖的大小範圍

在完成上面的指令碼掛載與設定後,就可以執行遊戲,進入遊戲場景後,點選空格即可建立地圖,

在建立地圖後,可以使用滑鼠點選地圖中的白色格子,第一次點選表示選擇了路徑的起始點,而第二次點選表示選擇了目標點格子

注意:

  • 這一塊Bug挺多的,我也沒有修改,所以儘量按著提示來,不要非要點選障礙物,或者非要在場景中點選三次

在完成對於兩個關鍵節點的選擇後,就可以點選Q鍵開始執行尋路過程,然後就可以直接觀察整個場景中的執行流程:
請新增圖片描述

4、關於A星尋路的能效問題

演算法複雜度問題:

第一張圖片:障礙物的比例比較低時,尋找的路徑接近於一條直線,同時沒有多餘的尋路節點產生:
在這裡插入圖片描述
當地圖複雜度上升後,A星尋路產生巨大的代價才能獲取最後的路徑,而這些代價產生的原因是由於為了獲取最短的路徑而進行大量的回溯,而回溯又進一步造成了遍歷列表長度的增加,進一步的消耗了計算資源。

所以當地圖複雜度到達一定閾值並再次上升後,尋路的代價會急速的上升,也可以簡單的理解為指數的形式,而當這一數值超過了0.5,地圖基本就處於不可用的狀態,會有大量的死衚衕,很大概率造成無路可循。

特殊情況的尋路效果:

話不多說,先看圖:
請新增圖片描述

通過上圖可以看出,雖然場景中的網格地圖很簡單,但是當兩個尋路點之間存在比較大的橫截面時,也同樣會付出巨大的尋路代價

擴充套件:

  • 看到這張圖,你知道Unity官方的NavMesh是如何實現尋路的嗎?

當我們使用NavMesh來執行尋路操作時,會事先對場景進行烘培,如果你曾經觀察過這張烘培地圖,就會發現其是由一張張三角面來構成的,而當我們進入遊戲,執行尋路操作時,NavMesh就會根據這些三角面的頂點來執行可移動的路徑計算。

在這裡插入圖片描述

如圖,其實NavMesh的優勢在與烘培階段對於地圖障礙的處理,通過一些頂點來大大簡化了尋路時的計算。

如果你先學習NavMesh 的使用方式:

總結

總的來說,A星是目前應用最廣的尋路方式,其特點簡單明瞭,整個過程以最短路徑為設計準則,逐漸的接近目標點。

但是要注意,A星雖然一直以最短為驅動,但是最終得到的路徑不一定最短(至少本篇文章的案例是這樣)。至於原因,你如果理解了程式碼的實現過程應該也能明白,如果你不理解,知道原因也沒意義,嘿嘿!

相關文章