A星寻路与二叉堆优化(2D)

内容概览

前言

Hi~你好,我是hutian,一名普通二本在读学生,正在学习游戏开发。接下来我会持续更新游戏技术相关的博客,记录我的学习成长历程.希望我的博客能够帮助到同样正在学习游戏开发的你。

吹水

敌人AI一直是很重要的一部分。平台跳跃类游戏中,往往是给敌人ai几个巡逻点,让敌人朝向巡逻点移动,到达巡逻点之后再切换到下一个巡逻点。但如果敌人ai仅仅是按照提前设定好的巡逻点徘徊,玩家很容易对这样的敌人感到厌倦。同时如果地形比较复杂,比如在地牢类游戏动态生成地图,敌人朝着巡逻点移动的过程中很容易被障碍物阻挡,这时设定巡逻点的方法并不好用。

参考《挺进地牢》中的敌人AI。敌人往往能够绕过墙壁,桌子之类的障碍物,找到抵达玩家位置的最优路径,这样的敌人ai更加智能化,

在这里插入图片描述
(刚好今天初见枪龙,一次过,真的爽到。)
这种找到最优路线的算法就是A星算法,接下来本文将详细讲解如何实现A星算法,并用二叉堆优化效率。

什么是A星寻路

A*寻路是一种游戏中常用的寻路算法。使用了一些启发式函数来预测每个节点到目标的距离,并结合当前节点的实际距离来选择下一个节点进行搜索。这种综合考虑实际距离和预估距离的方式,使得A星算法能够更快地找到从起点到目标点的最优路径,因此被广泛应用于实际场景中。

如何实现A星寻路

A星寻路的实现思路如下:

  1. 初始化:建立网格(Grid2D),节点位置是墙壁的标记为不可行走。
  2. 将起点设为当前节点并加入到开放列表。
  3. 从开放列表中选择具有最小F值(F=G+H)的节点,并将其作为当前节点。
  4. 检查当前节点是否是终点,如果是,则算法结束,从终点回溯到起点找到路径。
  5. 8方向搜索当前节点的相邻节点。对于每个相邻节点, 如果该相邻节点不可行走/已经在关闭列表里面了,则不进行操作。否则计算其到起点的代价(G)和到终点的估计代价(H),如果相邻节点不在开放列表中,则并将其加入到开放列表,并将相邻节点的父节点设为当前节点;如果相邻节点在开放列表中且新计算的总代价F(G+H)更小,则更新节点的总代价,父节点设为当前节点。
  6. 将当前节点从开放列表移动到关闭列表。
  7. 重复步骤3-6,直到起点被找到或者开放列表为空。
节点类
  public class Node2D:IComparable<Node2D>
    {
        public int gCost; //从起点到当前节点的代价
        public int hCost; //从当前格子到节点的代价(预估代价)
        public bool obstacle; //当前节点是否是障碍物
        public Vector3 worldPosition; //当前节点所在的世界坐标

        public int GridX, GridY; //节点在二维网格中的坐标
        public Node2D parent;   //父节点,根据父节点回溯到起点从而找到路径

        public Node2D(bool _obstacle, Vector3 _worldPos, int _gridX, int _gridY)
        {
            obstacle = _obstacle;
            worldPosition = _worldPos;
            GridX = _gridX;
            GridY = _gridY;
        }

        public int FCost //节点的总代价
        {
            get
            {
                return gCost + hCost;
            }

        }

        public int CompareTo(Node2D nodeToCompare)
        {
            int compare = FCost.CompareTo(nodeToCompare.FCost);
            
            //总代价相同,比较预估代价(选择离终点更近的节点)
            if (compare == 0)
            {
                compare = hCost.CompareTo(nodeToCompare.hCost);
            }

            return -compare;
        }

        public void SetObstacle(bool isObstacle)
        {
            obstacle = isObstacle;
        }
    }
网格类
   public class Grid2D : MonoBehaviour
    {
        public Vector3 gridWorldSize; //二维网格的大小
        public float nodeRadius;//节点半径
        public Node2D[,] Grid;
        public Tilemap obstaclemap; //障碍物所在tilemap
        public List<Node2D> path; 
        Vector3 worldBottomLeft;//网格的左下角

        float nodeDiameter; //节点直径
        public int gridSizeX, gridSizeY; //网格width,height

        void Awake()
        {
            nodeDiameter = nodeRadius * 2;
            gridSizeX = Mathf.RoundToInt(gridWorldSize.x / nodeDiameter);
            gridSizeY = Mathf.RoundToInt(gridWorldSize.y / nodeDiameter);
            CreateGrid();
        }



        void CreateGrid()
        {
            Grid = new Node2D[gridSizeX, gridSizeY];
            worldBottomLeft = transform.position - Vector3.right * gridWorldSize.x / 2 - Vector3.up * gridWorldSize.y / 2;

            for (int x = 0; x < gridSizeX; x++)
            {
                for (int y = 0; y < gridSizeY; y++)
                {
                    Vector3 worldPoint = worldBottomLeft + Vector3.right * (x * nodeDiameter + nodeRadius) + Vector3.up * (y * nodeDiameter + nodeRadius);
                    Grid[x, y] = new Node2D(false, worldPoint, x, y);

                    if (obstaclemap.HasTile(obstaclemap.WorldToCell(Grid[x, y].worldPosition)))
                        Grid[x, y].SetObstacle(true);
                    else
                        Grid[x, y].SetObstacle(false);


                }
            }
        }


        //gets the neighboring nodes in the 4 cardinal directions. If you would like to enable diagonal pathfinding, uncomment out that portion of code
        public List<Node2D> GetNeighbors(Node2D node)
        {
            List<Node2D> neighbors = new List<Node2D>();

            //八方向搜索
            for (int i = -1; i <= 1; i++)
            {
                for (int j = -1; j <= 1; j++)
                {
                    if (i == 0 && j == 0) continue;

                    //越界
                    if (node.GridX + i < 0 || node.GridX + i > gridSizeX ||
                        node.GridY + j < 0 || node.GridY + j > gridSizeY)
                    {
                        continue;
                    }

                    Node2D neighbourNode = Grid[node.GridX + i, node.GridY + j];
                    //处于斜角的位置
                    if (Mathf.Abs(i) == 1 && Mathf.Abs(j) == 1)
                    {
                        if (Grid[node.GridX, node.GridY + j].obstacle || Grid[node.GridX + i, node.GridY].obstacle) //斜角的节点的两个邻居节点有任意一个是障碍物,这个点就不能走
                        {
                            continue;
                        }
                    }
                    neighbors.Add(neighbourNode);
                }
            }
            return neighbors;
        }

        //根据世界坐标获取节点
        public Node2D NodeFromWorldPoint(Vector3 worldPosition)
        {

            int x = Mathf.RoundToInt(worldPosition.x + (gridSizeX / 2f));
            int y = Mathf.RoundToInt(worldPosition.y + (gridSizeY / 2f));
            return Grid[x, y];
        }



        //Draws visual representation of grid
        void OnDrawGizmos()
        {
            Gizmos.DrawWireCube(transform.position, new Vector3(gridWorldSize.x, gridWorldSize.y, 1));

            if (Grid != null)
            {
                foreach (Node2D n in Grid)
                {
                    if (n.obstacle)
                        Gizmos.color = Color.red;
                    else
                        Gizmos.color = Color.white;

                    if (path != null && path.Contains(n))
                        Gizmos.color = Color.black;
                    Gizmos.DrawCube(n.worldPosition, Vector3.one * (nodeRadius));

                }
            }
        }
    }

寻路
 public class Pathfinding2D : MonoBehaviour
    {

        public Transform seeker, target;
        Grid2D grid;
        Node2D seekerNode, targetNode;
        public GameObject GridOwner;
        public float interval;
        public float timer;

        void Start()
        {
            //Instantiate grid
            grid = GridOwner.GetComponent<Grid2D>();
        }

        private void Update()
        {
            if (timer < 0f)
            {
                timer = interval;
                FindPath(seeker.transform.position, target.position);
            }
            else
            {
                timer -= Time.deltaTime;
            }
        }

        public void FindPath(Vector3 startPos, Vector3 targetPos)
        {
            //get player and target position in grid coords
            seekerNode = grid.NodeFromWorldPoint(startPos); // 起点
            targetNode = grid.NodeFromWorldPoint(targetPos); // 终点

            // Heap<Node2D> openSet = new Heap<Node2D>(grid.gridSizeX*grid.gridSizeY);
            List<Node2D> openSet = new List<Node2D>(); //开放列表
            HashSet<Node2D> closedSet = new HashSet<Node2D>(); //关闭列表
            openSet.Add(seekerNode); //初始化将起点加入开放列表

            //calculates path for pathfinding
            while (openSet.Count > 0)
            {
                Debug.Log(openSet.Count);
                //iterates through openSet and finds lowest FCost
                Node2D node = openSet[0];
                // Node2D node = openSet.RemoveFirst();
                
                for (int i = 1; i < openSet.Count; i++)
                {
                   if (openSet[i].FCost <= node.FCost)
                   {
                       if (openSet[i].hCost < node.hCost)
                           node = openSet[i];
                   }
                }
                openSet.Remove(node);
                closedSet.Add(node);

                //If target found, retrace path
                if (node == targetNode)
                {
                    RetracePath(seekerNode, targetNode);
                    return;
                }

                //adds neighbor nodes to openSet
                foreach (Node2D neighbour in grid.GetNeighbors(node))
                {
                    if (neighbour.obstacle || closedSet.Contains(neighbour))
                    {
                        continue;
                    }

                    int newCostToNeighbour = node.gCost + GetDistance(node, neighbour);
                    if (newCostToNeighbour < neighbour.gCost || !openSet.Contains(neighbour))
                    {
                        neighbour.gCost = newCostToNeighbour;
                        neighbour.hCost = GetDistance(neighbour, targetNode);
                        neighbour.parent = node;

                        if (!openSet.Contains(neighbour))
                            openSet.Add(neighbour);
                        //加上heap
                        // else
                        //     openSet.UpdateItem(neighbour);
                    }
                }
            }
        }

        //reverses calculated path so first node is closest to seeker
        void RetracePath(Node2D startNode, Node2D endNode)
        {
            List<Node2D> path = new List<Node2D>();
            Node2D currentNode = endNode;

            while (currentNode != startNode)
            {
                path.Add(currentNode);
                currentNode = currentNode.parent;
            }
            path.Reverse();

            grid.path = path;

        }

        //gets distance between 2 nodes for calculating cost
        int GetDistance(Node2D nodeA, Node2D nodeB)
        {
            int dstX = Mathf.Abs(nodeA.GridX - nodeB.GridX);
            int dstY = Mathf.Abs(nodeA.GridY - nodeB.GridY);

            if (dstX > dstY)
                return 14 * dstY + 10 * (dstX - dstY);
            return 14 * dstX + 10 * (dstY - dstX);
        }
    }

优化

使用二叉堆可以帮助加速A*算法中的开放列表的维护过程。具体来说,可以使用最小堆来维护开放列表中节点的f值,每次选择f值最小的节点进行扩展,这个f值最小的节点就是根节点。对于堆我们主要有以下几种操作:

  1. 插入元素。(Add)
  2. 获取堆中f值最小的节点。(RemoveFirst)
  3. 堆是否包含当前元素。(Contains)
  4. 更新堆中元素的f值。(UpdateItem)
    (下面的脚本学习自https://www.youtube.com/watch?v=3Dw5d7PlcTM
    //Heap from https://www.youtube.com/watch?v=3Dw5d7PlcTM
    //T in this case will be Node
    //T : IHeapItem<T> means that the Node has to implement the interface
    public class Heap<T> where T : IHeapItem<T>
    {
        //The array that will hold the heap
        private T[] items;
        //How many nodes we have stored in the heap
        private int currentItemCount;



        //How many items can we have in the heap?
        public Heap(int maxHeapSize)
        {
            items = new T[maxHeapSize];
        }



        //Add new item to the heap
        public void Add(T item)
        {
            //Do we have room to add it?
            if (currentItemCount + 1 > items.Length)
            {
                //Debug.Log("Cant add item to heap becuse it's full");

                return;
            }

            item.HeapIndex = currentItemCount;

            //Add the item to the end of the array
            items[currentItemCount] = item;

            //But it may belong to another position in the heap
            SortUp(item);

            currentItemCount += 1;
        }



        //Remove the first item from the heap, which is the node with the lowest f cost
        public T RemoveFirst()
        {
            T firstItem = items[0];

            currentItemCount -= 1;

            //To resort the heap, we add the last item in the array to the first position in the array
            items[0] = items[currentItemCount];
            items[0].HeapIndex = 0;

            //And then move the first item to where it belongs in the array
            SortDown(items[0]);

            return firstItem;
        }



        //How many items do we have in the heap?
        public int Count
        {
            get
            {
                return currentItemCount;
            }
        }



        //Does the heap contain this item?
        public bool Contains(T item)
        {
            return Equals(items[item.HeapIndex], item);
        }



        //Update an item already in the heap, but we need to change its priority in the heap
        public void UpdateItem(T item)
        {
            //This is for pathfinding so we only need to add better nodes and thus only need to sort up
            SortUp(item); //更新后节点的总代价一定比之前更小,所以做上浮操作
        }



        //Clear the array
        public void Clear()
        {
            Array.Clear(items, 0, items.Length);

            currentItemCount = 0;
        }


        //Sorts and item down in the array to the position where it belongs
        private void SortDown(T item)
        {
            while (true)
            {
                //From heap index to array index
                int childIndexLeft = item.HeapIndex * 2 + 1;
                int childIndexRight = item.HeapIndex * 2 + 2;

                int swapIndex = 0;

                //Do we have a children to the left
                if (childIndexLeft < currentItemCount)
                {
                    swapIndex = childIndexLeft;

                    //But we also need to check if we have a children to the right
                    if (childIndexRight < currentItemCount)
                    {
                        //Compare the left and the right node, to find if we should swap with the left or the right node
                        if (items[childIndexLeft].CompareTo(items[childIndexRight]) < 0)//右子节点比左子节点更小,那么交换父元素与右子节点
                        {
                            swapIndex = childIndexRight;
                        }
                    }

                    if (item.CompareTo(items[swapIndex]) < 0)
                    {
                        Swap(item, items[swapIndex]);
                    }
                    else
                    {
                        return;
                    }
                }
                else
                {
                    return;
                }
            }
        }



        //Sorts an item up in the array to the position where it belongs
        private void SortUp(T item)
        {
            //From heap index to array index
            int parentIndex = (item.HeapIndex - 1) / 2;

            while (true)
            {
                T parentItem = items[parentIndex];

                //If item has a lower f cost than the parent
                if (item.CompareTo(parentItem) > 0)
                {
                    Swap(item, parentItem);
                }
                else
                {
                    break;
                }

                parentIndex = (item.HeapIndex - 1) / 2;
            }
        }



        //Swap 2 items in the heap, which is the same as moving one item up (or down) and the other item down (or up)
        private void Swap(T itemA, T itemB)
        {
            items[itemA.HeapIndex] = itemB;
            items[itemB.HeapIndex] = itemA;

            //We also need to swap the heap indexes
            int itemAIndex = itemA.HeapIndex;

            itemA.HeapIndex = itemB.HeapIndex;
            itemB.HeapIndex = itemAIndex;
        }
    }



    //Each node has to implement this, so both HeapIndex and CompareTo
    public interface IHeapItem<T> : IComparable<T>
    {
        int HeapIndex
        {
            get;
            set;
        }
    }

让节点类(Node2D)继承IHeapItem接口

 public class Node2D : IHeapItem<Node2D>
    {
        public int gCost; //从起点到当前节点的代价
        public int hCost; //从当前格子到节点的代价(预估代价)
        public bool obstacle; //当前节点是否是障碍物
        public Vector3 worldPosition; //当前节点所在的世界坐标

        public int GridX, GridY; //节点在二维网格中的坐标
        public Node2D parent;   //父节点,根据父节点回溯到起点从而找到路径

        private int heapIndex;

        public Node2D(bool _obstacle, Vector3 _worldPos, int _gridX, int _gridY)
        {
            obstacle = _obstacle;
            worldPosition = _worldPos;
            GridX = _gridX;
            GridY = _gridY;
        }

        public int FCost //节点的总代价
        {
            get
            {
                return gCost + hCost;
            }

        }

        public int HeapIndex
        {
            get => heapIndex;
            set => heapIndex = value;
        }

        public int CompareTo(Node2D nodeToCompare)
        {
            int compare = FCost.CompareTo(nodeToCompare.FCost);
            
            //总代价相同,比较预估代价(选择离终点更近的节点)
            if (compare == 0)
            {
                compare = hCost.CompareTo(nodeToCompare.hCost);
            }

            return -compare;
        }

        public void SetObstacle(bool isObstacle)
        {
            obstacle = isObstacle;
        }
    }

修改寻路脚本中的PathFinding函数

        public void FindPath(Vector3 startPos, Vector3 targetPos)
        {
            //get player and target position in grid coords
            seekerNode = grid.NodeFromWorldPoint(startPos); // 起点
            targetNode = grid.NodeFromWorldPoint(targetPos); // 终点

            Heap<Node2D> openSet = new Heap<Node2D>(grid.gridSizeX*grid.gridSizeY);//开放列表
            HashSet<Node2D> closedSet = new HashSet<Node2D>(); //关闭列表
            openSet.Add(seekerNode); //初始化将起点加入开放列表

            //calculates path for pathfinding
            while (openSet.Count > 0)
            {
                Debug.Log(openSet.Count);
                //iterates through openSet and finds lowest FCost
                Node2D node = openSet.RemoveFirst();
                
                closedSet.Add(node);

                //If target found, retrace path
                if (node == targetNode)
                {
                    RetracePath(seekerNode, targetNode);
                    return;
                }

                //adds neighbor nodes to openSet
                foreach (Node2D neighbour in grid.GetNeighbors(node))
                {
                    if (neighbour.obstacle || closedSet.Contains(neighbour))
                    {
                        continue;
                    }

                    int newCostToNeighbour = node.gCost + GetDistance(node, neighbour);
                    if (newCostToNeighbour < neighbour.gCost || !openSet.Contains(neighbour))
                    {
                        neighbour.gCost = newCostToNeighbour;
                        neighbour.hCost = GetDistance(neighbour, targetNode);
                        neighbour.parent = node;

                        if (!openSet.Contains(neighbour))
                            openSet.Add(neighbour);
                        //加上heap
                        else
                            openSet.UpdateItem(neighbour);
                    }
                }
            }
        }

使用

项目链接

使用演示
Hireachy窗口内配置

在这里插入图片描述

player配置

在这里插入图片描述

GridOwner配置

在这里插入图片描述

最终效果

在这里插入图片描述

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值