Unity中3D Spider寻路实现求助:支持墙顶行走及物体距离限制
解决方案思路与实现
针对你的类蜘蛛生物寻路需求(多表面行走、固定距离约束、Unity环境、预生成路径图),推荐采用离线表面采样+拓扑图构建+A*寻路的方案,以下是具体实现细节:
核心设计方向
- 离线预生成:在构建前完成所有路径节点和连接的计算,耗时不影响运行时性能
- 距离约束保证:所有路径节点沿表面法线偏移固定距离,且节点间连接经过无障碍验证
- 低内存高效寻路:用轻量化拓扑图存储节点信息,运行时仅需基于A*在图上搜索
离线路径图生成步骤
1. 表面采样与节点筛选
遍历场景中所有可行走表面(通过Layer标记),对每个表面的三角面进行采样:
- 取三角面中心点,沿表面法线方向偏移指定距离得到路径节点位置
- 用SphereCast检查偏移后的位置是否处于无障碍空间(避免穿墙或挤压)
- 自适应采样:在曲率较高的区域(如角落、凹凸面)增加采样密度,平坦区域降低密度以节省内存
2. 拓扑连接构建
为每个节点建立合法的相邻连接:
- 搜索节点周围一定范围内的其他节点
- 检查两点间的路径是否符合要求:分段验证每个中间点是否能沿表面保持固定距离,且法线方向变化平滑(避免蜘蛛姿态突变)
- 仅保留合法连接,形成无向拓扑图
3. 路径图序列化
将生成的节点和连接数据序列化(如JSON、二进制),运行时直接加载使用
关键代码实现
离线路径节点生成代码
using UnityEngine; using System.Collections.Generic; public class SpiderPathGenerator : MonoBehaviour { public LayerMask walkableLayers; public float sampleSpacing = 1f; public float offsetDistance = 0.5f; public float obstacleCheckRadius = 0.3f; public List<PathNode> pathNodes = new List<PathNode>(); [System.Serializable] public class PathNode { public Vector3 position; public Vector3 normal; public List<int> neighborIndices = new List<int>(); } // 生成路径节点(编辑器模式下运行) public void GeneratePathNodes() { pathNodes.Clear(); Collider[] colliders = Physics.OverlapBox(Vector3.zero, new Vector3(100, 100, 100), Quaternion.identity, walkableLayers); foreach (Collider col in colliders) { MeshFilter meshFilter = col.GetComponent<MeshFilter>(); if (meshFilter == null) continue; Mesh mesh = meshFilter.mesh; int[] triangles = mesh.triangles; // 遍历所有三角面采样 for (int i = 0; i < triangles.Length; i += 3) { Vector3 v1 = mesh.vertices[triangles[i]]; Vector3 v2 = mesh.vertices[triangles[i+1]]; Vector3 v3 = mesh.vertices[triangles[i+2]]; Vector3 faceCenter = (v1 + v2 + v3) / 3f; Vector3 faceNormal = mesh.normals[triangles[i]]; // 转换到世界空间 Vector3 worldCenter = col.transform.TransformPoint(faceCenter); Vector3 worldNormal = col.transform.TransformDirection(faceNormal).normalized; Vector3 offsetPos = worldCenter + worldNormal * offsetDistance; // 验证偏移点是否无障碍 if (!Physics.CheckSphere(offsetPos, obstacleCheckRadius, ~walkableLayers)) { pathNodes.Add(new PathNode { position = offsetPos, normal = worldNormal }); } } } BuildNeighborConnections(); Debug.Log($"生成路径节点数:{pathNodes.Count}"); } // 构建节点间的合法连接 private void BuildNeighborConnections() { float connectThreshold = sampleSpacing * 1.5f; for (int i = 0; i < pathNodes.Count; i++) { PathNode currentNode = pathNodes[i]; for (int j = i + 1; j < pathNodes.Count; j++) { PathNode targetNode = pathNodes[j]; float distance = Vector3.Distance(currentNode.position, targetNode.position); if (distance > connectThreshold) continue; if (IsConnectionValid(currentNode.position, targetNode.position)) { currentNode.neighborIndices.Add(j); targetNode.neighborIndices.Add(i); } } } } // 验证两点间的连接是否符合距离约束 private bool IsConnectionValid(Vector3 start, Vector3 end) { int checkSteps = 4; float stepLength = Vector3.Distance(start, end) / checkSteps; Vector3 direction = (end - start).normalized; for (int i = 1; i < checkSteps; i++) { Vector3 checkPos = start + direction * stepLength * i; // 向下射线检测最近表面,验证偏移后位置是否与当前检查点一致 if (Physics.Raycast(checkPos, Vector3.down, out RaycastHit hit, offsetDistance * 2f, walkableLayers)) { Vector3 expectedPos = hit.point + hit.normal * offsetDistance; if (Vector3.Distance(checkPos, expectedPos) > 0.1f) { return false; } } else { return false; } } return true; } // 保存路径图到本地文件 public void SavePathGraph() { string json = JsonUtility.ToJson(new PathNodeCollection { nodes = pathNodes }, true); System.IO.File.WriteAllText(Application.dataPath + "/SpiderPathGraph.json", json); } [System.Serializable] private class PathNodeCollection { public List<PathNode> nodes; } }
运行时A*寻路代码
using UnityEngine; using System.Collections.Generic; public class SpiderPathfinder { private List<SpiderPathGenerator.PathNode> _pathNodes; public SpiderPathfinder(List<SpiderPathGenerator.PathNode> pathNodes) { _pathNodes = pathNodes; } // 查找从起点到终点的路径 public List<Vector3> FindPath(Vector3 startWorldPos, Vector3 endWorldPos) { int startNodeIdx = FindClosestNode(startWorldPos); int endNodeIdx = FindClosestNode(endWorldPos); if (startNodeIdx == -1 || endNodeIdx == -1) return null; Dictionary<int, float> gScores = new Dictionary<int, float>(); Dictionary<int, int> cameFrom = new Dictionary<int, int>(); PriorityQueue<int, float> openSet = new PriorityQueue<int, float>(); // 初始化G值 for (int i = 0; i < _pathNodes.Count; i++) { gScores[i] = float.MaxValue; } gScores[startNodeIdx] = 0; openSet.Enqueue(startNodeIdx, Heuristic(startNodeIdx, endNodeIdx)); while (openSet.Count > 0) { int currentIdx = openSet.Dequeue(); if (currentIdx == endNodeIdx) { return ReconstructPath(startNodeIdx, endNodeIdx, cameFrom); } foreach (int neighborIdx in _pathNodes[currentIdx].neighborIndices) { float tentativeG = gScores[currentIdx] + Vector3.Distance(_pathNodes[currentIdx].position, _pathNodes[neighborIdx].position); if (tentativeG < gScores[neighborIdx]) { cameFrom[neighborIdx] = currentIdx; gScores[neighborIdx] = tentativeG; openSet.Enqueue(neighborIdx, tentativeG + Heuristic(neighborIdx, endNodeIdx)); } } } return null; // 无可行路径 } // 曼哈顿启发函数(可替换为欧氏距离) private float Heuristic(int aIdx, int bIdx) { return Vector3.Distance(_pathNodes[aIdx].position, _pathNodes[bIdx].position); } // 重构路径 private List<Vector3> ReconstructPath(int startIdx, int endIdx, Dictionary<int, int> cameFrom) { List<Vector3> path = new List<Vector3>(); int currentIdx = endIdx; while (currentIdx != startIdx) { path.Add(_pathNodes[currentIdx].position); if (!cameFrom.TryGetValue(currentIdx, out currentIdx)) { return null; } } path.Add(_pathNodes[startIdx].position); path.Reverse(); return path; } // 找到离目标位置最近的路径节点 private int FindClosestNode(Vector3 targetPos) { float minDistance = float.MaxValue; int closestIdx = -1; for (int i = 0; i < _pathNodes.Count; i++) { float distance = Vector3.Distance(targetPos, _pathNodes[i].position); if (distance < minDistance) { minDistance = distance; closestIdx = i; } } return closestIdx; } } // 简易优先队列实现 public class PriorityQueue<T, U> where U : System.IComparable<U> { private List<KeyValuePair<T, U>> _items = new List<KeyValuePair<T, U>>(); public int Count => _items.Count; public void Enqueue(T item, U priority) { _items.Add(new KeyValuePair<T, U>(item, priority)); _items.Sort((x, y) => x.Value.CompareTo(y.Value)); } public T Dequeue() { T item = _items[0].Key; _items.RemoveAt(0); return item; } }
优化建议
- 空间划分加速:用Grid或Octree管理采样节点,构建连接时仅查询相邻单元格的节点,减少遍历次数
- 节点合并优化:对位置接近、法线方向一致的节点进行合并,降低总节点数和内存占用
- 法线平滑处理:寻路后对路径节点的法线进行插值,让蜘蛛移动姿态更自然
- 数据压缩存储:序列化时用定点数存储Vector3数据,减少文件大小和加载后的内存占用
注意事项
- 动态障碍物处理:若场景存在动态物体,运行时需额外用射线检查路径是否被阻挡,临时禁用对应节点
- 蜘蛛移动控制:沿路径移动时,每帧用Raycast检测当前位置的表面,微调蜘蛛位置以保持固定偏移距离
内容的提问来源于stack exchange,提问作者PHOENIX33201
相关产品推荐
相关产品推荐

