A*尋路算法在Unity中的簡單應用

前言

在使用Unity開發游戲項目時,經常會遇到一些角色的導航需求,然而Unity提供給我們的NavMesh+NavMeshAgent并不能很好幫我們實現,且尋路網格的烘焙,以及在導航過程中尋路網格的檢測,都是非常消耗性能的,因此在很多企業項目中,會自己寫一下高效的尋路算法來完成需求,其中有一種就是A*尋路算法。A*尋路算法是一種啟發式算法,從所有可以行走的路徑中找出估量代價最小的,遞歸每步這樣的操作,最終到達目標點。

  • A尋路算法的估量代價*
    在A*算法中核心的尋路依據就是估量代價,在A*中通常用 F 表示。

F = G + H
其中G表示當前點到起始點的估量代價,H表示當前點到終點的代價。

  • G的計算方式:最開始,以起點為中心開始計算周邊的八個格子,然后在以這算出來的八個格子為中心,繼續計算他們周圍的八個格子的G值。以此類推,直到找到終端,或遍歷完所有的格子。G值的計算結果為:中心點的G值+距離值【10 or 14】

    FGH示意圖

                  圖中格子左下角為G值,右下角為H值,左上角為F值
    

因此從當前格子周邊的八個格子中找到下一步所走的格子,依據F值,當F值相同時隨機選擇。

當然在尋路過程中,會有障礙,不能通過,通過可以行走的格子走到終點。下圖中綠色為起點,紅色為終點,藍色是障礙,淺藍邊框是參與計算的格子,A*就是通過這樣的一系列計算完成的最優尋路。

A*行走路線
  • AStar尋路步驟

    1. 設置開放列表OpenList和關閉列表CloseList
    2. 將起點放置到OpenList
    3. 開啟循環While(OpenList.count > 0)
      3.1 將OpenList排序【F值從小到大】
      3.2 OpenList[0]必定是F值最小的,命名為Center
      3.2.1 發現Center就是終點,回溯找到導航路徑
      3.3 以這個點為中心,去發現它周圍的8個格子
      3.4 計算發現的每個格子G H F三個值
      3.5 如果這個格子沒有被計算過或原來G值,比這次計算出來的G要大
      3.5.1 此時,設置新的FGH值給這個格子,并設置該格子的發現者是Center
      3.6 如果這個格子被計算過,且原G值,比這次計算出來的G要小
      3.6.1 此時,就不能替換原來FGH值
      3.7 將發現的每個格子放入到OpenList
      3.7.1 放入的時候要做檢測【該格子不在OpenList、該格子不在CloseList】
      3.8 將此次循環的發現者Center放入到CloseList
      3.8 判斷OpenList空了
      3.8.1 說明所有的可發現的格子都被遍歷過了,始終沒有找到中,說明無法到達終點
下面我寫了一個小例子,方便大家學習。
  • 簡單效果


    簡單效果
  • 首先需要創建一個格子類Grid

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

/// <summary>
/// 格子類型
/// </summary>
public enum GridType
{
    //正常類型
    Normal,
    //障礙物類型
    Obstacle,
    //起點類型
    Start,
    //終點類型
    End
}

/// <summary>
/// 格子類(實現IComparable方便排序)
/// </summary>
public class Grid : IComparable
{
    //格子坐標x-y
    public int x;
    public int y;
    //格子A*三屬性f-g-h
    public int f;
    public int g;
    public int h;
    //格子類型
    public GridType type;
    //格子的歸屬(父格子)
    public Grid parent;
    //構造賦值
    public Grid (int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    /// <summary>
    /// 實現排序接口方法
    /// </summary>
    /// <returns>The to.</returns>
    /// <param name="obj">Object.</param>
    public int CompareTo (object obj)
    {
        Grid grid = (Grid)obj;
        if (this.f < grid.f) {
            //升序
            return -1;
        }
        if (this.f > grid.f) {
            //降序
            return 1;
        }
        return 0;
    }
}

  • 然后主邏輯AStar類
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MyAStar : MonoBehaviour
{
    /// <summary>
    /// 單例腳本
    /// </summary>
    public static MyAStar instance;

    //參考物體預設體
    public GameObject reference;
    //格子數組
    public Grid[,] grids;
    //格子數組對應的參考物(方塊)對象
    public GameObject[,] objs;
    //開啟列表
    public ArrayList openList;
    //關閉列表
    public ArrayList closeList;
    //目標點坐標
    public int targetX;
    public int targetY;
    //起始點坐標
    public int startX;
    public int startY;

    //格子行列數
    private int row;
    private int colomn;
    //結果棧
    private Stack<string> parentList;
    //基礎物體
    private Transform plane;
    private Transform start;
    private Transform end;
    private Transform obstacle;
    //流顏色參數
    private float alpha = 0;
    private float incrementPer = 0;

    void Awake ()
    {
        instance = this;
        plane = GameObject.Find ("Plane").transform;
        start = GameObject.Find ("Start").transform;
        end = GameObject.Find ("End").transform;
        obstacle = GameObject.Find ("Obstacle").transform;
        parentList = new Stack<string> ();
        openList = new ArrayList ();
        closeList = new ArrayList ();
    }

    /// <summary>
    /// 初始化操作
    /// </summary>
    void Init ()
    {
        //計算行列數
        int x = (int)(plane.localScale.x * 20);
        int y = (int)(plane.localScale.z * 20);
        row = x;
        colomn = y;
        grids = new Grid[x, y];
        objs = new GameObject[x, y];
        //起始坐標
        Vector3 startPos = 
            new Vector3 (plane.localScale.x * -5, 0, plane.localScale.z * -5);
        //生成參考物體(Cube)
        for (int i = 0; i < x; i++) {
            for (int j = 0; j < y; j++) {
                grids [i, j] = new Grid (i, j);
                GameObject item = (GameObject)Instantiate (reference, 
                                      new Vector3 (i * 0.5f, 0, j * 0.5f) + startPos, 
                                      Quaternion.identity);
                item.transform.GetChild (0).GetComponent<Reference> ().x = i;
                item.transform.GetChild (0).GetComponent<Reference> ().y = j;
                objs [i, j] = item;
            }
        }
    }

    /// <summary>
    /// A*計算
    /// </summary>
    IEnumerator Count ()
    {
        //等待前面操作完成
        yield return new WaitForSeconds (0.1f);
        //添加起始點
        openList.Add (grids [startX, startY]);
        //聲明當前格子變量,并賦初值
        Grid currentGrid = openList [0] as Grid;
        //循環遍歷路徑最小F的點
        while (openList.Count > 0 && currentGrid.type != GridType.End) {
            //獲取此時最小F點
            currentGrid = openList [0] as Grid;
            //如果當前點就是目標
            if (currentGrid.type == GridType.End) {
                Debug.Log ("Find");
                //生成結果
                GenerateResult (currentGrid);
            }
            //上下左右,左上左下,右上右下,遍歷
            for (int i = -1; i <= 1; i++) {
                for (int j = -1; j <= 1; j++) {
                    if (i != 0 || j != 0) {
                        //計算坐標
                        int x = currentGrid.x + i;
                        int y = currentGrid.y + j;
                        //如果未超出所有格子范圍,不是障礙物,不是重復點
                        if (x >= 0 && y >= 0 && x < row && y < colomn
                            && grids [x, y].type != GridType.Obstacle
                            && !closeList.Contains (grids [x, y])) {
                            //計算G值
                            int g = currentGrid.g + (int)(Mathf.Sqrt ((Mathf.Abs (i) + Mathf.Abs (j))) * 10);
                            //與原G值對照
                            if (grids [x, y].g == 0 || grids [x, y].g > g) {
                                //更新G值
                                grids [x, y].g = g;
                                //更新父格子
                                grids [x, y].parent = currentGrid;
                            }
                            //計算H值
                            grids [x, y].h = Manhattan (x, y);
                            //計算F值
                            grids [x, y].f = grids [x, y].g + grids [x, y].h;
                            //如果未添加到開啟列表
                            if (!openList.Contains (grids [x, y])) {
                                //添加
                                openList.Add (grids [x, y]);
                            }
                            //重新排序
                            openList.Sort ();
                        }
                    }
                }
            }
            //完成遍歷添加該點到關閉列表
            closeList.Add (currentGrid);
            //從開啟列表中移除
            openList.Remove (currentGrid);
            //如果開啟列表空,未能找到路徑
            if (openList.Count == 0) {
                Debug.Log ("Can not Find");
            }
        }


    }

    /// <summary>
    /// 生成結果
    /// </summary>
    /// <param name="currentGrid">Current grid.</param>
    void GenerateResult (Grid currentGrid)
    {
        //如果當前格子有父格子
        if (currentGrid.parent != null) {
            //添加到父對象棧(即結果棧)
            parentList.Push (currentGrid.x + "|" + currentGrid.y);
            //遞歸獲取
            GenerateResult (currentGrid.parent);
        }
    }

    /// <summary>
    /// 顯示結果
    /// </summary>
    /// <returns>The result.</returns>
    IEnumerator ShowResult ()
    {
        //等待前面計算完成
        yield return new WaitForSeconds (0.3f);
        //計算每幀顏色值增量
        incrementPer = 1 / (float)parentList.Count;
        //展示結果
        while (parentList.Count != 0) {
            //出棧
            string str = parentList.Pop ();
            //等0.3秒
            yield return new WaitForSeconds (0.3f);
            //拆分獲取坐標
            string[] xy = str.Split (new char[]{ '|' });
            int x = int.Parse (xy [0]);
            int y = int.Parse (xy [1]);
            //當前顏色值
            alpha += incrementPer;
            //以顏色方式繪制路徑
            objs [x, y].transform.GetChild (0).GetComponent<MeshRenderer> ().material.color
            = new Color (1 - alpha, alpha, 0, 1);
        }
    }

    /// <summary>
    /// 曼哈頓方式計算H值
    /// </summary>
    /// <param name="x">The x coordinate.</param>
    /// <param name="y">The y coordinate.</param>
    int Manhattan (int x, int y)
    {
        return (int)(Mathf.Abs (targetX - x) + Mathf.Abs (targetY - y)) * 10;
    }

    void Start ()
    {
        Init ();
        StartCoroutine (Count ());
        StartCoroutine (ShowResult ());
    }
}
  • 最后是參考預設體方塊的簡單實現
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class Reference : MonoBehaviour
{
    //顏色材質區分
    public Material startMat;
    public Material endMat;
    public Material obstacleMat;
    //顯示信息Text
    private Text text;
    //當前格子坐標
    public int x;
    public int y;

    void Awake ()
    {
        text = GameObject.Find ("Text").GetComponent<Text> ();
    }
    //判斷當前格子的類型
    void OnTriggerEnter (Collider other)
    {
        if (other.name == "Start") {
            GetComponent<MeshRenderer> ().material = startMat;
            MyAStar.instance.grids [x, y].type = GridType.Start;
            MyAStar.instance.openList.Add (MyAStar.instance.grids [x, y]);
            MyAStar.instance.startX = x;
            MyAStar.instance.startY = y;
        } else if (other.name == "End") {
            GetComponent<MeshRenderer> ().material = endMat;
            MyAStar.instance.grids [x, y].type = GridType.End;
            MyAStar.instance.targetX = x;
            MyAStar.instance.targetY = y;
        } else if (other.name == "Obstacle") {
            GetComponent<MeshRenderer> ().material = obstacleMat;
            MyAStar.instance.grids [x, y].type = GridType.Obstacle;
        }
    }

    /// <summary>
    /// 鼠標點擊顯示當前格子基礎信息
    /// </summary>
    void OnMouseDown ()
    {
        text.text = "XY(" + x + "," + y + ")" + "\n" +
        "FGH(" + MyAStar.instance.grids [x, y].f + "," +
        MyAStar.instance.grids [x, y].g + "," +
        MyAStar.instance.grids [x, y].h + ")";
        text.color = GetComponent<MeshRenderer> ().material.color;
    }
}
  • 多障礙效果圖


    多障礙效果圖
  • 遍歷流程監測
    其實A*遍歷的格子還是蠻多的,因為曼哈頓計算的H值是不考慮障礙物的,所以會有很多格子的F值會很小,但不一定此時很小的F值格子就是要走的路徑,最終的最優路徑是通過,終點格子反推回來的,就如代碼中GenerateResult遞歸方法所示,為了方便大家理解,我做了一個小動畫,方便大家對A*的理解。(粉色是此時F值最小的格子,藍色是最小F值格子周邊正在遍歷的格子,黃色格子是從未設置父物體,第一次被遍歷的格子)


    遍歷流程監測

    慢放版

結束語

A*只是游戲算法中的鳳毛麟角,其中的邏輯也相對簡單,所以想要提升編碼質量,想要寫出高效的游戲邏輯,還需要更多的算法學習。還是那個道理,編程 = 數據結構 + 算法,不帶班的這段時間我會盡量分享一些東西給大家,同仁們加油。本文項目下載鏈接:http://pan.baidu.com/s/1hs13F8K 密碼: rbs1

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,791評論 6 545
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,795評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,943評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,057評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,773評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,106評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,082評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,282評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,793評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,507評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,741評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,220評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,929評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,325評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,661評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,482評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,702評論 2 380

推薦閱讀更多精彩內容