using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif using IndianOceanAssets.Engine2_5D; namespace IndianOceanAssets.EditorTools { /// /// 随机出生点生成器 —— 在地图边界内随机分布指定数量的 EnemySpawnPoint。 /// 复用 Sprite Physics Shape / PolygonCollider2D 的边界数据。 /// /// 使用方式: /// 1. 场景中创建一个空物体,挂上 SpawnPointGenerator 组件 /// 2. 指定 groundSprite 或 polygonCollider(同 BoundaryWallGenerator) /// 3. Inspector 中设置数量和参数,点 "Generate Spawn Points" /// public class SpawnPointGenerator : MonoBehaviour { [Header("数据源")] [SerializeField] SpriteRenderer groundSprite; [SerializeField] PolygonCollider2D polygonCollider; [Header("生成参数")] [Tooltip("要生成的出生点数量")] [SerializeField] int spawnCount = 50; [Tooltip("出生点离边界的最小距离(防止刷在墙边)")] [SerializeField] float borderMargin = 0.5f; [Tooltip("出生点之间的最小间距(防止重叠)")] [SerializeField] float minDistance = 1.0f; [Tooltip("随机种子(0 = 每次随机)")] [SerializeField] int seed = 0; /// /// 从数据源提取边界点(世界坐标),与 BoundaryWallGenerator 一致。 /// 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; } public SpriteRenderer GroundSprite => groundSprite; public PolygonCollider2D PolygonCollider => polygonCollider; public int SpawnCount => spawnCount; public float BorderMargin => borderMargin; public float MinDistance => minDistance; public int Seed => seed; private void OnDrawGizmosSelected() { var paths = GetBoundaryPaths(); if (paths.Count == 0) return; // 绘制边界多边形 Gizmos.color = new Color(0f, 1f, 0f, 0.8f); // 绿色 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); Gizmos.DrawSphere(cur, 0.1f); } } // 绘制包围盒 if (paths.Count > 0) { float minX = float.MaxValue, maxX = float.MinValue; float minZ = float.MaxValue, maxZ = float.MinValue; foreach (var path in paths) { foreach (var p in path) { if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.z < minZ) minZ = p.z; if (p.z > maxZ) maxZ = p.z; } } Gizmos.color = new Color(1f, 1f, 0f, 0.3f); // 黄色 Vector3 center = new Vector3((minX + maxX) / 2, 0, (minZ + maxZ) / 2); Vector3 size = new Vector3(maxX - minX, 0.1f, maxZ - minZ); Gizmos.DrawWireCube(center, size); } } } #if UNITY_EDITOR [CustomEditor(typeof(SpawnPointGenerator))] public class SpawnPointGeneratorEditor : UnityEditor.Editor { public override void OnInspectorGUI() { DrawDefaultInspector(); var gen = (SpawnPointGenerator)target; EditorGUILayout.Space(); // 边界信息 var paths = gen.GetBoundaryPaths(); if (paths.Count > 0) { int total = 0; foreach (var p in paths) total += p.Count; EditorGUILayout.LabelField($"边界: {paths.Count} 条路径, {total} 个点"); } else { EditorGUILayout.HelpBox("未检测到边界数据,请指定 groundSprite 或 polygonCollider。", MessageType.Warning); return; } if (GUILayout.Button("Generate Spawn Points", GUILayout.Height(36))) { GenerateSpawnPoints(gen, paths); } if (GUILayout.Button("Clear Spawn Points", GUILayout.Height(24))) { ClearSpawnPoints(); } } void GenerateSpawnPoints(SpawnPointGenerator gen, List> paths) { // 计算包围盒 float minX = float.MaxValue, maxX = float.MinValue; float minZ = float.MaxValue, maxZ = float.MinValue; foreach (var path in paths) { foreach (var p in path) { if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.z < minZ) minZ = p.z; if (p.z > maxZ) maxZ = p.z; } } // 使用主路径(最大的路径)做点内测试 var mainPath = paths[0]; for (int i = 1; i < paths.Count; i++) { if (paths[i].Count > mainPath.Count) mainPath = paths[i]; } // 收集其他路径(可能是空洞/岛屿) var otherPaths = new List>(); foreach (var path in paths) { if (path != mainPath) otherPaths.Add(path); } // 调试:输出多边形信息 Debug.Log($"[SpawnPointGenerator] 共 {paths.Count} 条路径,主路径 {mainPath.Count} 个点"); for (int i = 0; i < paths.Count; i++) { Debug.Log($" 路径{i}: {paths[i].Count} 个点"); } Debug.Log($" 包围盒: X=[{minX:F2}, {maxX:F2}] Z=[{minZ:F2}, {maxZ:F2}]"); int seed = gen.Seed; if (seed == 0) seed = Random.Range(1, 999999); var rng = new System.Random(seed); float margin = gen.BorderMargin; float minDist = gen.MinDistance; int targetCount = gen.SpawnCount; var generated = new List(); int maxAttempts = targetCount * 200; // 防止死循环 int attempts = 0; while (generated.Count < targetCount && attempts < maxAttempts) { attempts++; // 在包围盒内随机一个点 float rx = (float)(minX + rng.NextDouble() * (maxX - minX)); float rz = (float)(minZ + rng.NextDouble() * (maxZ - minZ)); Vector3 candidate = new Vector3(rx, 0f, rz); // 检查是否在主多边形内(带边距) if (!IsInsidePolygon(candidate, mainPath, margin)) continue; // 检查是否在其他多边形(空洞)内 — 如果在则排除 bool inHole = false; foreach (var otherPath in otherPaths) { if (IsInsidePolygon(candidate, otherPath, 0f)) { inHole = true; break; } } if (inHole) continue; // 检查与已有点的距离 bool tooClose = false; foreach (var existing in generated) { if (Vector3.Distance(candidate, existing) < minDist) { tooClose = true; break; } } if (tooClose) continue; generated.Add(candidate); } // 创建 GameObject GameObject container = GameObject.Find("Generated_SpawnPoints"); if (container == null) { container = new GameObject("Generated_SpawnPoints"); Undo.RegisterCreatedObjectUndo(container, "Create SpawnPoints Container"); } // 清除旧的 while (container.transform.childCount > 0) { Undo.DestroyObjectImmediate(container.transform.GetChild(0).gameObject); } for (int i = 0; i < generated.Count; i++) { GameObject sp = new GameObject($"SpawnPoint_{i}"); sp.transform.SetParent(container.transform, false); sp.transform.position = generated[i]; var spComp = sp.AddComponent(); spComp.SetIndex(i); Undo.RegisterCreatedObjectUndo(sp, $"Create SpawnPoint_{i}"); EditorUtility.SetDirty(spComp); } Debug.Log($"[SpawnPointGenerator] 生成 {generated.Count} 个出生点(尝试 {attempts} 次,种子 {seed})"); EditorUtility.SetDirty(container); } void ClearSpawnPoints() { GameObject container = GameObject.Find("Generated_SpawnPoints"); if (container != null) { Undo.DestroyObjectImmediate(container); Debug.Log("[SpawnPointGenerator] 已清除所有出生点"); } } /// /// 判断点是否在闭合多边形内(XZ 平面),可选内缩边距。 /// 使用射线法(Ray Casting)。 /// bool IsInsidePolygon(Vector3 point, List polygon, float margin) { int count = polygon.Count; // 先检查离每条边的距离,如果太近则排除 if (margin > 0f) { for (int i = 0; i < count; i++) { Vector3 a = polygon[i]; Vector3 b = polygon[(i + 1) % count]; float dist = PointToSegmentDistanceXZ(point, a, b); if (dist < margin) return false; } } // 射线法判断点是否在多边形内(XZ 平面,沿 X 轴发射射线) bool inside = false; for (int i = 0, j = count - 1; i < count; j = i++) { float zi = polygon[i].z; float zj = polygon[j].z; float xi = polygon[i].x; float xj = polygon[j].x; if ((zi > point.z) != (zj > point.z)) { float intersectX = (xj - xi) * (point.z - zi) / (zj - zi) + xi; if (point.x < intersectX) inside = !inside; } } return inside; } /// /// XZ 平面上点到线段的距离。 /// float PointToSegmentDistanceXZ(Vector3 p, Vector3 a, Vector3 b) { Vector3 ab = new Vector3(b.x - a.x, 0, b.z - a.z); Vector3 ap = new Vector3(p.x - a.x, 0, p.z - a.z); float t = Vector3.Dot(ap, ab) / ab.sqrMagnitude; t = Mathf.Clamp01(t); Vector3 proj = a + t * ab; return Vector3.Distance(new Vector3(p.x, 0, p.z), new Vector3(proj.x, 0, proj.z)); } } #endif }