352 lines
13 KiB
C#
352 lines
13 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEditor;
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
[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));
|
||
}
|
||
}
|
||
}
|