增加出身点随机算法
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using IndianOceanAssets.Engine2_5D;
|
||||||
|
|
||||||
|
namespace IndianOceanAssets.EditorTools
|
||||||
|
{
|
||||||
|
[InitializeOnLoad]
|
||||||
|
public class EnemySpawnPointSceneGUI
|
||||||
|
{
|
||||||
|
/// <summary>全局开关:是否在 Scene 视图中显示所有出生点编号</summary>
|
||||||
|
public static bool ShowLabels = true;
|
||||||
|
|
||||||
|
static EnemySpawnPointSceneGUI()
|
||||||
|
{
|
||||||
|
SceneView.duringSceneGui += OnSceneGUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void OnSceneGUI(SceneView sceneView)
|
||||||
|
{
|
||||||
|
if (!ShowLabels) return;
|
||||||
|
|
||||||
|
var spawnPoints = Object.FindObjectsByType<EnemySpawnPoint>(FindObjectsSortMode.None);
|
||||||
|
foreach (var sp in spawnPoints)
|
||||||
|
{
|
||||||
|
if (sp.Index < 0) continue;
|
||||||
|
|
||||||
|
Vector3 labelPos = sp.transform.position + Vector3.up * 2.5f;
|
||||||
|
Vector2 screenPos = HandleUtility.WorldToGUIPoint(labelPos);
|
||||||
|
|
||||||
|
Handles.BeginGUI();
|
||||||
|
GUIStyle style = new GUIStyle("Label")
|
||||||
|
{
|
||||||
|
fontSize = 14,
|
||||||
|
fontStyle = FontStyle.Bold,
|
||||||
|
alignment = TextAnchor.MiddleCenter,
|
||||||
|
normal = { textColor = Color.yellow }
|
||||||
|
};
|
||||||
|
GUIContent content = new GUIContent($"#{sp.Index}");
|
||||||
|
Vector2 size = style.CalcSize(content);
|
||||||
|
Rect rect = new Rect(screenPos.x - size.x * 0.5f, screenPos.y - size.y * 0.5f, size.x + 8, size.y + 4);
|
||||||
|
EditorGUI.DrawRect(rect, new Color(0, 0, 0, 0.7f));
|
||||||
|
GUI.Label(rect, content, style);
|
||||||
|
Handles.EndGUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[CustomEditor(typeof(EnemySpawnPoint))]
|
||||||
|
public class EnemySpawnPointEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
DrawDefaultInspector();
|
||||||
|
|
||||||
|
EditorGUILayout.Space();
|
||||||
|
EnemySpawnPointSceneGUI.ShowLabels = EditorGUILayout.Toggle(
|
||||||
|
"显示所有编号", EnemySpawnPointSceneGUI.ShowLabels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: DXpLtiqpASjqX7R7WRcfF+Fkr6TaUg+DiIvQeBWFfl8Tv5CLR7cYRek=
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
+2510
-20
File diff suppressed because it is too large
Load Diff
@@ -7,13 +7,21 @@ namespace IndianOceanAssets.Engine2_5D
|
|||||||
/// EnemyManager 会在游戏开始时找到所有出生点并生成敌人。
|
/// EnemyManager 会在游戏开始时找到所有出生点并生成敌人。
|
||||||
///
|
///
|
||||||
/// 用法:在场景中放置空物体,挂上此组件,调整位置即可。
|
/// 用法:在场景中放置空物体,挂上此组件,调整位置即可。
|
||||||
/// Scene 视图中显示红色线框球 + 竖线,方便辨认。
|
/// Scene 视图中显示红色线框球 + 竖线 + 编号,方便辨认。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnemySpawnPoint : MonoBehaviour
|
public class EnemySpawnPoint : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
[Tooltip("出生点编号(自动生成,用于 Scene 视图显示)")]
|
||||||
|
[SerializeField] private int index = -1;
|
||||||
|
|
||||||
[Tooltip("Gizmo 球体半径(仅编辑器可视化)")]
|
[Tooltip("Gizmo 球体半径(仅编辑器可视化)")]
|
||||||
[SerializeField] private float gizmoRadius = 0.5f;
|
[SerializeField] private float gizmoRadius = 0.5f;
|
||||||
|
|
||||||
|
public int Index => index;
|
||||||
|
|
||||||
|
/// <summary>设置编号(由 SpawnPointGenerator 调用)。</summary>
|
||||||
|
public void SetIndex(int idx) => index = idx;
|
||||||
|
|
||||||
private void OnDrawGizmos()
|
private void OnDrawGizmos()
|
||||||
{
|
{
|
||||||
Gizmos.color = Color.red;
|
Gizmos.color = Color.red;
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: WSgZsS6sUC7f433xhEcz9R4TfRcOXPqjYFL/R5UlXpum/24m4g15VPk=
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user