1. 程式人生 > 實用技巧 >unity A星尋路教程

unity A星尋路教程

A星尋路演算法是什麼#

遊戲開發中往往有這樣的需求,讓玩家控制的角色自動尋路到目標地點,或是讓AI角色移動到目標位置,實際的情況可能很複雜,比如地圖上有無法通過的障礙或者需要付出代價(時間或其他資源)才能通過的河流、沼澤等,想要讓角色找到一條付出最小代價到達目標的路徑,就需要使用一些特殊的演算法,而A星尋路演算法就是目前應用最廣泛的尋路演算法之一,unity asset store上廣受好評的A* Pathfinding project外掛也是基於A星尋路演算法實現的,簡單來說:A星演算法是一種尋找最短路徑並避開障礙物的演算法。

A星演算法的基本概念#

要實現A星演算法,首先需要將紛繁複雜的遊戲地圖抽象成尋路網格,最簡單的方式是將遊戲地圖劃分為多個正方形單元或正多邊形單元,也可以劃分為非均勻的凸多邊形,這些網格可以看做是一個個“尋路點”,網格越精細,尋路的效果越好,但計算量也越大,所以針對實際的遊戲環境,需要好好平衡一下效能和效果。

A星演算法的基本思想就是藉助這些網格實現尋路,從起點開始遍歷四周的點,尋找最有可能在最短路徑上的點,並以這個點為基準繼續向四周遍歷,直至遍歷到終點,路徑也就找到了。
通過這個思想也可以看出,A星演算法其實只能得到一種近似最優解,實際上對於尋路問題,往往存在不止一個最優解,如果非要找出所有的解就只能遍歷所有可能的路徑一一比較,但這樣效率太低,所以A星演算法並不去遍歷整個地圖,而是隻遍歷了最短路徑上的點和其周圍的點,所以得到的是一種近似最優解。
那麼遍歷周圍的點時怎樣確定哪個點最有可能在最短路徑上呢?這就是A星演算法的核心:F=G+H
每個尋路點都有F、G、H這三個屬性,F可以理解為通過這個點的總代價,代價越低,這個點當然就更有可能在最短路徑上。G是從起點到這個點的代價,H是從這個點到終點的代價,這兩個代價加起來就是這個點的總代價,關於具體如何計算,下面給出示例。
我們還需要兩個集合,一個是open集合,一個是close集合,open集合裡存放的是還未計算代價的點,close集合裡是已經計算過的點。開始時open集合裡只有起點,close集合沒有元素,每次迭代將open集合裡F最小的點作為基點,對於基點周圍的相鄰點做如下處理:
(1)如果這個點是障礙,直接無視。
(2)如果這個點不在open表和close表中,則加入open表
(3)如果這個點已經在open表中,並且當前基點所在路徑代價更低,則更新它的G值和父親
(4)如果這個點在close表中,忽略。
處理完之後將基點加入close集合。
當終點出現在open表中的時候,迭代結束。
如果到達終點前open表空了,說明沒有路徑可以到達終點。

A星演算法實現#

下面來動手實現最簡單的A星演算法,A星演算法針對實際開發有著相當多的變化,怎樣設計跟遊戲的需求有關,這裡用unity來實現一個最基本的2D正方形網格尋路,實際開發中也可以直接使用unity的導航網格或者A* Pathfinding Project外掛。
在這個實現中,我定義了一個10x10的網格,網格中有一些無法通過的障礙。

public class Point
{
    public int X;
    public int Y;
    public int F;
    public int G;
    public int H;
    public Point parent=null;

    public bool isObstacle = false;

    public Point(int x,int y)
    {
        X = x;
        Y = y;
    }

    public void SetParent(Point parent,int g)
    {
        this.parent = parent;
        G = g;
        F = G + H;
    }
}

這裡定義了一個Point類代表每一個尋路點,X和Y代表座標,F、G、H就是上面說的三個屬性,isObstacle代表這個點是否是障礙(無法通過),parent則代表這個點的父親結點,每當我們遍歷到下一個可能在最短路徑上的點時,就把它的父親設為當前結點,這樣尋路結束後我們可以從終點通過訪問父親結點一步步回溯到起點,將路徑儲存下來。

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

public class AStar : MonoBehaviour
{
    public const int width = 10;
    public const int height = 10;

    public Point[,] map = new Point[height,width];
    public SpriteRenderer[,] sprites = new SpriteRenderer[height, width];//圖片和結點一一對應

    public GameObject prefab;   //代表結點的圖片
    public Point start;
    public Point end;

    void Start()
    {
        InitMap();
        //測試程式碼
        AddObstacle(2, 4);
        AddObstacle(2, 3);
        AddObstacle(2, 2);
        AddObstacle(2, 0);
        AddObstacle(6, 4);
        AddObstacle(8, 4);
        SetStartAndEnd(0, 0, 7, 7);
        FindPath();
        ShowPath();
    }

    public void InitMap()//初始化地圖
    {
        for(int i=0;i<width;i++)
        {
            for (int j = 0; j < height; j++)
            {
                sprites[i, j] = Instantiate(prefab, new Vector3(i, j, 0),Quaternion.identity).GetComponent<SpriteRenderer>();
                map[i, j] = new Point(i, j);
            }
        }
    }

    public void AddObstacle(int x,int y)//新增障礙
    {
        map[x, y].isObstacle = true;
        sprites[x, y].color = Color.black;
    }

    public void SetStartAndEnd(int startX,int startY,int endX,int endY)//設定起點和終點
    {
        start = map[startX,startY];
        sprites[startX, startY].color = Color.green;
        end = map[endX, endY];
        sprites[endX, endY].color = Color.red;
    }

    public void ShowPath()//顯示路徑
    {
        Point temp = end.parent;
        while(temp!=start)
        {
            sprites[temp.X, temp.Y].color = Color.gray;
            temp = temp.parent;
        }
    }

    public void FindPath()
    {
        List<Point> openList = new List<Point>();
        List<Point> closeList = new List<Point>();
        openList.Add(start);
        while(openList.Count>0)//只要開放列表還存在元素就繼續
        {
            Point point = GetMinFOfList(openList);//選出open集合中F值最小的點
            openList.Remove(point);
            closeList.Add(point);
            List<Point> SurroundPoints = GetSurroundPoint(point.X,point.Y);

            foreach(Point p in closeList)//在周圍點中把已經在關閉列表的點刪除
            {
                if(SurroundPoints.Contains(p))
                {
                    SurroundPoints.Remove(p);
                }
            }

            foreach (Point p in SurroundPoints)//遍歷周圍的點
            {
                if (openList.Contains(p))//周圍點已經在開放列表中
                {
                    //重新計算G,如果比原來的G更小,就更改這個點的父親
                    int newG = 1 + point.G;
                    if(newG<p.G)
                    {
                        p.SetParent(point, newG);
                    }
                }
                else
                {
                    //設定父親和F並加入開放列表
                    p.parent = point;
                    GetF(p);
                    openList.Add(p);
                }
            }
            if (openList.Contains(end))//只要出現終點就結束
            {
                break;
            }
        }
    }


    public List<Point> GetSurroundPoint(int x,int y)//得到一個點周圍的點
    {
        List<Point> PointList = new List<Point>();
        if(x>0&&!map[x-1,y].isObstacle)
        {
            PointList.Add(map[x - 1, y]);
        }
        if(y>0 && !map[x , y-1].isObstacle)
        {
            PointList.Add(map[x, y - 1]);
        }
        if(x<height-1 && !map[x + 1, y].isObstacle)
        {
            PointList.Add(map[x + 1, y]);
        }
        if(y<width-1 && !map[x , y+1].isObstacle)
        {
            PointList.Add(map[x, y + 1]);
        }
        return PointList;
    }


    public void GetF(Point point)//計算某個點的F值
    {
        int G = 0;
        int H = Mathf.Abs(end.X - point.X) + Mathf.Abs(end.Y - point.Y);
        if(point.parent!=null)
        {
            G = 1 + point.parent.G;
        }
        int F = H + G;
        point.H = H;
        point.G = G;
        point.F = F;
    }


    public Point GetMinFOfList(List<Point> list)//得到一個集合中F值最小的點
    {
        int min = int.MaxValue;
        Point point = null;
        foreach(Point p in list)
        {
            if(p.F<min)
            {
                min = p.F;
                point = p;
            }
        }
        return point;
    }
}

上面是A星演算法的程式碼,我使用了一張100x100畫素的圖片代表每一個結點,修改它們的顏色用來表示起點、終點、障礙和路徑。在這裡我計算的方式是每移動一個格子代價為1,所以起點的G值為0,每次遍歷把G+1,H則是當前結點和終點在x軸和y軸上的差之和。

最終效果(綠色代表起點,紅色代表終點,黑色代表障礙,灰色代表路徑)

尋路前

尋路結果

最後#

A星尋路有相當多可以擴充套件的地方,只要抓住核心,就是不斷計算周圍點的代價,找出花費最小代價到達終點的路徑,這個代價可以針對各種複雜的情況採取不同的計算方法,比如說一個FPS遊戲的AI,遊戲中玩家肯定會向火力範圍內的敵人攻擊,這時候如果為了走最短的路徑而暴露在玩家的槍口下就得不償失了,這時可以加大處在玩家攻擊範圍內的點的代價值,讓AI在更短路徑和受到攻擊的風險之間做出權衡,或者某個地方有獎勵道具,這時可以減少獎勵道具附近的點的代價值,讓AI更傾向於繞一些路去獲取道具,總之理解了演算法思想,就能靈活運用於各種尋路情境。