using UnityEditor; using UnityEngine; using System.Collections.Generic; public class RandomEmptyGeneratorWindow : EditorWindow { private int emptyCount = 10; private int seed = 0; private Vector3 areaSize = Vector3.zero; private GameObject target; // 新增:指定对象配置 [System.Serializable] public class ObjectConfig { public GameObject prefab; public int count = 1; public float ratio = 1f; } private List objectConfigs = new List(); private Vector2 objectConfigScroll; private int editingRatioIndex = -1; // 当前正在编辑占比的索引 [MenuItem("我的工具/随机生成空物体 (带种子)")] public static void ShowWindow() { GetWindow("随机空物体生成器"); } private void OnGUI() { GUILayout.Label("生成设置", EditorStyles.boldLabel); target = (GameObject)EditorGUILayout.ObjectField("区域中心对象", target, typeof(GameObject), true); emptyCount = EditorGUILayout.IntField("空物体生成数量", emptyCount); if (target != null) { areaSize = GetTargetBoundsSize(target); EditorGUILayout.LabelField("自动识别区域范围", areaSize.ToString("F2")); areaSize.x = EditorGUILayout.FloatField("X轴长度", areaSize.x); areaSize.y = EditorGUILayout.FloatField("Y轴高度", areaSize.y); areaSize.z = EditorGUILayout.FloatField("Z轴长度", areaSize.z); } else { EditorGUILayout.LabelField("自动识别区域范围", "未选择对象"); } seed = EditorGUILayout.IntField("种子值", seed); EditorGUILayout.Space(); GUILayout.Label("指定对象生成配置", EditorStyles.boldLabel); objectConfigScroll = EditorGUILayout.BeginScrollView(objectConfigScroll, GUILayout.Height(120)); float totalRatio = 0f; for (int i = 0; i < objectConfigs.Count; i++) { EditorGUILayout.BeginHorizontal(); objectConfigs[i].prefab = (GameObject)EditorGUILayout.ObjectField(objectConfigs[i].prefab, typeof(GameObject), false, GUILayout.Width(120)); objectConfigs[i].count = EditorGUILayout.IntField("数量", objectConfigs[i].count, GUILayout.Width(80)); float oldRatio = objectConfigs[i].ratio; float newRatio = EditorGUILayout.Slider("占比", oldRatio, 0f, 100f, GUILayout.Width(200)); if (!Mathf.Approximately(newRatio, oldRatio)) { editingRatioIndex = i; AdjustRatios(i, newRatio); } objectConfigs[i].ratio = Mathf.Clamp(newRatio, 0f, 100f); if (GUILayout.Button("删除", GUILayout.Width(50))) { objectConfigs.RemoveAt(i); if (objectConfigs.Count > 0) AdjustRatios(0, objectConfigs[0].ratio); // 删除后重新调整 i--; continue; } EditorGUILayout.EndHorizontal(); totalRatio += objectConfigs[i].ratio; } EditorGUILayout.EndScrollView(); if (GUILayout.Button("添加指定对象")) { float remain = 100f; foreach (var cfg in objectConfigs) remain -= cfg.ratio; objectConfigs.Add(new ObjectConfig { ratio = Mathf.Max(0, remain) }); AdjustRatios(objectConfigs.Count - 1, Mathf.Max(0, remain)); } EditorGUILayout.LabelField($"占比总和: {totalRatio:F2}% (应为100%)", totalRatio == 100f ? EditorStyles.label : EditorStyles.boldLabel); EditorGUILayout.Space(); if (GUILayout.Button("生成空物体")) { GenerateEmpties(); } if (GUILayout.Button("生成指定对象(按数量/占比)")) { GenerateCustomObjectsByEmptyCount(); } if (GUILayout.Button("用种子随机重排所有对象位置")) { ShuffleAllChildrenWithSeed(); } // 按空物体数量分配生成指定对象 void GenerateCustomObjectsByEmptyCount() { if (target == null) { EditorUtility.DisplayDialog("错误", "请先选择一个区域中心对象!", "确定"); return; } if (objectConfigs.Count == 0) { EditorUtility.DisplayDialog("错误", "请至少添加一个指定对象配置!", "确定"); return; } Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("生成指定对象"); var group = Undo.GetCurrentGroup(); var rand = new System.Random(seed); Bounds bounds = GetTargetBounds(target, areaSize); int totalCount = emptyCount; float sum = 0f; for (int i = 0; i < objectConfigs.Count; i++) sum += objectConfigs[i].ratio; int[] assignCounts = new int[objectConfigs.Count]; int assigned = 0; for (int i = 0; i < objectConfigs.Count; i++) { if (i == objectConfigs.Count - 1) assignCounts[i] = totalCount - assigned; else { assignCounts[i] = Mathf.RoundToInt(objectConfigs[i].ratio / sum * totalCount); assigned += assignCounts[i]; } } int objIndex = 0; for (int i = 0; i < objectConfigs.Count; i++) { var cfg = objectConfigs[i]; if (cfg.prefab == null || assignCounts[i] <= 0) continue; string prefabName = cfg.prefab != null ? cfg.prefab.name : "Object"; for (int j = 0; j < assignCounts[i]; j++) { float x = (float)rand.NextDouble() * (bounds.max.x - bounds.min.x) + bounds.min.x; float y = (float)rand.NextDouble() * (bounds.max.y - bounds.min.y) + bounds.min.y; float z = (float)rand.NextDouble() * (bounds.max.z - bounds.min.z) + bounds.min.z; Vector3 worldPos = new Vector3(x, y, z); Vector3 localPos = target.transform.InverseTransformPoint(worldPos); GameObject go = (GameObject)PrefabUtility.InstantiatePrefab(cfg.prefab); if (go == null) go = new GameObject($"{prefabName}_{objIndex}"); Undo.RegisterCreatedObjectUndo(go, "Create Custom Object"); go.transform.parent = target.transform; go.transform.localPosition = localPos; go.name = $"{prefabName}_{objIndex}"; objIndex++; } } Undo.CollapseUndoOperations(group); EditorUtility.DisplayDialog("完成", $"已生成 {objIndex} 个指定对象。", "确定"); } // 用种子随机重排所有子物体 void ShuffleAllChildrenWithSeed() { if (target == null) { EditorUtility.DisplayDialog("错误", "请先选择一个区域中心对象!", "确定"); return; } var objs = new List(); for (int i = 0; i < target.transform.childCount; i++) { objs.Add(target.transform.GetChild(i)); } if (objs.Count == 0) { EditorUtility.DisplayDialog("提示", "未找到可重排的对象。", "确定"); return; } Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("重排所有对象位置"); var group = Undo.GetCurrentGroup(); var rand = new System.Random(seed); Bounds bounds = GetTargetBounds(target, areaSize); Vector3 min = bounds.min; Vector3 max = bounds.max; foreach (var go in objs) { Vector3 localPos = go.localPosition; float originalY = go.localPosition.y; int tryCount = 0; bool inBounds = false; while (tryCount < 30 && !inBounds) { float x = (float)rand.NextDouble() * (max.x - min.x) + min.x; float z = (float)rand.NextDouble() * (max.z - min.z) + min.z; float y = (float)0; // Y轴不变,始终为0 Vector3 worldPos = new Vector3(x, y + bounds.center.y, z); if (bounds.Contains(worldPos)) { localPos = target.transform.InverseTransformPoint(worldPos); // 保持原Y轴本地坐标 localPos.y = originalY; inBounds = true; } tryCount++; } if (!inBounds) { // fallback:中心点,Y轴不变 localPos = target.transform.InverseTransformPoint(bounds.center); localPos.y = originalY; } Undo.RecordObject(go.transform, "重排对象位置"); go.localPosition = localPos; } Undo.CollapseUndoOperations(group); EditorUtility.DisplayDialog("完成", $"已用种子 {seed} 随机重排 {objs.Count} 个对象。", "确定"); } if (GUILayout.Button("将空物体替换为指定对象")) { ReplaceEmptiesWithCustomObjects(); } if (GUILayout.Button("将所有指定对象替换为空物体")) { ReplaceCustomObjectsWithEmpties(); } // 保证占比总和为100%,调整某一项时自动调整其他项 void AdjustRatios(int changedIndex, float newRatio) { if (objectConfigs.Count == 0) return; float remain = 100f - newRatio; float oldSum = 0f; for (int i = 0; i < objectConfigs.Count; i++) { if (i != changedIndex) oldSum += objectConfigs[i].ratio; } if (oldSum <= 0.0001f) { // 只有一个或其他都为0 for (int i = 0; i < objectConfigs.Count; i++) { if (i != changedIndex) objectConfigs[i].ratio = remain / (objectConfigs.Count - 1); } } else { for (int i = 0; i < objectConfigs.Count; i++) { if (i == changedIndex) continue; objectConfigs[i].ratio = objectConfigs[i].ratio / oldSum * remain; } } // 最后修正总和误差 float sum = 0f; for (int i = 0; i < objectConfigs.Count; i++) sum += objectConfigs[i].ratio; float diff = 100f - sum; if (Mathf.Abs(diff) > 0.01f && objectConfigs.Count > 0) { // 分配到第一个非changedIndex for (int i = 0; i < objectConfigs.Count; i++) { if (i != changedIndex) { objectConfigs[i].ratio += diff; break; } } } } } // 新增功能方法:生成指定对象 private void GenerateCustomObjects() { if (target == null) { EditorUtility.DisplayDialog("错误", "请先选择一个区域中心对象!", "确定"); return; } if (objectConfigs.Count == 0) { EditorUtility.DisplayDialog("错误", "请至少添加一个指定对象配置!", "确定"); return; } Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("生成指定对象"); var group = Undo.GetCurrentGroup(); var rand = new System.Random(seed); Bounds bounds = GetTargetBounds(target); // 计算总数量(优先用count字段,若全为0则用ratio) int totalCount = 0; foreach (var cfg in objectConfigs) totalCount += Mathf.Max(0, cfg.count); if (totalCount == 0) { float ratioSum = 0f; foreach (var cfg in objectConfigs) ratioSum += Mathf.Max(0, cfg.ratio); totalCount = 10; // 默认生成10个 foreach (var cfg in objectConfigs) cfg.count = Mathf.RoundToInt((cfg.ratio / ratioSum) * totalCount); } int objIndex = 0; foreach (var cfg in objectConfigs) { if (cfg.prefab == null || cfg.count <= 0) continue; for (int i = 0; i < cfg.count; i++) { float x = (float)rand.NextDouble() * (bounds.max.x - bounds.min.x) + bounds.min.x; float y = (float)rand.NextDouble() * (bounds.max.y - bounds.min.y) + bounds.min.y; float z = (float)rand.NextDouble() * (bounds.max.z - bounds.min.z) + bounds.min.z; Vector3 worldPos = new Vector3(x, y, z); Vector3 localPos = target.transform.InverseTransformPoint(worldPos); GameObject go = (GameObject)PrefabUtility.InstantiatePrefab(cfg.prefab); if (go == null) go = new GameObject($"Custom_{objIndex}"); Undo.RegisterCreatedObjectUndo(go, "Create Custom Object"); go.transform.parent = target.transform; go.transform.localPosition = localPos; go.name = $"Custom_{objIndex}"; objIndex++; } } Undo.CollapseUndoOperations(group); EditorUtility.DisplayDialog("完成", $"已生成 {objIndex} 个指定对象。", "确定"); } private void ReplaceEmptiesWithCustomObjects() { if (target == null) { EditorUtility.DisplayDialog("错误", "请先选择一个区域中心对象!", "确定"); return; } if (objectConfigs.Count == 0) { EditorUtility.DisplayDialog("错误", "请至少添加一个指定对象配置!", "确定"); return; } // 收集所有空物体 var empties = new List(); for (int i = 0; i < target.transform.childCount; i++) { var child = target.transform.GetChild(i); if (child.name.StartsWith("Empty_")) empties.Add(child); } if (empties.Count == 0) { EditorUtility.DisplayDialog("提示", "未找到以Empty_开头的空物体。", "确定"); return; } Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("空物体替换为指定对象"); var group = Undo.GetCurrentGroup(); // 计算每个对象应分配的数量(按占比分配) int total = empties.Count; int[] assignCounts = new int[objectConfigs.Count]; float sum = 0f; for (int i = 0; i < objectConfigs.Count; i++) sum += objectConfigs[i].ratio; int assigned = 0; for (int i = 0; i < objectConfigs.Count; i++) { if (i == objectConfigs.Count - 1) assignCounts[i] = total - assigned; // 最后一个分配剩余 else { assignCounts[i] = Mathf.RoundToInt(objectConfigs[i].ratio / sum * total); assigned += assignCounts[i]; } } int objIndex = 0; int configIdx = 0; int configObjCount = 0; for (int i = 0; i < empties.Count; i++) { // 跳过无prefab的 while (configIdx < objectConfigs.Count && (objectConfigs[configIdx].prefab == null || assignCounts[configIdx] <= 0)) configIdx++; if (configIdx >= objectConfigs.Count) break; var cfg = objectConfigs[configIdx]; var empty = empties[i]; string prefabName = cfg.prefab != null ? cfg.prefab.name : "Object"; GameObject go = (GameObject)PrefabUtility.InstantiatePrefab(cfg.prefab); if (go == null) go = new GameObject($"{prefabName}_{objIndex}"); Undo.RegisterCreatedObjectUndo(go, "Create Custom Object"); go.transform.parent = target.transform; go.transform.localPosition = empty.localPosition; go.name = $"{prefabName}_{objIndex}"; Undo.DestroyObjectImmediate(empty.gameObject); objIndex++; configObjCount++; assignCounts[configIdx]--; if (assignCounts[configIdx] <= 0) configIdx++; } Undo.CollapseUndoOperations(group); EditorUtility.DisplayDialog("完成", $"已替换 {objIndex} 个空物体为指定对象。", "确定"); } private void ReplaceCustomObjectsWithEmpties() { if (target == null) { EditorUtility.DisplayDialog("错误", "请先选择一个区域中心对象!", "确定"); return; } // 收集所有属于objectConfigs中prefab的对象 var customs = new List(); var prefabNames = new HashSet(); foreach (var cfg in objectConfigs) { if (cfg.prefab != null) prefabNames.Add(cfg.prefab.name); } for (int i = 0; i < target.transform.childCount; i++) { var child = target.transform.GetChild(i); // 判断是否为指定对象(Prefab名匹配) string goName = child.name; string baseName = goName; int lastUnderscore = baseName.LastIndexOf('_'); if (lastUnderscore > 0 && int.TryParse(baseName.Substring(lastUnderscore + 1), out _)) baseName = baseName.Substring(0, lastUnderscore); if (prefabNames.Contains(baseName) || prefabNames.Contains(goName)) customs.Add(child); } if (customs.Count == 0) { EditorUtility.DisplayDialog("提示", "未找到指定对象。", "确定"); return; } Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("指定对象替换为空物体"); var group = Undo.GetCurrentGroup(); int emptyIdx = 0; foreach (var go in customs) { GameObject empty = new GameObject($"Empty_{emptyIdx}"); Undo.RegisterCreatedObjectUndo(empty, "Create Empty"); empty.transform.parent = target.transform; empty.transform.localPosition = go.localPosition; Undo.DestroyObjectImmediate(go.gameObject); emptyIdx++; } Undo.CollapseUndoOperations(group); EditorUtility.DisplayDialog("完成", $"已将 {customs.Count} 个指定对象替换为空物体。", "确定"); } private Vector3 GetTargetBoundsSize(GameObject go) { // 优先用Renderer包围盒,否则用Collider,否则为(10,1,10) var renderers = go.GetComponentsInChildren(); if (renderers != null && renderers.Length > 0) { var bounds = renderers[0].bounds; foreach (var r in renderers) bounds.Encapsulate(r.bounds); return bounds.size; } var colliders = go.GetComponentsInChildren(); if (colliders != null && colliders.Length > 0) { var bounds = colliders[0].bounds; foreach (var c in colliders) bounds.Encapsulate(c.bounds); return bounds.size; } return new Vector3(10, 1, 10); } private void GenerateEmpties() { if (target == null) { EditorUtility.DisplayDialog("错误", "请先选择一个区域中心对象!", "确定"); return; } Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("随机生成空物体"); var group = Undo.GetCurrentGroup(); var rand = new System.Random(seed); // 获取包围盒中心和最小/最大边界 Bounds bounds = GetTargetBounds(target); for (int i = 0; i < emptyCount; i++) { // 在包围盒内随机一个世界坐标点 float x = (float)rand.NextDouble() * (bounds.max.x - bounds.min.x) + bounds.min.x; float y = (float)rand.NextDouble() * (bounds.max.y - bounds.min.y) + bounds.min.y; float z = (float)rand.NextDouble() * (bounds.max.z - bounds.min.z) + bounds.min.z; Vector3 worldPos = new Vector3(x, y, z); // 转为父物体的本地坐标 Vector3 localPos = target.transform.InverseTransformPoint(worldPos); GameObject go = new GameObject($"Empty_{i}"); Undo.RegisterCreatedObjectUndo(go, "Create Empty"); go.transform.parent = target.transform; go.transform.localPosition = localPos; } Undo.CollapseUndoOperations(group); EditorUtility.DisplayDialog("完成", $"已生成 {emptyCount} 个空物体。", "确定"); } private void ShuffleEmptiesWithSeed() { if (target == null) { EditorUtility.DisplayDialog("错误", "请先选择一个区域中心对象!", "确定"); return; } var empties = new System.Collections.Generic.List(); for (int i = 0; i < target.transform.childCount; i++) { var child = target.transform.GetChild(i); if (child.name.StartsWith("Empty_")) { empties.Add(child.gameObject); } } if (empties.Count == 0) { EditorUtility.DisplayDialog("提示", "未找到以Empty_开头的空物体。", "确定"); return; } Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("重排空物体位置"); var group = Undo.GetCurrentGroup(); var rand = new System.Random(seed); Bounds bounds = GetTargetBounds(target); foreach (var go in empties) { float x = (float)rand.NextDouble() * (bounds.max.x - bounds.min.x) + bounds.min.x; float y = (float)rand.NextDouble() * (bounds.max.y - bounds.min.y) + bounds.min.y; float z = (float)rand.NextDouble() * (bounds.max.z - bounds.min.z) + bounds.min.z; Vector3 worldPos = new Vector3(x, y, z); Vector3 localPos = target.transform.InverseTransformPoint(worldPos); Undo.RecordObject(go.transform, "重排空物体位置"); go.transform.localPosition = localPos; } Undo.CollapseUndoOperations(group); EditorUtility.DisplayDialog("完成", $"已用种子 {seed} 随机重排 {empties.Count} 个空物体。", "确定"); } // 获取包围盒 private Bounds GetTargetBounds(GameObject go) { return GetTargetBounds(go, Vector3.zero); } // 新增:可指定区域范围 private Bounds GetTargetBounds(GameObject go, Vector3 customSize) { var renderers = go.GetComponentsInChildren(); if (renderers != null && renderers.Length > 0) { var bounds = renderers[0].bounds; foreach (var r in renderers) bounds.Encapsulate(r.bounds); if (customSize != Vector3.zero) bounds.size = new Vector3(bounds.size.x, customSize.y, bounds.size.z); return bounds; } var colliders = go.GetComponentsInChildren(); if (colliders != null && colliders.Length > 0) { var bounds = colliders[0].bounds; foreach (var c in colliders) bounds.Encapsulate(c.bounds); if (customSize != Vector3.zero) bounds.size = new Vector3(bounds.size.x, customSize.y, bounds.size.z); return bounds; } // 默认包围盒 var b = new Bounds(go.transform.position, new Vector3(10, customSize.y > 0 ? customSize.y : 1, 10)); return b; } // 判断点是否在包围盒内 private bool IsInsideBounds(Vector3 localPos, Bounds bounds, Vector3 center) { Vector3 worldPos = localPos + center; return bounds.Contains(worldPos); } }