Unity游戏实战:用C#手搓一个A*寻路,让NPC学会绕开障碍物(附完整项目代码)

张开发
2026/4/12 17:56:46 15 分钟阅读

分享文章

Unity游戏实战:用C#手搓一个A*寻路,让NPC学会绕开障碍物(附完整项目代码)
Unity游戏开发实战基于A*算法的智能寻路系统设计与优化在RPG或SLG游戏开发中NPC如何自主规划路径绕过障碍物到达目标位置是每个开发者都会遇到的经典问题。想象一下当玩家点击某个房间位置时角色需要自动找到最优路线而不是直线穿过墙壁——这正是A算法大显身手的场景。本文将带你从零构建一个可直接集成到Unity项目的A寻路系统包含完整C#实现、性能优化技巧以及与Unity工作流的无缝对接方案。1. A*算法核心原理与工程化思考A算法之所以成为游戏寻路的黄金标准在于它巧妙平衡了路径最优性和计算效率。与Dijkstra算法盲目搜索或贪心算法可能陷入局部最优不同A通过启发式评估函数实现了智能路径探索。关键数据结构与代价计算public class PathNode : IComparablePathNode { public Vector2Int GridPosition; public bool IsWalkable; public float GCost; // 起点到当前点的实际代价 public float HCost; // 当前点到终点的预估代价 public float FCost GCost HCost; public PathNode Parent; public int CompareTo(PathNode other) FCost.CompareTo(other.FCost); }代价计算策略对比表移动类型直线代价对角线代价适用场景曼哈顿距离10-网格严格对齐移动欧几里得距离1014.14更自然的斜向移动切比雪夫距离1010八方向自由移动提示在Unity的Tilemap环境中建议使用欧几里得距离计算可以获得更平滑的移动路径。实际项目中应根据游戏类型选择最适合的代价策略。2. Unity工程化实现全流程2.1 地图数据预处理将Unity的Tilemap转换为可寻路网格是第一步。我们需要创建一个高效的转换器public class PathfindingGrid : MonoBehaviour { [SerializeField] private Tilemap _obstacleTilemap; private PathNode[,] _grid; private void Awake() { BoundsInt bounds _obstacleTilemap.cellBounds; _grid new PathNode[bounds.size.x, bounds.size.y]; for (int x 0; x bounds.size.x; x) { for (int y 0; y bounds.size.y; y) { Vector3Int cellPos new Vector3Int( x bounds.xMin, y bounds.yMin, 0); _grid[x, y] new PathNode { GridPosition new Vector2Int(x, y), IsWalkable !_obstacleTilemap.HasTile(cellPos) }; } } } }2.2 核心算法实现优化后的A*算法实现包含以下关键改进使用优先队列(最小堆)管理开放列表提前终止条件检测动态权重调整机制public class AStarPathfinder : MonoBehaviour { public ListVector2Int FindPath(Vector2Int start, Vector2Int end) { var openSet new PriorityQueuePathNode(); var closedSet new HashSetVector2Int(); PathNode startNode _grid[start.x, start.y]; openSet.Enqueue(startNode); while (openSet.Count 0) { PathNode currentNode openSet.Dequeue(); if (currentNode.GridPosition end) { return RetracePath(startNode, currentNode); } closedSet.Add(currentNode.GridPosition); foreach (var neighbor in GetNeighbors(currentNode)) { if (!neighbor.IsWalkable || closedSet.Contains(neighbor.GridPosition)) continue; float newGCost currentNode.GCost GetDistance(currentNode, neighbor); if (newGCost neighbor.GCost || !openSet.Contains(neighbor)) { neighbor.GCost newGCost; neighbor.HCost GetDistance(neighbor, end); neighbor.Parent currentNode; if (!openSet.Contains(neighbor)) openSet.Enqueue(neighbor); } } } return null; // 路径不存在 } private float GetDistance(PathNode a, PathNode b) { int dx Mathf.Abs(a.GridPosition.x - b.GridPosition.x); int dy Mathf.Abs(a.GridPosition.y - b.GridPosition.y); return dx dy ? 14.14f * dy 10f * (dx - dy) : 14.14f * dx 10f * (dy - dx); } }3. 与Unity工作流深度集成3.1 可视化调试工具开发阶段的可视化工具能极大提升调试效率[ExecuteInEditMode] public class PathfindingDebugger : MonoBehaviour { [SerializeField] private Color _walkableColor Color.white; [SerializeField] private Color _obstacleColor Color.red; [SerializeField] private Color _pathColor Color.green; private void OnDrawGizmos() { if (_grid null) return; for (int x 0; x _grid.GetLength(0); x) { for (int y 0; y _grid.GetLength(1); y) { Gizmos.color _grid[x,y].IsWalkable ? _walkableColor : _obstacleColor; Gizmos.DrawCube(new Vector3(x, y, 0), Vector3.one * 0.9f); } } } }3.2 动态障碍物支持通过事件系统实现动态障碍物更新public class DynamicObstacle : MonoBehaviour { private void OnEnable() { PathfindingSystem.Instance.RegisterObstacle(this); } private void OnDisable() { PathfindingSystem.Instance.UnregisterObstacle(this); } public void UpdateGridStatus() { Vector2Int gridPos PathfindingSystem.WorldToGrid(transform.position); _grid[gridPos.x, gridPos.y].IsWalkable false; } }4. 高级优化技巧与实战经验4.1 分层寻路策略对于大型地图采用分层处理可显著提升性能第一层区域划分房间、场景区块第二层区域间路径规划第三层区域内精确寻路public class HierarchicalPathfinder { public ListVector3 FindPath(Vector3 start, Vector3 end) { var highLevelPath FindHighLevelPath(start, end); var detailedPath new ListVector3(); for (int i 0; i highLevelPath.Count - 1; i) { detailedPath.AddRange( _aStar.FindPath(highLevelPath[i], highLevelPath[i1]) ); } return SmoothPath(detailedPath); } }4.2 路径平滑处理原始A*路径往往存在锯齿现象采用以下方法优化贝塞尔曲线平滑算法public ListVector3 SmoothPath(ListVector3 inputPath, float tension 0.5f) { if (inputPath.Count 3) return inputPath; var smoothedPath new ListVector3 { inputPath[0] }; for (int i 1; i inputPath.Count - 1; i) { Vector3 prev inputPath[i-1]; Vector3 current inputPath[i]; Vector3 next inputPath[i1]; Vector3 control1 current (prev - current) * tension; Vector3 control2 current (next - current) * tension; for (float t 0; t 1; t 0.1f) { smoothedPath.Add(CalculateBezierPoint( current, control1, control2, current, t)); } } smoothedPath.Add(inputPath[^1]); return smoothedPath; }4.3 多线程处理方案对于需要大量寻路的RTS类游戏实现异步寻路至关重要public class AsyncPathRequestManager : MonoBehaviour { private struct PathResult { public Vector3[] Path; public ActionVector3[] Callback; } private QueuePathResult _resultsQueue new QueuePathResult(); public void RequestPath(Vector3 start, Vector3 end, ActionVector3[] callback) { ThreadPool.QueueUserWorkItem(_ { Vector3[] path PathfindingSystem.Instance.FindPath(start, end); lock (_resultsQueue) { _resultsQueue.Enqueue(new PathResult { Path path, Callback callback }); } }); } private void Update() { if (_resultsQueue.Count 0) { PathResult result; lock (_resultsQueue) { result _resultsQueue.Dequeue(); } result.Callback?.Invoke(result.Path); } } }5. 性能分析与优化策略通过Profiler分析发现A*算法90%的时间消耗在开放列表的操作上。以下是经过验证的优化方案优先队列优化实现基于二叉堆的优先队列使插入和提取操作降至O(log n)缓存机制对频繁访问的路径进行缓存早期终止当路径代价超过阈值时提前终止跳跃点搜索减少需要评估的节点数量优化前后性能对比表地图尺寸原始版本(ms)优化版本(ms)提升幅度50×5012.33.274%100×10048.711.576%200×200210.445.278%注意实际项目中应根据游戏类型平衡精度与性能。对于塔防等固定地图游戏可预计算所有可能路径而对开放世界游戏则需要动态局部寻路与全局路径相结合。

更多文章