using System.Collections.Generic; using UnityEngine; namespace IndianOceanAssets.Engine2_5D { /// /// 空气墙自动生成器 —— 从 Sprite Physics Shape 或 PolygonCollider2D 提取边界, /// 沿边界路径生成 3D 碰撞墙,阻止玩家 / 敌人走出地图。 /// /// 使用方式: /// 1. 场景中挂有此脚本的 GameObject(与 GroundBuilder 放在同一位置即可) /// 2. 指定 groundSprite 或 polygonCollider 作为数据源 /// 3. Inspector 点 "Build Walls" 按钮生成碰撞墙 /// 4. 点 "Clear Walls" 可清除已生成的碰撞墙 /// public class BoundaryWallGenerator : MonoBehaviour { // ============================================================ // 数据源 // ============================================================ [Header("数据源")] [Tooltip("地面精灵渲染器。从此 Sprite 的 Physics Shape 自动提取边界轮廓。\n" + "需要在 Sprite Editor 里 Generate Physics Shape(默认自动生成)。")] [SerializeField] SpriteRenderer groundSprite; [Tooltip("可选:直接用 PolygonCollider2D 的路径作为边界。\n" + "留空则用 Sprite Physics Shape。")] [SerializeField] PolygonCollider2D polygonCollider; // ============================================================ // 碰撞设置 // ============================================================ [Header("碰撞设置")] [Tooltip("碰撞墙高度(Y 轴方向延伸)")] [SerializeField] float wallHeight = 3f; [Tooltip("碰撞墙厚度(XZ 平面法线方向)。BoxCollider 模式下建议 ≥ 0.3,太薄容易穿透。")] [SerializeField] float wallThickness = 0.5f; [Tooltip("碰撞墙所在的 Layer")] [SerializeField] int wallLayer = 0; [Tooltip("碰撞模式:BoxCollider 阵列(推荐,有厚度,碰撞可靠);MeshCollider(零厚度,可能穿透)。")] [SerializeField] ColliderMode colliderMode = ColliderMode.BoxColliderArray; [Header("调试")] [Tooltip("勾选后为每段墙体添加可见 Cube,方便在 Scene 中查看位置和大小")] [SerializeField] bool showDebugVisuals = true; [Header("优化")] [Tooltip("Douglas-Peucker 简化容差(世界单位)。0 = 不简化。\n" + "建议 0.05~0.2,减少顶点数同时保持轮廓形状。")] [SerializeField] float simplifyTolerance = 0.1f; public enum ColliderMode { MeshCollider, BoxColliderArray } private const string GENERATED = "Generated_BoundaryWalls"; // ============================================================ // 公开 API // ============================================================ /// /// 从数据源提取边界点(世界坐标)。与 GroundBuilder 逻辑一致。 /// public List> GetBoundaryPaths() { var paths = new List>(); if (polygonCollider != null) { for (int i = 0; i < polygonCollider.pathCount; i++) { Vector2[] path = polygonCollider.GetPath(i); var worldPath = new List(); foreach (var p in path) { Vector3 world = polygonCollider.transform.TransformPoint(p.x, p.y, 0f); worldPath.Add(world); } if (worldPath.Count >= 3) paths.Add(worldPath); } } else if (groundSprite != null && groundSprite.sprite != null) { Sprite sprite = groundSprite.sprite; int shapeCount = sprite.GetPhysicsShapeCount(); for (int i = 0; i < shapeCount; i++) { var shape = new List(); sprite.GetPhysicsShape(i, shape); var worldPath = new List(); foreach (var p in shape) { Vector3 world = groundSprite.transform.TransformPoint(p.x, p.y, 0f); worldPath.Add(world); } if (worldPath.Count >= 3) paths.Add(worldPath); } } return paths; } /// /// 生成碰撞墙。 /// [ContextMenu("Build Walls")] public void Build() { var paths = GetBoundaryPaths(); if (paths.Count == 0) { Debug.LogWarning("[BoundaryWallGenerator] 没有找到边界数据。\n" + "请指定 groundSprite(Sprite 需有 Physics Shape)或 polygonCollider。"); return; } // 简化 if (simplifyTolerance > 0f) { for (int i = 0; i < paths.Count; i++) paths[i] = SimplifyClosedPath(paths[i], simplifyTolerance); } // 清除旧的 ClearGenerated(); // 创建容器(放在场景根层级,确保 mesh 世界坐标 = 本地坐标) GameObject container = new GameObject(GENERATED); container.transform.position = Vector3.zero; container.transform.rotation = Quaternion.identity; container.transform.localScale = Vector3.one; container.layer = wallLayer; container.isStatic = true; switch (colliderMode) { case ColliderMode.MeshCollider: BuildMeshColliders(container, paths); break; case ColliderMode.BoxColliderArray: BuildBoxColliders(container, paths); break; } int totalEdges = 0; foreach (var p in paths) totalEdges += p.Count; Debug.Log($"[BoundaryWallGenerator] 生成完成:{paths.Count} 条路径,{totalEdges} 个边界点," + $"碰撞模式 {colliderMode},墙高 {wallHeight},厚度 {wallThickness}。"); } /// /// 清除所有已生成的碰撞墙。 /// [ContextMenu("Clear Walls")] public void ClearGenerated() { // 容器在场景根层级(非子物体),用 FindObjectOfType 查找 GameObject old = GameObject.Find(GENERATED); if (old != null) { if (Application.isPlaying) Destroy(old); else DestroyImmediate(old); } } // ============================================================ // MeshCollider 模式 // ============================================================ void BuildMeshColliders(GameObject container, List> paths) { for (int i = 0; i < paths.Count; i++) { var path = paths[i]; GameObject wallObj = new GameObject($"BoundaryWall_{i}"); wallObj.transform.SetParent(container.transform, false); wallObj.layer = wallLayer; wallObj.isStatic = true; MeshFilter mf = wallObj.AddComponent(); mf.sharedMesh = GenerateWallMesh(path); // 不添加 MeshRenderer —— 空气墙只需要碰撞,不需要渲染(无阴影、无绘制开销) MeshCollider mc = wallObj.AddComponent(); mc.sharedMesh = mf.sharedMesh; mc.convex = false; } } /// /// 沿闭合路径生成垂直墙体 Mesh(双面),用于 MeshCollider。 /// 直接使用世界坐标(容器物体与 BoundaryWallGenerator 同 Transform,本地偏移为零)。 /// Mesh GenerateWallMesh(List worldPoints) { int n = worldPoints.Count; // 顶点:top ring (0..n-1) + bottom ring (n..2n-1) Vector3[] verts = new Vector3[n * 2]; Vector3[] norms = new Vector3[n * 2]; for (int i = 0; i < n; i++) { int next = (i + 1) % n; verts[i] = worldPoints[i] + new Vector3(0, wallHeight, 0); // 顶部 verts[n + i] = worldPoints[i]; // 底部(地面高度) // 法线:边的垂直方向,朝外 Vector3 edge = worldPoints[next] - worldPoints[i]; Vector3 normal = Vector3.Cross(edge, Vector3.down).normalized; norms[i] = normal; norms[n + i] = normal; } // 三角形:双面 var tris = new List(n * 12); for (int i = 0; i < n; i++) { int next = (i + 1) % n; int ti = i, tj = next, bi = n + i, bj = n + next; // 正面 tris.Add(ti); tris.Add(bi); tris.Add(bj); tris.Add(ti); tris.Add(bj); tris.Add(tj); // 背面(反绕) tris.Add(ti); tris.Add(bj); tris.Add(bi); tris.Add(ti); tris.Add(tj); tris.Add(bj); } Mesh mesh = new Mesh { name = "BoundaryWall" }; mesh.SetVertices(verts); mesh.SetNormals(norms); mesh.SetTriangles(tris, 0); mesh.RecalculateBounds(); return mesh; } // ============================================================ // BoxCollider 阵列模式 // ============================================================ void BuildBoxColliders(GameObject container, List> paths) { int segIndex = 0; for (int p = 0; p < paths.Count; p++) { var path = paths[p]; int n = path.Count; for (int i = 0; i < n; i++) { int next = (i + 1) % n; Vector3 a = path[i]; Vector3 b = path[next]; // 边段中点(世界坐标) Vector3 mid = (a + b) * 0.5f; float length = Vector3.Distance(a, b); if (length < 0.001f) continue; // 边段方向(投影到 XZ 平面,忽略 Y 分量) Vector3 dir = (b - a).normalized; dir.y = 0f; if (dir.sqrMagnitude < 0.0001f) continue; dir.Normalize(); // 创建子物体 GameObject segObj = new GameObject($"WallSeg_{segIndex}"); segObj.transform.SetParent(container.transform, false); segObj.layer = wallLayer; segObj.isStatic = true; // 定位到中点,Y 偏移到墙高一半 segObj.transform.position = mid + new Vector3(0, wallHeight * 0.5f, 0); // 旋转使 Z 轴沿边段方向(LookRotation: forward=Z 对齐 dir,up=Y 保持朝上) segObj.transform.rotation = Quaternion.LookRotation(dir, Vector3.up); // 添加 BoxCollider:X=厚度, Y=墙高, Z=边段长度 BoxCollider bc = segObj.AddComponent(); bc.size = new Vector3(wallThickness, wallHeight, length); bc.center = Vector3.zero; // 调试可视化:添加 Cube 让墙体可见 if (showDebugVisuals) { GameObject vis = GameObject.CreatePrimitive(PrimitiveType.Cube); vis.name = "Visual"; vis.transform.SetParent(segObj.transform, false); vis.transform.localPosition = Vector3.zero; vis.transform.localRotation = Quaternion.identity; vis.transform.localScale = new Vector3(wallThickness, wallHeight, length); vis.isStatic = false; // 移除 Cube 自带的 Collider,避免双重碰撞 var cubeCol = vis.GetComponent(); if (cubeCol != null) DestroyImmediate(cubeCol); } segIndex++; } } } // ============================================================ // Douglas-Peucker 路径简化(与 GroundBuilder 一致) // ============================================================ List SimplifyClosedPath(List points, float tolerance) { if (points.Count < 4) return points; // 找离 points[0] 最远的点 float maxDist = 0f; int farIdx = 1; for (int i = 1; i < points.Count; i++) { float d = Vector3.Distance(points[0], points[i]); if (d > maxDist) { maxDist = d; farIdx = i; } } var arc1 = new List(); var arc2 = new List(); for (int i = 0; i <= farIdx; i++) arc1.Add(points[i]); for (int i = farIdx; i < points.Count; i++) arc2.Add(points[i]); arc2.Add(points[0]); arc1 = DouglasPeucker(arc1, tolerance); arc2 = DouglasPeucker(arc2, tolerance); var result = new List(arc1); for (int i = 1; i < arc2.Count - 1; i++) result.Add(arc2[i]); return result; } List DouglasPeucker(List points, float tolerance) { if (points.Count < 3) return points; float maxDist = 0f; int maxIdx = 0; Vector3 a = points[0]; Vector3 b = points[points.Count - 1]; for (int i = 1; i < points.Count - 1; i++) { float d = PerpDistance(points[i], a, b); if (d > maxDist) { maxDist = d; maxIdx = i; } } if (maxDist > tolerance) { var left = DouglasPeucker(points.GetRange(0, maxIdx + 1), tolerance); var right = DouglasPeucker(points.GetRange(maxIdx, points.Count - maxIdx), tolerance); var result = new List(left); result.AddRange(right.GetRange(1, right.Count - 1)); return result; } else { return new List { a, b }; } } float PerpDistance(Vector3 p, Vector3 lineA, Vector3 lineB) { Vector3 ab = lineB - lineA; float lenSq = ab.sqrMagnitude; if (lenSq < 1e-8f) return Vector3.Distance(p, lineA); Vector3 ap = p - lineA; float t = Vector3.Dot(ap, ab) / lenSq; t = Mathf.Clamp01(t); Vector3 proj = lineA + t * ab; return Vector3.Distance(p, proj); } // ============================================================ // Gizmos —— 在 Scene 视图中预览碰撞边界 // ============================================================ private void OnDrawGizmosSelected() { var paths = GetBoundaryPaths(); if (paths.Count == 0) return; Gizmos.color = new Color(1f, 0.4f, 0.2f, 0.8f); // 橙色,区别于 GroundBuilder 的蓝色 foreach (var path in paths) { for (int i = 0; i < path.Count; i++) { Vector3 cur = path[i]; Vector3 next = path[(i + 1) % path.Count]; // 底部线 Gizmos.DrawLine(cur, next); // 顶部线 Vector3 curTop = cur + new Vector3(0, wallHeight, 0); Vector3 nextTop = next + new Vector3(0, wallHeight, 0); Gizmos.DrawLine(curTop, nextTop); // 垂直连接线 Gizmos.DrawLine(cur, curTop); Gizmos.DrawSphere(cur, 0.1f); } } } } }