You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中3D Spider寻路实现求助:支持墙顶行走及物体距离限制

解决方案思路与实现

针对你的类蜘蛛生物寻路需求(多表面行走、固定距离约束、Unity环境、预生成路径图),推荐采用离线表面采样+拓扑图构建+A*寻路的方案,以下是具体实现细节:

核心设计方向

  1. 离线预生成:在构建前完成所有路径节点和连接的计算,耗时不影响运行时性能
  2. 距离约束保证:所有路径节点沿表面法线偏移固定距离,且节点间连接经过无障碍验证
  3. 低内存高效寻路:用轻量化拓扑图存储节点信息,运行时仅需基于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;
    }
}

优化建议

  1. 空间划分加速:用Grid或Octree管理采样节点,构建连接时仅查询相邻单元格的节点,减少遍历次数
  2. 节点合并优化:对位置接近、法线方向一致的节点进行合并,降低总节点数和内存占用
  3. 法线平滑处理:寻路后对路径节点的法线进行插值,让蜘蛛移动姿态更自然
  4. 数据压缩存储:序列化时用定点数存储Vector3数据,减少文件大小和加载后的内存占用

注意事项

  • 动态障碍物处理:若场景存在动态物体,运行时需额外用射线检查路径是否被阻挡,临时禁用对应节点
  • 蜘蛛移动控制:沿路径移动时,每帧用Raycast检测当前位置的表面,微调蜘蛛位置以保持固定偏移距离

内容的提问来源于stack exchange,提问作者PHOENIX33201

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 18:54:55