1. 程式人生 > >unity3D 簡單實現A*演算法

unity3D 簡單實現A*演算法

前言

在兩個物體之間尋找最短有效路徑

  1. 學習A*思想
  2. 其他演算法,計算機估計距離
  3. 曼哈頓(城市街區演算法、直來直去)
  4. 幾何法(對角線,勾股定理)
  5. 估演算法(先對角線,再直線)本文使用估演算法計算距離
  6. GHF
  7. G : 從開始到當前位置的移動步數
  8. H : 從當前位置到目標位置的距離,使用上面3種演算法獲得
  9. F :G + H 的總和
  10. OpenList 和 CloseList
  11. OpenList :被考慮最短有效路徑的位置集合
  12. CloseList : 不被考慮最短語效路徑的位置集合
  13. 尋路思想:知道F = G + H 且知道如何計算得出,知道OpenList 和CloseList並知道如何得出

F G H 分別表示什麼,有什麼用,如何計算機它們?

  1. 首先得G表示的是從起始結點走到下一個結點(下下…下個)的距離,因此這個G是每走一格就會疊加一次並賦值給新的結點,從而累計得到從當前結點到下下下..個結點的距離
    newCostg = gCost + GetDistance()
    nodeCell.gCost = newCostg; //移動步數
  2. 其次是H,它表示從當前結點到終點結點的最短距離
  3. F 就是 G + H 得到的估算值

再根據百度百科的解釋做個比較或許更容易理解:

f(n)=g(n)+h(n)
f(n) 是從初始狀態經由狀態n到目標狀態的代價估計,
g(n) 是在狀態空間中從初始狀態到狀態n的實際代價,
h(n) 是從狀態n到目標狀態的最佳路徑的估計代價。
(對於路徑搜尋問題,狀態就是圖中的節點,代價就是距離)

這裡需要寫一個兩點之間的距離估算方法,來計算得出G H 從而得出F
注意最好避免小數計算,
因為計算機計算機浮點的效率較低
而這裡的x表示長 y表示高 可以通過畫圖理解,x = y 就是正方形,兩點距離就是1.4 ,避免小數計算乘10 就是14整數了

    //計算距離 ,這裡是因為NodeItem是巢狀類,在Grid裡面 
    public int GetDistance(Grid.NodeItem a,Grid.NodeItem b) {
        //估演算法
        int x = Mathf.Abs (a.x - b.x);
        int y = Mathf.Abs (a.y - b.y);
        //根號2 = 1.4  然後都擴大10倍 去除小數計算,這裡返回值都放大了10倍
if (x > y) { return 14 * y + 10 * (x - y); }else{ return 14 * x + 10 * (y - x); } //也可以用曼哈頓得到距離 //return Mathf.Abs (a.x-b.x)+Mathf.Abs(a.y-b.y);//得到正數,但是可能是浮點數 }

OpenList和CloseList是什麼,可以用來做什麼?

OpenList :被考慮最短有效路徑的位置集合
CloseList : 不被考慮最短語效路徑的位置集合

1、從第一個結點開始,放入OpenList容器,然後這個結點獲取全部周圍符合條件(1)的結點;
2、把符合條件的結點全部放入OpenList容器;
3、並且還要對周圍符合條件(2)的每個結點進行更新他們的G值和H值,記錄這個當前結點為它們的父結點;
4、然後把這第一個結點放入CloseList容器裡 這是第一次迴圈

5、隨後進入第二次迴圈,
此時就從OpenList容器找到符合條件(3)的當前結點,
當前結點是要在OpenList容器中H值最小,且F值最小的;
6、找到後就獲取這個當前結點的周圍結點,然後重新進入第2步驟到第5步驟;
7、此時它會慢慢接近終點,直到發現當前結點==最終結點;
8、最終結點找到後根據最終結點的父結點一個一個返回(類似資料結構的連結串列)找到開始結點;
9、此時用一個容器(List容器、Stack容器等)來接收這些結點,這些結點的Position就是最短路徑

介紹符合條件:
第一個符合條件(1)是對周圍結點的判斷,該結點如果是標有牆類標記、或不可走標記就是不符合條件的,
第二個符合條件(2)是在滿足第一個條件的前提下,在CloseList容器下遍歷過,或更新後的G值比原來G值還要大的結點,2者不滿足一個就是不滿足條件,不能更新該結點的G,H值
第三個符合條件(3)就是儲存在OpenList容器中的結點中,找一個H值最小,且, F值最小的結點,找到這個H值且F值最小的結點後,就把這個結點設定為當前結點。

當前結點在這裡指為離終點結點的最短距離估計點。這個點其實並不會太智慧,一下就找到最短路徑。
而是根據OpenList裡的全部結點來判斷找到離終點最短估計值最小的結點,讓周圍的結點加入OpenList。然後就放入CloseList容器這個最短估計值最小的結點以後不再做任何判斷。

CS指令碼程式碼
結點類
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AStarNode {

    public bool isWall;
    public int posX;
    public int posY;
    public int posZ;

    public Vector3 pos;
    public AStarNode parentNode;

    public int costG;
    public int costH;

    public int CostF{
        get{ return costG + costH; }
    }

    public AStarNode(bool _isWall,Vector3 _pos,int _z,int _x){
        this.isWall = _isWall;
        this.pos = _pos;
        this.posX = _x;
        this.posZ = _z;
    }
}
結點地圖生成類
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AStarMAP : MonoBehaviour {

    public AStarNode [,] AllNodeGroup; 

    public LayerMask wallLayer;
    public int xWidth;
    public int zHeight;
    private float nodeRange = 0.4f;

    public GameObject nodeWallPrefabs;

    void Awake(){
        Init();
    }

    void Init(){

        zHeight = 40;
        xWidth = 40;
        AllNodeGroup = new AStarNode[zHeight, xWidth];
        //初始化所有結點,把結點node存入二維數組裡
        for (int i = 0; i < zHeight; i++) {
            for (int j = 0; j < xWidth; j++) {

                Vector3 nodePos =new Vector3 (j,0,i);
                nodePos += new Vector3 (0.5f, 0, 0.5f); 
                bool isWall = Physics.CheckSphere (nodePos, nodeRange, wallLayer);
                AStarNode nd = new AStarNode (isWall, nodePos,i,j);

                if (isWall) {
                    GameObject obj = GameObject.Instantiate (nodeWallPrefabs, nodePos, Quaternion.identity) as GameObject;

                }
                AllNodeGroup[i,j] = nd;

            }
        }
    }

    public List<AStarNode> GetAroundNodes(AStarNode curNode){

        List<AStarNode> retGroup = new List<AStarNode> ();
        for (int i = -1; i <= 1; i++) {
            for (int j = -1; j <= 1; j++) {
                if (i == 0 && j == 0) {
                    continue;
                }
                int z = curNode.posZ + i;
                int x = curNode.posX + j;

                if (x >= 0 && x < xWidth && z >= 0 && z < zHeight) {
                    retGroup.Add (AllNodeGroup [z, x]);
                }
            }
        }
        return retGroup;

    }

    public AStarNode GetItem(Vector3 pos){
        int x = Mathf.RoundToInt (pos.x - 0.5f);
        int z = Mathf.RoundToInt (pos.z - 0.5f);
        x = Mathf.Clamp (x, 0, xWidth - 1);
        z = Mathf.Clamp (z, 0, zHeight - 1);
        return AllNodeGroup [z, x];
    }
}
找最短路徑類
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AStarFindPath : MonoBehaviour {

    private AStarMAP aStarMap;

    public List<Vector3> peoplePath;
    public List<AStarNode> peopleNodePath;

    void Start(){
        aStarMap = GetComponent<AStarMAP> ();
    }

    private int GetDistance(AStarNode startNode,AStarNode endNode){
        int x = Mathf.Abs (startNode.posX - endNode.posX);
        int z = Mathf.Abs (startNode.posZ - endNode.posZ);
        if (x > z) {
            return 10 * (x - z) + 14 * z;
        } else {
            return 10 * (z - x) + 14 * x;
        }
    }


    //根據開始和結束點來查詢最優路徑
    private void toFindPath(Vector3 startPos,Vector3 endPos) {

        //根據位置獲取到NodeItem
        AStarNode start = aStarMap.GetItem (startPos);
        AStarNode end = aStarMap.GetItem (endPos);

        List<AStarNode> openList = new List<AStarNode> ();
        List<AStarNode> closeList = new List<AStarNode> ();

        openList.Add (start);
        //從開始點開始判斷
        while (openList.Count > 0) {
            AStarNode curNode = openList [0];
            for (int i = 0; i < openList.Count; i++) {
                //h是估演算法的距離  f是估演算法加實際格子的距離g
                if (openList [i].CostF < curNode.CostF && openList [i].costH < curNode.costH) {
                    curNode = openList [i];
                }
            }

            openList.Remove (curNode);
            closeList.Add (curNode);

            //已經找到結束點
            if (curNode == end) {
                Debug.Log (">>");
                GetPathWithPos (startPos, endPos);
                return;
            }
            // 獲取當前點的周圍點
            List<AStarNode> nodeItemGroup =  aStarMap.GetAroundNodes(curNode);

            //遍歷當前點周圍的NodeItem 對其進行
            foreach (AStarNode nodeCell in nodeItemGroup) {
                //先過濾: 牆 closeList
                if (nodeCell.isWall || closeList.Contains (nodeCell)) {
                    continue;
                }

                //計算 G H F 進行賦值
                int newCostg = curNode.costG + GetDistance (curNode, nodeCell);

                if (newCostg <= nodeCell.costG || !openList.Contains (nodeCell)) {
                    //重新整理g h
                    nodeCell.costG = newCostg; //移動步數距離
                    nodeCell.costH = GetDistance (nodeCell, end);//到該結點到終點的距離
                    //設定中心點為父親
                    nodeCell.parentNode = curNode;
                    if (!openList.Contains (nodeCell)) {
                        openList.Add (nodeCell);
                    }
                }
            }
        }
    }

    //獲取路徑
    private void GetPathWithPos(Vector3 startPos,Vector3 endNodePos){
        //此處可以優化GC記憶體
        peopleNodePath = new List<AStarNode> ();
        peoplePath = new List<Vector3> ();

        AStarNode endNode = aStarMap.GetItem (endNodePos);
        AStarNode startNode = aStarMap.GetItem (startPos);

        if (endNode != null) {

            AStarNode temp = endNode;

            while (temp != startNode) {
                peoplePath.Add (temp.pos);
                peopleNodePath.Add (temp);
                temp = temp.parentNode;
            }
            peopleNodePath.Reverse ();
            peoplePath.Reverse ();
        }
    }
    //提供給人物移動類使用
    public List<Vector3> PeopleGoTo(Vector3 startpos,Vector3 endpos){
        toFindPath (startpos, endpos);
        return peoplePath;
    }

    public List<AStarNode> PeopleGoToWithNode(Vector3 startpos,Vector3 endpos){
        toFindPath (startpos, endpos);
        return peopleNodePath;
    }
}
人物移動實現類

這裡是我以前實現了一個有點簡單AI的A*移動,現在看起來有點醜,,還可以做優化,
實現的時候也可以發現:
A*演算法可以做出介面來用的,不過需要根據需求情況設定一些值。
開始結點,最終點結點的位置(可自動獲取開始結點位置,滑鼠點選獲取終點結點位置)
生成地圖的大小,結點的大小,地圖、結點座標設計需要考慮
找到後的路徑結點容器該怎麼優化,實現是也是要考慮的

這裡我使用了協程來寫。

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

public class PlayerTest : MonoBehaviour {

    AStarFindPath astarFindPath;
    public Vector3 endPos = new Vector3(12.5f,0,0.5f);
    RaycastHit hit;
    Ray ray;

    AStarNode currentNode;
    public bool isGO = true;
    List<AStarNode> pathGroupNode;
    void Awake(){
        astarFindPath = GetComponent<AStarFindPath> ();
    }
    void Start () {
        pathGroupNode = new List<AStarNode> ();
    }
    void Update(){
        peopleCtrl ();
    }
    void peopleCtrl(){
        if (Input.GetMouseButtonDown (0)) {
            ray = Camera.main.ScreenPointToRay (Input.mousePosition);
            if (Physics.Raycast (ray, out hit)) {
                endPos = hit.point;
                pathGroupNode.Clear ();
                i = 1;
                //執行A*演算法
                pathGroupNode = astarFindPath.PeopleGoToWithNode (transform.position, endPos);
                if (pathGroupNode.Count <= 1) {
                    Debug.Log ("不需要走");
                    return;
                }
                currentNode = pathGroupNode [1];
                StopAllCoroutines ();
                StartCoroutine (pahtGo ());

            } else {
                Debug.Log ("重新點選");
            }
        }
    }
    IEnumerator pahtGo(){
        while (true) {
            if (Vector3.Distance (transform.position, currentNode.pos) <= 0.5f) {
                currentNode = pathGroupNode[i++];
                if (currentNode == null) {
                    Debug.Log ("到終點了...");
                    yield break;
                }   
                if (i >= pathGroupNode.Count) {
                    Debug.Log ("到達目的地");
                    yield break;
                }
            } else {
                transform.LookAt (currentNode.pos);
                transform.Translate (Vector3.forward * 2f * Time.deltaTime, Space.Self);
            }
            yield return new WaitForEndOfFrame ();
        }
    }
}