Unity 環境區域網格化,A星算法實例,飛行游戲簡單設計


區域網格化

在使用A星算法和物體布局的過程中,常常會使用的網格的概念,即建立在網格的基礎上,會使得游戲的相關編程變得簡單的多。

格子的代碼:

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

[System.Serializable]
public class Node
{
    public Vector3 _worldPos;//格子中心點的位置
    public int _gridX, _gridY;//在網格列表的下標

    public Node(Vector3 Position, int x, int y)
    {
        _worldPos = Position;
        _gridX = x;
        _gridY = y;
    }
}

網格代碼:

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

//網格,網格的起點是在左下角,終點是右上角
public class Grid : MonoBehaviour
{
    public static Grid instance;

    private Node[,] grid;//網格
    public Vector2 gridSize;//網格橫縱大小
    public float nodeRadius;//格子的半徑
    private float nodeDiameter;//格子的直徑
    public int gridCntX, gridCntY;//兩個方向上的網格數量

    //Test
    public Transform tarTrans;//目標
    public Node tar;
    public float dir;//射程
    //目標區域
    public Node zoneLeftDown;//網格的左下角
    public Node zoneRightUp;//網格的右上角

    Vector3 pos = new Vector3();
    // Start is called before the first frame update
    void Awake()
    {
        instance = this;

        nodeDiameter = nodeRadius * 2;
        gridCntX = Mathf.RoundToInt(gridSize.x / nodeDiameter);
        gridCntY = Mathf.RoundToInt(gridSize.y / nodeDiameter);
        grid = new Node[gridCntX, gridCntY];
        CreateGrid();
    }//創建網格,起始點在左下角
    private void CreateGrid()
    {
        //獲得網格的左下角的坐標
        Vector3 startPoint = transform.position - gridSize.x / 2 * Vector3.right - gridSize.y / 2* Vector3.up;
        for (int i = 0; i < gridCntX; i++)
        {
            for (int j = 0; j < gridCntY; j++)
            {
                Vector3 worldPoint = startPoint + Vector3.right * (i * nodeDiameter + nodeRadius) + Vector3.up * (j * nodeDiameter + nodeRadius);
                grid[i, j] = new Node(worldPoint, i, j);
            }
        }
    }

    //獲取某個坐標處的格子
    public Node GetFromPosition(Vector3 position)
    {
        //首先獲得該坐標相對於網格的寬高的百分比
        float percentX = (position.x + gridSize.x / 2) / gridSize.x;
        float percentY = (position.y + gridSize.y / 2) / gridSize.y;

        //保證百分比值在0到1之間
        percentX = Mathf.Clamp01(percentX);
        percentY = Mathf.Clamp01(percentY);

        int x = Mathf.RoundToInt((gridCntX - 1) * percentX);
        int y = Mathf.RoundToInt((gridCntY - 1) * percentY);

        return grid[x, y];
    }

    //獲取一個正方形區域中隨機點,length為區域的邊長
    public Vector3 GetZoneRandomPos(Vector3 center,float length)
    {
        //射程一定要大於等於0
        //float len = Mathf.Abs(length) / 2;
        //獲取射程網格區域
        zoneLeftDown = GetFromPosition(center - new Vector3(length, length));
        zoneRightUp = GetFromPosition(center + new Vector3(length, length));
        //獲取並返回射程網格區域中的一個隨機點
        int i = Random.Range(zoneLeftDown._gridX, zoneRightUp._gridX);
        int j = Random.Range(zoneLeftDown._gridY, zoneRightUp._gridY);

        return grid[i, j]._worldPos;
    }

    //獲取整個區域中的一個隨機點
    public Vector3 GetZoneRandomPos()
    {
        int i = Random.Range(0, gridCntX);
        int j = Random.Range(0, gridCntY);
        return grid[i, j]._worldPos;
    }

    private void OnDrawGizmos()
    {
        
        //繪制網格邊界線
        Gizmos.DrawWireCube(transform.position, new Vector3(gridSize.x, gridSize.y, 1));
        if (grid == null) return;

        Gizmos.color = new Color(1, 1, 1, 0.2f);
        //繪制網格
        foreach (var node in grid)
        {
            Gizmos.DrawCube(node._worldPos+Vector3.forward, Vector3.one * (nodeDiameter - .1f*nodeDiameter));
        }
    }
}

運行結果:

 一般A星算法實例一,具體算法原理略

這里修改原來的網格代碼,從而符合A星算法的需求

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

[System.Serializable]
public class Node
{
    public bool _walkable;//是否可走
    public Vector3 _worldPos;//格子中心點的位置
    public int _gridX, _gridY;//在網格列表的下標

    public int gCost;//到起始點的曼哈頓距離
    public int hCost;//到目標點的曼哈頓距離

    public Node parent;//父親網格點

    public int fCost {//曼哈頓距離綜合
        get { return gCost +hCost; }
    }

    public Node(bool walkable,Vector3 Position, int x, int y)
    {
        _walkable = walkable;
        _worldPos = Position;
        _gridX = x;
        _gridY = y;
    }
}

public class Grid : MonoBehaviour
{
    public static Grid instance;

    private Node[,] grid;//網格
    public Vector2 gridSize;//網格橫縱大小
    public float nodeRadius;//格子的半徑
    private float nodeDiameter;//格子的直徑
    public int gridCntX, gridCntY;//兩個方向上的網格數量
    public Vector3 startPoint;//網格的最右下角的坐標
    public LayerMask layer;

    //Test
    public Transform tarTrans;//目標
    public Node tar;
    public float dir;//射程

    public List<Node> path = new List<Node>();

    //目標區域
    public Node zoneLeftDown;//網格的左下角
    public Node zoneRightUp;//網格的右上角

    Vector3 pos = new Vector3();
    // Start is called before the first frame update
    void Awake()
    {
        instance = this;

        nodeDiameter = nodeRadius * 2;
        gridCntX = Mathf.RoundToInt(gridSize.x / nodeDiameter);
        gridCntY = Mathf.RoundToInt(gridSize.y / nodeDiameter);
        grid = new Node[gridCntX, gridCntY];
        CreateGrid();
    }

    //創建網格,起始點在左下角
    private void CreateGrid()
    {
        //獲得網格的左下角的坐標
        startPoint = transform.position - gridSize.x / 2 * Vector3.right - gridSize.y / 2 * Vector3.up;
        for (int i = 0; i < gridCntX; i++)
        {
            for (int j = 0; j < gridCntY; j++)
            {
                Vector3 worldPoint = startPoint + Vector3.right * (i * nodeDiameter + nodeRadius) + Vector3.up * (j * nodeDiameter + nodeRadius);
                bool walkable = !Physics2D.OverlapCircle(worldPoint, nodeRadius, layer);
                grid[i, j] = new Node(walkable,worldPoint, i, j);
            }
        }
    }

    //獲取某個坐標處的格子,利用百分比,那么在網格區域外的一個點的網格點就是離它最近的那個網格點
    public Node GetFromPosition(Vector3 position)
    {
        //首先獲得該坐標相對於網格的寬高的百分比
        float percentX = (position.x + gridSize.x / 2) / gridSize.x;
        float percentY = (position.y + gridSize.y / 2) / gridSize.y;

        //保證百分比值在0到1之間
        percentX = Mathf.Clamp01(percentX);
        percentY = Mathf.Clamp01(percentY);

        int x = Mathf.RoundToInt((gridCntX - 1) * percentX);
        int y = Mathf.RoundToInt((gridCntY - 1) * percentY);

        return grid[x, y];
    }

    //獲取網格范圍內一個正方形區域中隨機點,length為區域的邊長
    public Vector3 GetZoneRandomPos(Vector3 center, float length)
    {
        //射程一定要大於等於0
        //float len = Mathf.Abs(length) / 2;
        //獲取射程網格區域
        zoneLeftDown = GetFromPosition(center - new Vector3(length, length));
        zoneRightUp = GetFromPosition(center + new Vector3(length, length));
        //獲取並返回射程網格區域中的一個隨機點
        int i = Random.Range(zoneLeftDown._gridX, zoneRightUp._gridX);
        int j = Random.Range(zoneLeftDown._gridY, zoneRightUp._gridY);

        return grid[i, j]._worldPos;
    }

    //獲取整個區域中的一個隨機點
    public Vector3 GetZoneRandomPos()
    {
        int i = Random.Range(0, gridCntX);
        int j = Random.Range(0, gridCntY);
        return grid[i, j]._worldPos;
    }

    //獲取某格子周圍除了自身外的另外所有可走格子
    public List<Node> GetNeibourhood(Node node)
    {
        List<Node> neibourhood = new List<Node>();
        for(int i = -1; i <= 1; i++)
        {
            for(int j = -1; j<= 1; j++)
            {
                if (i == 0 && j == 0) continue;

                int tempX = node._gridX + i;
                int tempY = node._gridY + j;
                if (tempX > 0 && tempX < gridCntX && tempY > 0 && tempY < gridCntY)
                {
                    neibourhood.Add(grid[tempX,tempY]);
                }
            }
        }
        return neibourhood;
    }

    private void OnDrawGizmos()
    {

        //繪制網格邊界線
        Gizmos.DrawWireCube(transform.position, new Vector3(gridSize.x, gridSize.y, 1));
        if (grid == null) return;
        //Gizmos.color = new Color(1, 1, 1, 0.2f);
        //繪制網格
        foreach (var node in grid)
        {
            Gizmos.color = node._walkable ? Color.white : Color.red;
            Gizmos.DrawCube(node._worldPos + Vector3.forward, Vector3.one * (nodeDiameter - .1f * nodeDiameter));
        }

        if (path != null)
        {
            foreach(var node in path)
            {
                Gizmos.color = Color.black;
                Gizmos.DrawCube(node._worldPos + Vector3.forward, Vector3.one * (nodeDiameter - .1f * nodeDiameter));
            }
        }

        Node tarNode = GetFromPosition(tarTrans.position);
        if (tarNode != null && tarNode._walkable)
        {
            Gizmos.color = Color.cyan;
            Gizmos.DrawCube(tarNode._worldPos, Vector3.one * (nodeDiameter - .1f * nodeDiameter));
        }
    }
}

下面是A星尋路算法,這里使用了兩種不能的路徑處理結果

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


//FindingPath為經典的A星尋路算法,獲得網格路徑。
//FindingPathForFlay是在尋路結束之后,將網格路徑中不需要的網格刪除調,從而使得運動方向更加平滑,剪除的過程在GeneratePathForFly中進行,
//后期優化可以放在尋路過程中進行
public class FindPath : MonoBehaviour
{
    public Transform hunter, tar;
    Grid _grid;

    // Start is called before the first frame update
    void Start()
    {
        _grid = GetComponent<Grid>();
    }

    // Update is called once per frame
    void Update()
    {
        //FindingPath(hunter.position, tar.position);
        FindingPathForFly(hunter.position, tar.position);
    }

    //經典A*算法到目標點的精確路徑查找
    void FindingPath(Vector3 startPos,Vector3 endPos)
    {
        Node startNode = _grid.GetFromPosition(startPos);
        Node endNode = _grid.GetFromPosition(endPos);

        List<Node> openSet = new List<Node>();
        HashSet<Node> closeSet = new HashSet<Node>();
        openSet.Add(startNode);

        
        while (openSet.Count > 0)
        {
            //根據曼哈頓距離尋找新結點
            Node currentNode = openSet[0];

            for (int i = 0; i < openSet.Count; i++)
            {
                if (openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost)
                {
                    currentNode = openSet[i];
                }
            }

            openSet.Remove(currentNode);
            closeSet.Add(currentNode);

            //如果當前結點是最后一個結點,那么尋找結束
            if (currentNode == endNode) {
                GeneratePath(startNode,endNode);
                return;
            }

            //添加周圍的可用結點到openSet
            foreach (var node in _grid.GetNeibourhood(currentNode))
            {
                if (!node._walkable || closeSet.Contains(node)) continue;
                int newCost = currentNode.gCost + GetDistanceNodes(currentNode, node);
                if (newCost < node.gCost || !openSet.Contains(node))
                {
                    node.gCost = newCost;
                    node.hCost = GetDistanceNodes(node, endNode);
                    node.parent = currentNode;
                    if (!openSet.Contains(node))
                    {
                        openSet.Add(node);
                    }
                }
            }
        }
    }

    //經典A*算法下的飛行游戲路徑查找
    void FindingPathForFly(Vector3 startPos, Vector3 endPos)
    {
        Node startNode = _grid.GetFromPosition(startPos);
        Node endNode = _grid.GetFromPosition(endPos);

        List<Node> openSet = new List<Node>();
        HashSet<Node> closeSet = new HashSet<Node>();
        openSet.Add(startNode);


        while (openSet.Count > 0)
        {
            //根據曼哈頓距離尋找新結點
            Node currentNode = openSet[0];

            for (int i = 0; i < openSet.Count; i++)
            {
                if (openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost)
                {
                    currentNode = openSet[i];
                }
            }

            openSet.Remove(currentNode);
            closeSet.Add(currentNode);

            //如果當前結點是最后一個結點,那么尋找結束
            if (currentNode == endNode)
            {
                GeneratePathForFly(startNode, endNode);
                return;
            }

            //添加周圍的可用結點到openSet
            foreach (var node in _grid.GetNeibourhood(currentNode))
            {
                if (!node._walkable || closeSet.Contains(node)) continue;
                int newCost = currentNode.gCost + GetDistanceNodes(currentNode, node);
                if (newCost < node.gCost || !openSet.Contains(node))
                {
                    node.gCost = newCost;
                    node.hCost = GetDistanceNodes(node, endNode);
                    node.parent = currentNode;
                    if (!openSet.Contains(node))
                    {
                        openSet.Add(node);
                    }
                }
            }
        }
    }

    //經典A*算法下根據父子關系回溯獲得路徑
    void GeneratePath(Node startNode,Node endNode)
    {
        List<Node> path = new List<Node>();
        Node temp = endNode;
        while (temp != startNode)//回溯,將所有路徑結點結合成路徑列表
        {
            path.Add(temp);
            temp = temp.parent;
        }
        path.Reverse();//將列表反轉
        _grid.path = path;//將列表交予Grid;
    }

    //經典A*算法下根據父子關系回溯獲得路徑,並且回溯。如果兩個網格點之間沒有障礙,可以直達,那么剪除兩個網格點之間剪除不要的網格點
    void GeneratePathForFly(Node startNode, Node endNode)
    {
        List<Node> path = new List<Node>();
        Node temp = endNode;
        while (temp != startNode)//回溯,將所有路徑網格結合成路徑列表
        {
            if(path.Count==0)
                path.Add(temp);

            RaycastHit2D hit;
            //如果前一個網格點與當前網格點的父親網格之間有障礙,那么表明需要當前網格點,將其添加到路徑表中
            if(hit=Physics2D.Raycast(path[path.Count-1]._worldPos, temp.parent._worldPos - path[path.Count - 1]._worldPos,Vector3.Distance(temp.parent._worldPos, path[path.Count - 1]._worldPos)))
            {
                path.Add(temp);
            }

            temp = temp.parent;
        }
        path.Reverse();//將列表反轉

        _grid.path = path;//將列表交予Grid;
    }

    //獲取兩個點之間的曼哈頓距離
    int GetDistanceNodes(Node a,Node b)
    {
        int cntX = Mathf.Abs(a._gridX - b._gridX);
        int cntY = Mathf.Abs(a._gridY - b._gridY);
        if (cntX > cntY)
        {
            return 14 * cntY + 10 * (cntX - cntY);
        }
        else
        {
            return 14 * cntX + 10 * (cntY - cntX);
        }
    }
}

結果

 

剪除多余的網格點之后

 

對於不同體積的角色,避免碰撞的處理方案有兩種:

第一種:將網格分為不同的層,每層的格子的大小不同。

第二種:尋路時,網格可以走的條件為,walkable為真,且在設定半徑范圍內沒有其他碰撞體存在。

 

2D飛行游戲最簡單AI設計:

1.如果自己與目標之間沒有障礙,那么直接飛向目標

2.飛向目標的方式:轉向目標方向,如果當前的方向與到目標方向的角度小於設定的某個角度,那么開啟噴射引擎。

3.逃離目標:這里以圓形的障礙目標為例,如果檢測到障礙,那么設定障礙到自身的方向為目標方向,轉向該目標方向,從而逃離目標。由於這個過程中自身的位置 不斷變化,目標方向也是變化的,但是獲得的結果也更加真實。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM