从A*算法的二维列表构建到四叉树优化设计实战应用

简述A*的实现以及优化方向:(已经清楚这一部分的同学可以跳过)

A*算法是一种启发式算法,它是一种根据已知信息去合理的,有规律的猜测下一步,直至构建完整的路径(或者是遇见死路强制结束算法)

A*算法是怎么出现的

当我们把世界抽象成一个二维网格,这样,世界就成为了N*N个点,起点和终点也抽象为了这个二维网格上的某两个点

接下来将所有的障碍设置成红色的点(不可通过的),所有的可行点设置为白色的点(可通过),一个寻找最短路径的算法题就抽象出来了。(中间蓝色的点为中心,方便确立中心位置)

(此时只需要看红白点)

假设我们是一个笨蛋,不清楚任何的算法,只知道白色的点可行,红色的点不可行

那么最初,我们在起点的时候,会有8个点可以去检查是否可行(越界全当不可通过的红点处理),检查方式为8邻域是否是白点,如果是白点,那么我们就有可能会选择它,所以我们把它加入列表(开启列表)作为备选;对于红点,我们无法到达,所以直接不看。

那么剩下几个可行的点如何选择呢?我们的目的是最短路径,此时我们可以把这个最短路径(f = g + h)划分为实际走过的(当前点到起点) + 可能要走的(预估的,即当前点到终点),我们把这个路程称为“代价”

实际走过的部分,对于四邻域,代价为 1 * 两水平(垂直)相邻点距离,对于对角邻域,代价为 1.4 * 两水平(垂直)相邻点距离(注:此处的1.4为四舍五入后的根号2),这一部分的代价是明确确定的,没有异议的,剩下一部分为预估的,因为我们没有确凿的证据保证我们能准确计算出剩下的代价,于是我们可以用两种“猜”的方式:欧式距离,曼哈顿距离。为什么我们可以去猜?

我们“乐观”的认为未来的路线是直线(欧式距离)或者是前后左右水平的线(曼哈顿距离),但实际的路径代价大概率是比我们想象的要“悲观”(因为会有弯路),所以它可以保证不破坏我们未来路径的最优性——“乐观”的路径如果代价都很大,那么这条路径实际代价会更大;“乐观”的路径如果代价很小,那么它就有可能真实的代价也很小,所以这个合理的猜测可以作为我们的预估代价依据

想了这么多,目前我们一步也没走,但我们通过f = g + h计算出了我们8邻域可行点各自的代价,依照目的,我们应该往代价最小的白点走一步。因为我们选择了这个点,所以要把它从开启列表(候选点列表)中移除并加入关闭列表(已走点列表)——为了返回我们的最短路径,我们是要记录我们走过的点的,因此我们想到了把走过的点加入到关闭列表里
很好我们已经走出第一步了,接下来类似的思路,但是我们要考虑的更多了,是否是已经走过的点,如果是,我们坚决不能采纳,这相当于绕回来了,与目的地的距离没变,代价反而变多了,所以是万万不可取的;是否是开启列表里的点,如果是,我们依旧不能采纳,因为如果走到这个点,这个点的预估代价没变,实际代价只增不减,所以不选——我们最初就能选,为什么要绕个大弯再去选它呢?

所以,我们把筛选后的可行点再次计算代价并加入开启列表(候选列表),接下来我们继续从开启列表选择代价(路径)最小的点,此时可能出现两种情况——1.代价最小的点在当前点的8邻域,2.代价最小的点在上一个或者上几个点的8邻域附近。有可能会有读者有疑问,这个时候怎么办,解决方法就是,依旧,选择代价最小的点,因为我们目的为找到最短路径,起点到a点再到终点的总代价比起点到b点再到终点小,就说明这个路径是更短的,我们没有理由不选择它,有读者追问,那么如何返回完整的最短路径列表?我们只需要在计算c点八邻域各个筛选后的点时加上父节点为c点时就可以了,在返回路径的时候不断递归链表就能找到路径此时我们就可以不断循环这个步骤,直至找到一个点与终点重合,我们设置好终点的父节点后,就可以通过父节点的链表得到这个最优路径。当然,若开启列表为空时依旧没有找到终点,不用怀疑,当前是死路,我们无法到达终点。

A*算法的优化方向

从计算上:

ECS并行处理,将寻路任务分配到多个线程或使用 Unity 的 Job System(此处不做说明,待作者学习完毕后补充)

从空间结构上:

对于大型地图来说,存在大量空旷或连续障碍区域,如果我们依旧是采用二维数组去遍历八邻域的方式,未免效率低下,因此,为了加速查询,我们可以把二维空间切割成四个子空间,对于每个子空间,如果全为单一类型(可通过或不可通过中的一种)则无需划分,若为复合类型(同时拥有可通过和不可通过的点),则继续划分,直至完全划分或到达最大深度。

示例图(应为QuadTree,Oct为八叉树)

实机图

此时主播想说的是,理想很丰富,现实很骨感,在实际应用时,作者依旧是碰壁了很多次。具体请看下文A*算法优化代码四叉树部分

A*寻路的代码实现(基础版)

分为:绘制(编辑器阶段),制图(为A*管理器提供操作对象),寻点(将起点(Vector3)和终点(Vector3)贴合到离散的点上并找到对应的数据结构(节点类)),寻路,返回路径,移动。这几个部分

Node类:

[Serializable]
public class Node
{
    //初始时固定的
    public Vector3 worldPosition;
    public bool walkable;
    [NonSerialized] public List<Node> neighbors;
    //每次使用a*时计算的
    [NonSerialized] public Node parent;
    public float gCost;
    public float hCost;
    public float fCost; 

    public Node(Vector3 _worldPos, bool _walkable)
    {
        worldPosition = _worldPos;
        walkable = _walkable;
        neighbors = new List<Node>();
    }
}

初始化:

 public void Init()
 {
     PathfindingGridVisualizer pathfindingGridVisualizer = GameObject.Find("Main Camera").GetComponent<PathfindingGridVisualizer>();
     pathfindingGridVisualizer.UseWay(out List<List<Node>> np, out List<QuadTreeNode> le);
     nodeMap = np;
     leaves = le;
     BuildIndex();
     this.isUseQuadTree = pathfindingGridVisualizer.isUseQuadTree;
     InitMap();
     interval = (nodeMap[0][0].worldPosition - nodeMap[0][1].worldPosition).magnitude;
 }

初始化地图函数:

    //初始化地图
    public void InitMap()
    {
        if(!isUseQuadTree)
        {
            openList.Clear();
            closeList.Clear();
        }
        else
        {
            openListAboutQuadTree.Clear();
            closeListAboutQuadTree.Clear();
        }
    }

绘制(编辑器阶段)

绘制脚本:

参数:网格大小,网格半径,检测半径(检测是否有障碍),地图初始坐标,各种格子绘制的颜色,检测的障碍物Layer层级

    [Header("Grid Settings")]
    public float gridSize = 1f;          // 网格大小
    public int gridRadius = 20;         // 网格半径(单位:格子数)
    public float checkRadius = 0.1f;    // 检测半径
    public Vector3 startPosition;
    [Header("Visualization")]
    public Color walkableColor = Color.white;
    public Color obstacleColor = Color.red;
    public Color centerColor = Color.cyan;

    [Header("Layer Mask")]
    public LayerMask obstacleMask;      // 障碍物检测的层级

绘制函数:

    private void OnDrawGizmos()
    {
        if (!Application.isEditor) return;

            Vector3 center = startPosition;

            // 绘制中心点(特殊颜色)
            Gizmos.color = centerColor;
            Gizmos.DrawSphere(center, checkRadius * 1.5f);

            // 计算网格范围
            int totalSize = gridRadius * 2 + 1;

            for (int x = -gridRadius; x <= gridRadius; x++)
            {
                for (int z = -gridRadius; z <= gridRadius; z++)
                {
                    Vector3 pos = center + new Vector3(x * gridSize, 0, z * gridSize);

                    // 检测障碍物
                    bool isObstacle = Physics.CheckSphere(pos, checkRadius, obstacleMask);

                    // 设置颜色
                    Gizmos.color = isObstacle ? obstacleColor : walkableColor;

                    // 绘制点
                    Gizmos.DrawSphere(pos, checkRadius);

                    // 可选:绘制网格线
                }
            }
    }

示例图:

此时我们只是在编辑器页面有了可视化二维数组,但我们还没有真正应用到A*当中。

制图(为A*管理器提供操作对象)

// 获取可行走点列表(可用于实际寻路)
private List<List<Node>> GetWalkablePoints()
{
    Vector3 center = startPosition;
    int diameter = 2 * gridRadius + 1; // 网格的边长(如 gridRadius=2 -> 5x5)
    List<List<Node>> points = new List<List<Node>>(diameter);

    // 初始化每一行
    for (int i = 0; i < diameter; i++)
    {
        points.Add(new List<Node>(diameter));
    }

    // 填充节点
    for (int x = -gridRadius; x <= gridRadius; x++)
    {
        for (int z = -gridRadius; z <= gridRadius; z++)
        {
            Vector3 pos = center + new Vector3(x * gridSize, 0, z * gridSize);
            bool isWalkable = !Physics.CheckSphere(pos, checkRadius, obstacleMask);
            Node node = new Node(pos, isWalkable);

            // 将负坐标转换为正索引(x + gridRadius)
            int rowIndex = x + gridRadius;
            //int colIndex = z + gridRadius;
            points[rowIndex].Add(node); 
        }
    }
    return points;
}

寻点(将起点(Vector3)和终点(Vector3)贴合到离散的点上并找到对应的数据结构(节点类))

 public List<Vector3> GetPathAboutAStar(Vector3 start,Vector3 end)
 {
     InitMap();
     start.y = nodeMap[0][0].worldPosition.y;
     end.y = nodeMap[0][0].worldPosition.y;
     List<Vector3> path = new List<Vector3>();
     Node s = null;
     Node e = null;
     for (int i = 0;i< nodeMap.Count; i++)
     {
         s ??= nodeMap[i].Find(p => Vector3.Distance(p.worldPosition, start) < 0.71 && p.walkable);//保证在点与点距离为1时百分百能找到贴合的点
         e ??= nodeMap[i].Find(p => Vector3.Distance(p.worldPosition, end) < 0.71 && p.walkable);
         if (s != null && e != null) break;
     }
     if (s == null)
     {
         Debug.Log("找不到贴合的点");
         //检查点的间隔以及是否附近有可行的点
         return null;
     }
     if (e == null)
     {
         Debug.Log("目标点脱离A*范围");
         return null;
     }
     if(FindPath(s, e))
     {
         return GetFinalPath(s, e);
     }
     return null;
 }

寻路(遍历邻域的方式为建立二维列表前用字典以Node为键,索引为值构建字典

private bool FindPath(Node start, Node end)
{
    openList.Add(start);//按代价大小排序,最小的在前
    Node currentNode = null;
    while (openList.Count > 0)
    {
        currentNode = openList[0];
        openList.RemoveAt(0);
        closeList.Add(currentNode);
        //找到其相邻的可加入的节点加入开启列表
        var (i, j) = _indexMap[currentNode.worldPosition];
        for(int a = -1; a < 2; a++)
        {
            for(int b = -1;b < 2; b++)
            {
                if (a == 0 && b == 0)//排除自己
                {
                    continue;
                }
                if(i + a < 0 || i + a >= nodeMap.Count || j + b < 0 || j + b >= nodeMap[0].Count)
                {
                    continue;//越界点不要
                }
                //当前点
                Node theNode = nodeMap[i + a][j + b];
                if (!theNode.walkable)
                {
                    continue;//阻挡点不要
                }
                if (openList.Find(p => p.worldPosition == theNode.worldPosition)!=null || closeList.Find(p => p.worldPosition == theNode.worldPosition) != null)
                {
                    continue;//在开启列表和关闭列表中也不要
                }
                //设置父节点并计算寻路消耗
                theNode.parent = currentNode;
                float d = 1 * interval;
                if (a != 0 && b != 0)
                {
                    d = 1.4f * interval;
                }
                float g = theNode.parent.gCost + d;
                float h1 = Mathf.Abs(theNode.worldPosition.x - end.worldPosition.x);
                float h2 = Mathf.Abs(theNode.worldPosition.z - end.worldPosition.z);
                float h = h1 + h2;
                float f = g + h;
                theNode.gCost = g;
                theNode.hCost = h;
                theNode.fCost = f;
                //加入开启列表
                openList.Add(theNode);
                if (theNode.worldPosition == end.worldPosition)
                {
                    end.parent = currentNode;
                    return true; // 找到路径
                }
            }
        }
        if (openList.Count == 0)
        {
            Debug.Log("此路不通");
            return false;
        }
        //排序,f大的排在后面
        openList.Sort((node1, node2) =>
        {
            return node1.fCost >= node2.fCost ? 1 : -1;
        });
        //把消耗最小的f的节点放入关闭列表并且移除
    }
    return true;
}

返回路径

public List<Vector3> GetFinalPath(Node start, Node end)
{
    List<Vector3> path = new List<Vector3>();
    Node currentNode = end;

    while (currentNode != start)
    {
        path.Add(currentNode.worldPosition); // 将当前节点添加到路径列表
        currentNode = currentNode.parent;    // 回溯到父节点
    }

    path.Add(start.worldPosition);  // 添加起点
    path.Reverse();  // 将路径反转,确保从起点到终点
    return path;
}

移动

绘制路径

void OnDrawGizmos()
{
    if (nodes.Count < 2) return;

    Gizmos.color = Color.blue;
    for (int i = 0; i < nodes.Count - 1; i++)
    {
        Gizmos.DrawLine(nodes[i], nodes[i + 1]);
    }
}

移动脚本(在HFSM追击状态追击函数内部)

                // 判断是否已经到达终点
                if (hFSMController.currentPointIndex < hFSMController.nodes.Count)
                {
                    // 获取当前目标点
                    Vector3 targetPoint = hFSMController.nodes[hFSMController.currentPointIndex];
                    // 检查是否到达当前路径点(先检查再移动,否则会出现到达点时抽出一帧为翻转朝向的情况
                    while (hFSMController.nodes != null && hFSMController.nodes.Count > 0 && Vector3.Distance(characterObject.transform.position, targetPoint) < 0.5f)
                    {
                        hFSMController.currentPointIndex++;
                        hFSMController.currentPointIndex %= hFSMController.nodes.Count; // 确保索引在合法范围内
                        targetPoint = hFSMController.nodes[hFSMController.currentPointIndex];
                    }
                    MoveTowardsPoint(targetPoint);
    private void MoveTowardsPoint(Vector3 targetPoint)
    {
        // 计算朝向目标点的方向
        Vector3 direction = (targetPoint - characterObject.transform.position).normalized;
        Vector3 velocity = direction * moveSpeed;
        // 使用物理力推动角色
        characterObject.GetComponent<Rigidbody>().linearVelocity = new Vector3(velocity.x, characterObject.GetComponent<Rigidbody>().linearVelocity.y, velocity.z);
    }

A*寻路的四叉树优化(进阶版)

分为:绘制(编辑器阶段),制图(为A*管理器提供操作对象),寻点(将起点(Vector3)和终点(Vector3)贴合到离散的点上并找到对应的数据结构(节点类)),寻路,返回路径,移动。这几个部分,初始化见上文,此处不再赘述

QuadTreeNode类:

public class QuadTreeNode
{
    public QuadTreeNode pathparent; //回溯寻找路径时的父节点
    public QuadTreeNode parent;     // 父节点
    public Bounds bounds;           // 节点边界(Unity的Bounds类)
    public QuadTreeNode[] children; // 4个子节点
    public bool isLeaf = false;     // 是否为叶节点
    public bool isWalkable;         // 叶节点专用:是否可通过
    public Vector3 center;          // 区域中心坐标
    public int depth;               // 节点深度(根节点为0)
    public List<QuadTreeNode> neighbors;

    public float gCost;
    public float hCost;
    public float fCost;

    public QuadTreeNode(QuadTreeNode parent,Bounds bounds, int depth)
    {
        this.parent = parent;
        this.bounds = bounds;
        this.center = bounds.center;
        this.depth = depth;
    }
}

绘制(编辑器阶段)

绘制脚本:参数:树,叶子结点列表,是否启用四叉树(在Inspector提供开关,可动态支持调整搜索方式为四叉树还是二维列表),当前是否启用了四叉树(检测用)

    public QuadTreeNode root;
    public List<QuadTreeNode> leaves = new List<QuadTreeNode>();
    [Header("启用四叉树搜索(true)")]
    public bool isUseQuadTree;
    private bool currentUseQuadTree;
private void Awake()
{
    currentUseQuadTree = isUseQuadTree;
}
private void OnDrawGizmos()
{
    if (isUseQuadTree)
    {
        if (root != null)
        {
            DrawNode(root);
        }
    }

绘制脚本(部分,仅展示与四叉树相关)

 private void DrawNode(QuadTreeNode node)
 {
     if (node == null) return;

     // 设置颜色
     if (node.isLeaf)
     {
         Gizmos.color = node.isWalkable ? Color.green : Color.red;
     }
     else
     {
         Gizmos.color = Color.yellow;
     }

     // 画出边界(线框立方体)
     Gizmos.DrawWireCube(node.bounds.center, node.bounds.size);

     // 如果有子节点,递归画
     if (node.children != null)
     {
         foreach (var child in node.children)
         {
             DrawNode(child);
         }
     }
 }

递归树并绘制叶子节点为红色(不可通过),绿色(可通过),非叶子节点为黄色

实机图

制图(为A*管理器提供操作对象)

    /// <summary>
    /// 初始化并获取二维列表和树的叶子结点列表
    /// </summary>
    /// <param name="nodes"></param>
    /// <param name="quadTreeNodes"></param>
    public void UseWay(out List<List<Node>> nodes,out List<QuadTreeNode> quadTreeNodes)
    {
        nodes = null;
        quadTreeNodes = null;
        root = BuildQuadTree(root, new Bounds(startPosition, new Vector3(2 * gridRadius + 1, 1, 2 * gridRadius + 1)), 5);
        if (root != null)
        {
            leaves.Clear();
            CollectLeaves(root);//提取出所有叶子结点
            BuildNeighbors(leaves);//构建邻接点
            quadTreeNodes = leaves;
        }
        nodes = GetWalkablePoints();
        if(quadTreeNodes == null)
        {
            Debug.LogWarning("树构建失败");
        }
        if(nodes == null)
        {
            Debug.LogWarning("二维列表构建失败");
        }
    }

制图阶段相较二维数组更加复杂,首先要构建树,然后要提取出所有叶子结点(我们操作的部分就是叶子结点,和非叶子结点无关),最后为叶子结点构建邻接表用于a*寻路时遍历相邻元素

递归建树:

 private QuadTreeNode BuildQuadTree(QuadTreeNode parent,Bounds bounds, int maxDepth,  int depth = 0)
 {
     QuadTreeNode node = new QuadTreeNode(parent,bounds, depth);

     // 判断该区域是否完全可行走
     bool allWalkable = CheckAreaWalkable(bounds,  out bool allBlocked);

     if (allWalkable || allBlocked || depth >= maxDepth)
     {
         node.isLeaf = true;
         node.isWalkable = allWalkable;
         return node;
     }

     // 否则细分四个子节点
     node.children = new QuadTreeNode[4];
     Vector3 childSize = bounds.size / 2f; // 子区域大小
     Vector3 half = childSize / 2f;        // 子区域一半尺寸
     Vector3 center = bounds.center;

     // 四个象限(在 XZ 平面)
     node.children[0] = BuildQuadTree(node, new Bounds(center + new Vector3(-half.x, 0, half.z), childSize), maxDepth, depth + 1); // 西北
     node.children[1] = BuildQuadTree(node, new Bounds(center + new Vector3(half.x, 0, half.z), childSize), maxDepth, depth + 1); // 东北
     node.children[2] = BuildQuadTree(node, new Bounds(center + new Vector3(-half.x, 0, -half.z), childSize), maxDepth, depth + 1); // 西南
     node.children[3] = BuildQuadTree(node, new Bounds(center + new Vector3(half.x, 0, -half.z), childSize), maxDepth, depth + 1); // 东南

     return node;
 }

建立好后递归提取叶子结点:

   void CollectLeaves(QuadTreeNode node)
   {
       if (node.isLeaf)
       {
           leaves.Add(node);
       }
       else if (node.children != null)
       {
           foreach (var child in node.children)
               CollectLeaves(child);
       }
   }

递归建立叶子结点的邻接点列表:

void BuildNeighbors(List<QuadTreeNode> leaves)
{
    foreach (var node in leaves)
    {
        node.neighbors = new List<QuadTreeNode>();

        foreach (var other in leaves)
        {
            if (node == other) continue;

            if (AreNeighbors(node.bounds, other.bounds))
            {
                node.neighbors.Add(other);
            }
        }
    }
}

判断相邻函数:

    // 判断两个叶子是否相邻
    bool AreNeighbors(Bounds a, Bounds b)
    {
        // 判断XZ平面接触
        bool xOverlap = (a.max.x >= b.min.x && a.min.x <= b.max.x);
        bool zOverlap = (a.max.z >= b.min.z && a.min.z <= b.max.z);

        bool xTouch = Mathf.Approximately(a.max.x, b.min.x) || Mathf.Approximately(a.min.x, b.max.x);
        bool zTouch = Mathf.Approximately(a.max.z, b.min.z) || Mathf.Approximately(a.min.z, b.max.z);

        // 必须在 X 或 Z 上接触,并且在另一个方向上有重叠
        return (xTouch && zOverlap) || (zTouch && xOverlap);
    }

寻点(将起点(Vector3)和终点(Vector3)贴合到离散的叶子结点上并找到对应的数据结构(树节点类))

public List<Vector3> QuadTreeAStar(Vector3 start, Vector3 end)
{
    InitMap();
    start.y = leaves[0].center.y;
    end.y = leaves[0].center.y;
    List<Vector3> path = new List<Vector3>();
    QuadTreeNode s = null;
    QuadTreeNode e = null;
    for (int i = 0; i < leaves.Count; i++)
    {
        if (s == null && leaves[i].bounds.Contains(start))
        {
            s = leaves[i];
        }
        if (e == null && leaves[i].bounds.Contains(end))
        {
            e = leaves[i];
        }
        if (s != null && e != null)
        {
            break;
        }
    }
    if (s == null)
    {
        Debug.Log("找不到贴合的点");
        //检查点的间隔以及是否附近有可行的点
        return null;
    }
    if (e == null)
    {
        Debug.Log("目标点脱离A*范围");
        return null;
    }
    if (FindPathAboutQuadTree(s, e))
    {
        return GetFinalPathAboutQuadTree(s, e);
    }
    return null;
}

寻路(遍历邻域方式为使用其成员变量neighbors(上文已构建)

private bool FindPathAboutQuadTree(QuadTreeNode start, QuadTreeNode end)
{
    openListAboutQuadTree.Add(start);//按代价大小排序,最小的在前
    QuadTreeNode currentNode = null;
    while (openListAboutQuadTree.Count > 0)
    {
        currentNode = openListAboutQuadTree[0];
        openListAboutQuadTree.RemoveAt(0);
        closeListAboutQuadTree.Add(currentNode);
        foreach (var child in currentNode.neighbors)
        {
            if (!child.isWalkable)
            {
                continue;//阻挡点不要
            }
            if (openListAboutQuadTree.Find(p => p.bounds == child.bounds) != null || closeListAboutQuadTree.Find(p => p.bounds == child.bounds) != null)
            {
                continue;//在开启列表和关闭列表中也不要
            }
            child.pathparent = currentNode;
            float g = child.pathparent.gCost + (child.pathparent.center - child.center).magnitude;

            float h1 = Mathf.Abs(child.center.x - end.center.x);
            float h2 = Mathf.Abs(child.center.z - end.center.z);
            float h = h1 + h2;
            float f = g + h;
            child.gCost = g;
            child.hCost = h;
            child.fCost = f;
            openListAboutQuadTree.Add(child);
            if (child.bounds == end.bounds)
            {
                child.pathparent = currentNode;
                return true; // 找到路径
            }
        }
        if (openListAboutQuadTree.Count == 0)
        {
            Debug.Log("此路不通");
            return false;
        }
        //排序,f大的排在后面
        openListAboutQuadTree.Sort((node1, node2) =>
        {
            return node1.fCost >= node2.fCost ? 1 : -1;
        });
        //把消耗最小的f的节点放入关闭列表并且移除
    }
    return true;
}

返回路径(此处使用的是pathParent而不是parent,parent是构建树时和构建邻接表时使用的)

    public List<Vector3> GetFinalPathAboutQuadTree(QuadTreeNode start, QuadTreeNode end)
    {
        List<Vector3> path = new List<Vector3>();
        QuadTreeNode currentNode = end;
        int i = 0;
        while (currentNode != start)
        {
            i++;
            path.Add(currentNode.center); // 将当前节点添加到路径列表
            currentNode = currentNode.pathparent;    // 回溯到父节点
        }

        path.Add(start.center);  // 添加起点
        path.Reverse();  // 将路径反转,确保从起点到终点
        return path;
    }

移动:相同的代码不在赘述,此处仅解释固定时刻刷新(重新寻路)的协程处理

private IEnumerator DelayedPathCalculation()
{
    yield return null; // 等待一帧,确保其他脚本初始化完成

    if (AStarManager.Instance == null || PlayerManager.Instance == null)
    {
        Debug.LogError("依赖管理器未初始化!");
        yield break;
    }
    while (true)
    {
        Vector3 startPos = transform.position;
        Vector3 endPos = PlayerManager.Instance.player.transform.position;
        List<Vector3> path = null;
        if (pathfindingGridVisualizer.isUseQuadTree)
        {
            path = AStarManager.Instance.QuadTreeAStar(startPos, endPos);
        }
        else
        {
            path = AStarManager.Instance.GetPathAboutAStar(startPos, endPos);
        }

        if (path == null)
        {
            Debug.LogWarning("未找到路径!");
        }
        else
        {
            nodes = path;
            nodes[0] = startPos;
            nodes[nodes.Count - 1] = endPos;
            currentPointIndex = 0;
        }
        yield return new WaitForSecondsRealtime(2f);
    }
}

作者算法依旧还有很多改进的地方,比如对开启列表的最小堆(优先队列)排序优化,没有做动态地形设计(此方法若用于动态地形则会因为重复构建树导致FPS下降严重),仅考虑了二维等。但总体思路是正确且可行的,感谢亲爱的读者能看到这里,如果作者后续有能力,会把ECS的优化补上,感谢观看!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值