游戏AI路径寻路指南:如何用A*和D*Lite实现《原神》式动态地形避障(Unity/C#版)

游戏AI路径寻路指南:如何用A和DLite实现《原神》式动态地形避障(Unity/C#版)

想象一下,你正在开发一款开放世界游戏,玩家可以自由探索,甚至能像《原神》里那样,用元素反应炸毁桥梁、改变地形。当玩家做出这些操作时,那些在附近游荡的怪物、NPC,或者玩家自己的自动寻路系统,该如何应对?它们不能像傻子一样撞上刚被摧毁的障碍物,也不能在原地卡死。这正是动态路径规划的魅力所在,也是现代游戏AI从“能走”到“会走”的关键一步。

静态地图的寻路,A算法早已是行业标配。但游戏世界是鲜活的,是动态的。今天,我们就深入游戏开发的腹地,探讨如何将经典的A算法与更先进的D*Lite算法结合,在Unity引擎中,用C#打造一套能够优雅应对“炸桥”这类动态地形变化的智能寻路系统。这不仅仅是算法的堆砌,更是对游戏体验流畅度与沉浸感的深度雕琢。

1. 寻路基石:在Unity中构建高效的静态A*寻路器

在谈论动态避障之前,我们必须先有一块坚实的基石——一个在静态地图中表现优异的A寻路器。A算法的核心思想非常直观:它结合了从起点到当前点的实际代价(g值)和从当前点到终点的预估代价(h值),总是优先探索综合代价(f值)最低的节点。

f(n) = g(n) + h(n)

在游戏开发中,我们通常将游戏世界离散化为一个网格(Grid)或导航网格(NavMesh)。这里我们以2D网格为例,因为它更直观,原理也易于扩展到3D。

1.1 定义地图与节点

首先,我们需要一个数据结构来表示地图上的每一个点(节点)。这个节点需要记录位置、代价以及寻路过程中的状态。

public class PathNode : IHeapItem<PathNode>
{
    public int x;
    public int y;

    public int gCost; // 从起点到该节点的实际代价
    public int hCost; // 从该节点到终点的启发式预估代价
    public int fCost { get { return gCost + hCost; } } // 综合优先级

    public bool isWalkable = true; // 该节点是否可通行
    public PathNode parent; // 用于回溯路径的父节点

    private int heapIndex;

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

    public int CompareTo(PathNode nodeToCompare)
    {
        int compare = fCost.CompareTo(nodeToCompare.fCost);
        if (compare == 0)
        {
            compare = hCost.CompareTo(nodeToCompare.hCost);
        }
        return -compare; // 返回负值,因为堆需要升序,而我们希望fCost小的优先级高
    }
}

注意:这里实现了 IHeapItem 接口,是为了将节点放入一个最小堆(优先队列)中,以便能高效地取出 fCost 最小的节点。这是A*性能优化的关键一步,相比在列表中线性查找,时间复杂度从O(n)降到了O(log n)。

地图则是一个二维的 PathNode 数组,我们用一个 Grid 类来管理。

public class Grid : MonoBehaviour
{
    public LayerMask unwalkableMask; // 用于检测障碍物的Layer
    public Vector2 gridWorldSize; // 网格覆盖的世界空间大小
    public float nodeRadius; // 每个节点的物理半径
    private float nodeDiameter;
    private int gridSizeX, gridSizeY;

    private PathNode[,] grid;

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

    void CreateGrid()
    {
        grid = new PathNode[gridSizeX, gridSizeY];
        Vector3 worldBottomLeft = transform.position - Vector3.right * gridWorldSize.x / 2 - Vector3.forward * 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.forward * (y * nodeDiameter + nodeRadius);
                bool walkable = !(Physics.CheckSphere(worldPoint, nodeRadius, unwalkableMask));
                grid[x, y] = new PathNode(walkable, worldPoint, x, y);
            }
        }
    }

    public PathNode GetNodeFromWorldPoint(Vector3 worldPosition)
    {
        // 将世界坐标转换为网格坐标
        float percentX = (worldPosition.x + gridWorldSize.x / 2) / gridWorldSize.x;
        float percentY = (worldPosition.z + gridWorldSize.y / 2) / gridWorldSize.y; // 注意:在Unity中,forward对应z轴
        percentX = Mathf.Clamp01(percentX);
        percentY = Mathf.Clamp01(percentY);

        int x = Mathf.RoundToInt((gridSizeX - 1) * percentX);
        int y = Mathf.RoundToInt((gridSizeY - 1) * percentY);
        return grid[x, y];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值