Files
gold_dolphin/unity/Assets/enemy/SpawnPointGenerator.cs
T
2026-07-06 01:06:18 +08:00

356 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
using IndianOceanAssets.Engine2_5D;
namespace IndianOceanAssets.EditorTools
{
/// <summary>
/// 随机出生点生成器 —— 在地图边界内随机分布指定数量的 EnemySpawnPoint。
/// 复用 Sprite Physics Shape / PolygonCollider2D 的边界数据。
///
/// 使用方式:
/// 1. 场景中创建一个空物体,挂上 SpawnPointGenerator 组件
/// 2. 指定 groundSprite 或 polygonCollider(同 BoundaryWallGenerator
/// 3. Inspector 中设置数量和参数,点 "Generate Spawn Points"
/// </summary>
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;
/// <summary>
/// 从数据源提取边界点(世界坐标),与 BoundaryWallGenerator 一致。
/// </summary>
public List<List<Vector3>> GetBoundaryPaths()
{
var paths = new List<List<Vector3>>();
if (polygonCollider != null)
{
for (int i = 0; i < polygonCollider.pathCount; i++)
{
Vector2[] path = polygonCollider.GetPath(i);
var worldPath = new List<Vector3>();
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<Vector2>();
sprite.GetPhysicsShape(i, shape);
var worldPath = new List<Vector3>();
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<List<Vector3>> 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<List<Vector3>>();
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<Vector3>();
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<EnemySpawnPoint>();
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] 已清除所有出生点");
}
}
/// <summary>
/// 判断点是否在闭合多边形内(XZ 平面),可选内缩边距。
/// 使用射线法(Ray Casting)。
/// </summary>
bool IsInsidePolygon(Vector3 point, List<Vector3> 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;
}
/// <summary>
/// XZ 平面上点到线段的距离。
/// </summary>
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
}