游戏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];
}
}

&spm=1001.2101.3001.5002&articleId=151606412&d=1&t=3&u=7c46fd04556541a2b96ebd7591dc64b1)

被折叠的 条评论
为什么被折叠?



