在游戏开发中,寻路系统是AI模块的基石。A算法作为最经典的启发式搜索算法,因其高效性和灵活性成为游戏开发者的首选。不同于Unity内置的NavMesh系统,自定义A实现能提供更精细的控制和更复杂的寻路行为。
A*算法的核心思想是结合Dijkstra算法的准确性(考虑实际代价)和贪心算法的高效性(考虑预估代价)。它通过评估函数f(n)=g(n)+h(n)来决定搜索方向,其中g(n)是从起点到当前节点的实际代价,h(n)是通过启发函数估算的当前节点到终点的代价。
关键提示:启发函数h(n)的选择直接影响算法效率。在网格地图中,常用的启发函数有曼哈顿距离(适用于四方向移动)和对角线距离(适用于八方向移动)。
我们先构建寻路系统的基础数据结构。PathNode类封装了A*算法所需的全部节点信息:
csharp复制public class PathNode : System.IComparable<PathNode>
{
public Vector3 WorldPosition { get; private set; }
public int GridX { get; private set; }
public int GridY { get; private set; }
// A*算法核心数据
public int GCost { get; set; } // 起点到当前节点的实际代价
public int HCost { get; set; } // 当前节点到终点的预估代价
public int FCost => GCost + HCost; // 总评估代价
public bool IsWalkable { get; set; }
public PathNode Parent { get; set; }
public List<PathNode> Neighbors { get; } = new List<PathNode>();
public float TerrainPenalty { get; set; }
public NodeType Type { get; set; } = NodeType.Normal;
public enum NodeType { Normal, DifficultTerrain, DangerousArea }
public PathNode(Vector3 position, int x, int y, bool walkable) {
WorldPosition = position;
GridX = x;
GridY = y;
IsWalkable = walkable;
Reset();
}
public void Reset() {
GCost = int.MaxValue;
HCost = 0;
Parent = null;
}
public int CompareTo(PathNode other) {
int compare = FCost.CompareTo(other.FCost);
return compare != 0 ? compare : HCost.CompareTo(other.HCost);
}
}
这个实现有几个关键设计点:
网格导航图是最常用的寻路空间表示方法。以下是GridBasedNavigationGraph的核心实现:
csharp复制public class GridBasedNavigationGraph : MonoBehaviour
{
[SerializeField] private Vector2 gridWorldSize = new Vector2(50, 50);
[SerializeField] private float nodeRadius = 0.5f;
[SerializeField] private LayerMask unwalkableMask;
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();
}
private 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(worldPoint, x, y, walkable);
}
}
}
public List<PathNode> GetNeighbors(PathNode node) {
List<PathNode> neighbors = new List<PathNode>();
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
if (x == 0 && y == 0) continue;
int checkX = node.GridX + x;
int checkY = node.GridY + y;
if (checkX >= 0 && checkX < gridSizeX &&
checkY >= 0 && checkY < gridSizeY) {
neighbors.Add(grid[checkX, checkY]);
}
}
}
return neighbors;
}
}
网格系统的关键参数包括:
实际项目经验:网格精度需要平衡性能和质量。通常角色半径的1.5-2倍是个不错的起点。过高的精度会导致计算量剧增。
以下是A*寻路算法的核心实现:
csharp复制public class AStarPathfinder : MonoBehaviour
{
private GridBasedNavigationGraph grid;
public List<Vector3> FindPath(Vector3 startPos, Vector3 targetPos) {
PathNode startNode = grid.GetNodeFromWorldPoint(startPos);
PathNode targetNode = grid.GetNodeFromWorldPoint(targetPos);
if (startNode == null || targetNode == null || !targetNode.IsWalkable)
return null;
Heap<PathNode> openSet = new Heap<PathNode>(grid.MaxSize);
HashSet<PathNode> closedSet = new HashSet<PathNode>();
openSet.Add(startNode);
while (openSet.Count > 0) {
PathNode currentNode = openSet.RemoveFirst();
closedSet.Add(currentNode);
if (currentNode == targetNode) {
return RetracePath(startNode, targetNode);
}
foreach (PathNode neighbor in grid.GetNeighbors(currentNode)) {
if (!neighbor.IsWalkable || closedSet.Contains(neighbor))
continue;
int newMovementCost = currentNode.GCost +
GetDistance(currentNode, neighbor) +
(int)neighbor.TerrainPenalty;
if (newMovementCost < neighbor.GCost || !openSet.Contains(neighbor)) {
neighbor.GCost = newMovementCost;
neighbor.HCost = GetDistance(neighbor, targetNode);
neighbor.Parent = currentNode;
if (!openSet.Contains(neighbor))
openSet.Add(neighbor);
else
openSet.UpdateItem(neighbor);
}
}
}
return null;
}
private List<Vector3> RetracePath(PathNode startNode, PathNode endNode) {
List<PathNode> path = new List<PathNode>();
PathNode currentNode = endNode;
while (currentNode != startNode) {
path.Add(currentNode);
currentNode = currentNode.Parent;
}
path.Reverse();
return SimplifyPath(path);
}
private List<Vector3> SimplifyPath(List<PathNode> path) {
List<Vector3> waypoints = new List<Vector3>();
Vector2 directionOld = Vector2.zero;
for (int i = 1; i < path.Count; i++) {
Vector2 directionNew = new Vector2(
path[i-1].GridX - path[i].GridX,
path[i-1].GridY - path[i].GridY);
if (directionNew != directionOld) {
waypoints.Add(path[i-1].WorldPosition);
}
directionOld = directionNew;
}
waypoints.Add(path[path.Count-1].WorldPosition);
return waypoints;
}
private int GetDistance(PathNode nodeA, PathNode nodeB) {
int dstX = Mathf.Abs(nodeA.GridX - nodeB.GridX);
int dstY = Mathf.Abs(nodeA.GridY - nodeB.GridY);
return dstX > dstY ?
14*dstY + 10*(dstX-dstY) :
14*dstX + 10*(dstY-dstX);
}
}
算法实现要点:
使用高效的数据结构能显著提升A*性能:
csharp复制public class Heap<T> where T : IComparable<T> {
private T[] items;
private int currentItemCount;
public Heap(int maxSize) {
items = new T[maxSize];
}
public void Add(T item) {
item.HeapIndex = currentItemCount;
items[currentItemCount] = item;
SortUp(item);
currentItemCount++;
}
public T RemoveFirst() {
T firstItem = items[0];
currentItemCount--;
items[0] = items[currentItemCount];
items[0].HeapIndex = 0;
SortDown(items[0]);
return firstItem;
}
// 排序方法实现...
}
避免主线程卡顿的关键技术:
csharp复制public IEnumerator FindPathAsync(Vector3 startPos, Vector3 targetPos,
Action<List<Vector3>> callback) {
// 寻路计算...
yield return null; // 每帧让步
callback?.Invoke(path);
}
常用优化技术:
实时更新导航网格应对场景变化:
csharp复制public void UpdateObstacle(Vector3 position, bool isObstacle) {
PathNode node = grid.GetNodeFromWorldPoint(position);
if (node != null && node.IsWalkable != !isObstacle) {
node.IsWalkable = !isObstacle;
grid.UpdateNeighborConnections(node);
}
}
避免单位碰撞的群体移动策略:
基于风险评估的路径选择:
csharp复制public int GetTacticalCost(PathNode node) {
float danger = CalculateDangerLevel(node.WorldPosition);
float cover = CalculateCoverValue(node.WorldPosition);
return Mathf.RoundToInt(danger * 100 - cover * 50);
}
关键性能数据:
csharp复制void OnDrawGizmos() {
if (grid != null) {
foreach (PathNode n in grid) {
Gizmos.color = n.IsWalkable ? Color.white : Color.red;
Gizmos.DrawCube(n.WorldPosition, Vector3.one * (grid.NodeDiameter - 0.1f));
}
}
}
问题1:路径抖动
问题2:长距离寻路卡顿
推荐参数组合:
与Unity导航系统的混合使用策略:
在实现自定义A寻路系统时,最重要的是理解游戏的具体需求。我曾在一个RTS项目中,通过结合网格A和航点图,实现了支持200+单位同时寻路的高性能系统。关键是将静态路径预计算与动态避障分离处理,并采用分帧的异步路径更新策略。