元某人也元某人也

Unity A星 寻路算法

A* 是通用网格最短路径算法,不限于 2D——3D 导航网格、瓦片地图、塔防、RPG 角色移动都能用;核心是在存在障碍物的网格中找出代价最低的最优路径。

本文目录

Unity A星 寻路算法

一、概念

1、解决什么问题

A * 是通用网格最短路径算法,不限于 2D,3D 导航网格、瓦片地图、塔防、RPG 角色移动都能用;核心:在存在障碍物的网格中,找出代价最低的最优路径。

2、基本原理

遍历九宫格中周围的八个点,舍弃阻挡点和边界点,选取最优路径点

3、详细原理

  1. 代价公式:

    1
    
    F = G + H
    
    • G:从起点走到当前节点的实际行走代价(已走过的路,固定真实值)
    • H:当前节点到终点的预估启发代价(没走的路,估算值)
    • F:总代价,A * 每次永远选 OpenList 中 F 最小的节点拓展
  2. OpenList(开启列表):待检查、待拓展的候选节点池

  3. CloseList(关闭列表):已经处理完毕、不再二次访问的节点;取出最优节点后立刻移入 Close

  4. Parent 父节点:记录路径前驱,寻路结束后从终点反向回溯,反转得到正向行走路径

  5. 终止条件:取出的最优节点 == 终点节点,算法结束

4. 关键约束:启发函数可采纳性

H 不能高估真实距离,否则 A * 无法保证最短路径:

  • 四向移动:曼哈顿距离(可采纳)
  • 八向移动:切比雪夫代价 10/14(可采纳)

二、具体代码实现

1. 格子类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 是否可以行走
public bool isWalkable;
// 格子的绝对位置(网格坐标)
public Vector2Int gridPos;
// 起点 -> 当前格子(准确值)
public float gCost;
// 当前格子 -> 终点(预测值)
public float hCost;
// 总代价,寻路消耗公式:f(寻路消耗)=g(距离起点的距离)+h(距离终点的距离)
public float FCost => gCost + hCost;
// 格子对象的父对象
public Node parent;
字段含义
gCost从起点到当前格子的实际代价
hCost从当前格子到终点的启发式估计代价
FCostgCost + hCost,A* 排序依据
parent格子对象的父对象

2. 寻路管理

2.1 生成网格地图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void CreateMap()
{
    Map = new Node[mapX, mapY];

    for (int x = 0; x < mapX; x++)
    {
        for (int y = 0; y < mapY; y++)
        {
            // 确定地图原点(第一象限)
            Vector2Int pos = new Vector2Int(mapOrigin.x + x, mapOrigin.y + y);
            GameObject obj = Instantiate(nodePrefab, new Vector3(pos.x, pos.y, 0), Quaternion.identity, mapRoot);

            Node node = obj.GetComponent<Node>();
            node.gridPos = new Vector2Int(x, y); // 世界坐标 -> 网格虚拟坐标
            node.isWalkable = true;

            Map[x, y] = node;
        }
    }
}

2.2 获取节点周围邻居(八方向)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public List<Node> GetNeighbors(Node node)
{
    List<Node> neighbors = new List<Node>();
    for (int x = -1; x <= 1; x++)
    {
        for (int y = -1; y <= 1; y++)
        {
            if (x == 0 && y == 0)
                continue; // 跳过中心节点

            int nodeX = node.gridPos.x + x;
            int nodeY = node.gridPos.y + y;

            if (nodeX >= 0 && nodeX < mapX && nodeY >= 0 && nodeY < mapY)
                neighbors.Add(Map[nodeX, nodeY]);
        }
    }
    return neighbors;
}

3. 算法核心

3.1 初始化 open / close 列表

1
2
3
4
List<Node> openList = new List<Node>();       // 待探索
HashSet<Node> closeList = new HashSet<Node>(); // 已探索

openList.Add(startNode);

3.2 主循环(openList 非空时)

① 选出 FCost 最小的节点

若 FCost 相同,则比较 hCost(gCost 为确定值,此处不参与 tie-break)。

1
2
3
4
5
6
7
8
9
Node currentNode = openList[0];
for (int i = 1; i < openList.Count; i++)
{
    if (openList[i].FCost < currentNode.FCost ||
        openList[i].FCost == currentNode.FCost && openList[i].hCost < currentNode.hCost)
    {
        currentNode = openList[i];
    }
}
② 移入 closeList
1
2
openList.Remove(currentNode);
closeList.Add(currentNode);
③ 到达终点:回溯路径判断是否到达终点
1
2
3
4
5
if (currentNode == targetNode)
{
    RetracePath(startNode, targetNode);
    return;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void RetracePath(Node startNode, Node endNode)
{
    List<Node> path = new List<Node>();
    Node currentNode = endNode;

    while (currentNode != startNode)
    {
        currentNode.spriteRenderer.color = Color.green;
        path.Add(currentNode);
        currentNode = currentNode.parent;
    }
    path.Reverse();
}

注:通过 parent 从终点一路回溯到起点。

④ 扩展邻居节点

对当前节点的邻居:

  • 不可行走或已在 closeList → 跳过
  • 计算 newGCost;若比原 gCost 更优,或该节点还不在 openList,则更新 g/h、设置 parent,并加入 openList
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
foreach (var node in gridManager.GetNeighbors(currentNode))
{
    if (!node.isWalkable || closeList.Contains(node))
        continue;

    float newGCost = currentNode.gCost + GetDistance(currentNode, node);

    if (newGCost < node.gCost || !openList.Contains(node))
    {
        node.gCost = newGCost;
        node.hCost = GetDistance(node, targetNode);
        node.parent = currentNode;

        if (!openList.Contains(node))
            openList.Add(node);
    }
}

说明

  • 邻居节点初始 gCost 为 0,第一次访问时 newGCost > node.gCost 仍成立,且不在任何队列 → 一定会进入 openList。
  • 赋值后若发现更短路径会更新;往回走时 newGCost 更大,不会错误覆盖。

3.3 启发函数 GetDistance

曼哈顿距离适用场景区分

曼哈顿距离只适合只能上下左右移动(四向),八向必须用 10/14 切比雪夫代价,混用会导致路径扭曲;

四方向:曼哈顿距离
1
2
3
int xDis = Mathf.Abs(node1.gridPos.x - node2.gridPos.x);
int yDis = Mathf.Abs(node1.gridPos.y - node2.gridPos.y);
return xDis + yDis;
八方向:切比雪夫式代价(直走 10,斜走 14)尽量走倾斜的道路
1
2
3
4
5
6
7
8
int xDis = Mathf.Abs(node1.gridPos.x - node2.gridPos.x);
int yDis = Mathf.Abs(node1.gridPos.y - node2.gridPos.y);

const int straightCost = 10;
const int diagonalCost = 14; // √2 ≈ 1.414,取整

// 先斜向走 min(dx,dy) 步,再直走 |dx-dy| 步
return diagonalCost * Mathf.Min(xDis, yDis) + straightCost * Mathf.Abs(xDis - yDis);

4. 多单位 A* 的优化

优化说明
优先队列 + HashSetopenList 用 Priority Queue(根为最小 F,O(log n) 取最优点);closeList 用 HashSet(O(1) 查找是否已探索)
相同起点直接复用缓存路径
起点不同、终点相同用 Dijkstra 从终点反向扩散,预计算每个格子的行进方向,多单位共享

优先队列:逻辑上是完全二叉树,实现上常用数组 + 下标公式;根节点始终为当前最小代价节点。


流程速查

image-20260520094242121


要点速查

概念要点
A*F = g + h,按 F 升序扩展,h 需可采纳(不高估)
open / close待探索 / 已探索,避免重复扩展
parent记录最短路上的前驱,终点回溯得路径
四向 / 八向曼哈顿 / 直10斜14 代价
多单位优先队列提速;同终点可反向 Dijkstra 预处理
仅用于交流学习,记录学习心得