using System.Collections.Generic; using BaseGames.Camera; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; namespace BaseGames.Editor { /// /// 将旧格式相机区域批量迁移到新架构。 /// /// 旧格式: /// Zone_xxx(挂 BoxCollider2D,定义相机可视矩形) /// ├─ Zone_xxx_TriggerRegion /// │ ├─ Zone_xxx_TriggerRegion_Point_0 … (多边形顶点) /// └─ Zone_xxx_Confiner(挂 BoxCollider2D / PolygonCollider2D,定义限位边界) /// /// 新格式: /// [新 CameraArea GO](CameraArea 组件,_visibleBounds = 本地 Rect) /// ├─ AreaBoundary(BoxCollider,对应旧 Confiner) /// ├─ TriggerZone(CameraTriggerZone + PolygonCollider2D,对应旧 TriggerRegion) /// └─ VCam_xxx(CinemachineCamera + 所有扩展组件,专属虚拟相机) /// /// 菜单:BaseGames → Camera → 相机区域迁移工具 /// public class CameraZoneMigrationTool : EditorWindow { [MenuItem("BaseGames/Camera/相机区域迁移工具", priority = 110)] public static void Open() { var win = GetWindow("相机区域迁移工具"); win.minSize = new Vector2(500f, 440f); } // ── 设置字段 ────────────────────────────────────────────────────────── private Transform _sourcesParent; // 旧 Zone_xxx 的父节点(通常名为 Zones) private Transform _targetParent; // 新对象放置位置(留空 = 与旧区域同级) private CameraLensConfigSO _lensConfig; // 绑定到新 CameraArea._lensConfig private bool _createDedicatedVCam = true; // 为每个区域创建专属 CinemachineCamera // ── 运行时状态 ──────────────────────────────────────────────────────── private readonly List _entries = new List(); private Vector2 _scroll; private bool _scanned; private int _lastMigratedCount; // ── 单条目数据 ──────────────────────────────────────────────────────── private class ZoneEntry { public GameObject ZoneObj; public BoxCollider2D VisibleBox; // Zone_xxx 上的 BoxCollider2D public List TriggerWorldPts = new List(); // TriggerRegion 各点世界坐标 public Collider2D ConfinerCollider; // Zone_xxx_Confiner 上的碰撞体(可空) public bool AlreadyMigrated; public bool Selected = true; } // ── 颜色 ───────────────────────────────────────────────────────────── private static readonly Color kOk = new Color(0.30f, 0.85f, 0.30f); private static readonly Color kWarning = new Color(1.00f, 0.75f, 0.10f); private static readonly Color kMuted = new Color(0.55f, 0.55f, 0.60f); // ══ GUI ═══════════════════════════════════════════════════════════════ private void OnGUI() { EditorGUILayout.Space(6); EditorGUILayout.LabelField("旧格式相机区域迁移工具", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "将旧架构(Zone_xxx + BoxCollider2D + TriggerRegion 点集 + Confiner)" + "批量转换为新架构(CameraArea + AreaBoundary + CameraTriggerZone)。\n" + "迁移结果可在 Scene 视图直接预览,原旧对象可选择禁用。", MessageType.Info); EditorGUILayout.Space(6); // ── 配置 ────────────────────────────────────────────────────────── _sourcesParent = (Transform)EditorGUILayout.ObjectField( new GUIContent("旧区域父节点 (Zones)", "包含所有 Zone_xxx 的父对象"), _sourcesParent, typeof(Transform), true); _targetParent = (Transform)EditorGUILayout.ObjectField( new GUIContent("新区域父节点(留空 = 同级)", "生成的 CameraArea 放在此节点下;留空则与旧 Zone_xxx 同级"), _targetParent, typeof(Transform), true); _lensConfig = (CameraLensConfigSO)EditorGUILayout.ObjectField( new GUIContent("镜头配置 SO", "赋给所有新 CameraArea._lensConfig;留空则不赋值"), _lensConfig, typeof(CameraLensConfigSO), false); _createDedicatedVCam = EditorGUILayout.Toggle( new GUIContent("创建专属 VCam", "为每个迁移区域创建子节点 CinemachineCamera(含所有扩展组件),\n" + "并绑定到 CameraArea._dedicatedCamera。"), _createDedicatedVCam); EditorGUILayout.Space(8); using (new EditorGUI.DisabledScope(_sourcesParent == null)) { if (GUILayout.Button("扫描区域", GUILayout.Height(30))) ScanZones(); } if (!_scanned) return; EditorGUILayout.Space(4); if (_entries.Count == 0) { EditorGUILayout.HelpBox( "未检测到旧格式区域。\n条件:BoxCollider2D + 含 \"TriggerRegion\" 字样的子节点。", MessageType.Warning); return; } // ── 统计行 ───────────────────────────────────────────────────────── int migrated = 0, pending = 0; foreach (var e in _entries) { if (e.AlreadyMigrated) migrated++; else if (e.Selected) pending++; } EditorGUILayout.LabelField( $"共 {_entries.Count} 个区域 | 已迁移 {migrated} | 已选待迁移 {pending}", EditorStyles.miniBoldLabel); EditorGUILayout.Space(2); // ── 条目列表 ─────────────────────────────────────────────────────── _scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.MaxHeight(240)); foreach (var entry in _entries) DrawEntryRow(entry); EditorGUILayout.EndScrollView(); EditorGUILayout.Space(8); // ── 批量迁移 ─────────────────────────────────────────────────────── int selectedPending = _entries.FindAll(e => e.Selected && !e.AlreadyMigrated).Count; using (new EditorGUI.DisabledScope(selectedPending == 0)) { if (GUILayout.Button($"迁移已选中区域({selectedPending} 个)", GUILayout.Height(36))) { int count = 0; foreach (var e in _entries) if (e.Selected && !e.AlreadyMigrated) { MigrateZone(e); count++; } _lastMigratedCount = count; ScanZones(); Debug.Log($"[迁移工具] 完成迁移 {count} 个相机区域。"); } } if (_lastMigratedCount > 0) EditorGUILayout.HelpBox( $"上次迁移完成 {_lastMigratedCount} 个。请在 Scene 视图确认后保存场景(Ctrl+S)。", MessageType.Info); } private void DrawEntryRow(ZoneEntry entry) { using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) { // 选择框(已迁移的不可再选) using (new EditorGUI.DisabledScope(entry.AlreadyMigrated)) entry.Selected = EditorGUILayout.Toggle(entry.Selected, GUILayout.Width(16)); // 名称 Color prev = GUI.color; GUI.color = entry.AlreadyMigrated ? kMuted : kOk; EditorGUILayout.LabelField(entry.ZoneObj.name, GUILayout.Width(170)); GUI.color = prev; // 可视框尺寸 if (entry.VisibleBox != null) { Vector2 sz = entry.VisibleBox.size; EditorGUILayout.LabelField($"Box {sz.x:F0}×{sz.y:F0}", EditorStyles.miniLabel, GUILayout.Width(72)); } else { GUI.color = kWarning; EditorGUILayout.LabelField("无 Box2D", EditorStyles.miniLabel, GUILayout.Width(72)); GUI.color = prev; } // 触发点数 Color ptColor = entry.TriggerWorldPts.Count >= 3 ? kOk : kWarning; GUI.color = ptColor; EditorGUILayout.LabelField($"触发 {entry.TriggerWorldPts.Count}pt", EditorStyles.miniLabel, GUILayout.Width(54)); GUI.color = prev; // 限位来源 string confLabel = entry.ConfinerCollider is PolygonCollider2D ? "Poly限位" : entry.ConfinerCollider is BoxCollider2D ? "Box限位" : "默认矩形"; EditorGUILayout.LabelField(confLabel, EditorStyles.miniLabel, GUILayout.Width(54)); // 状态 / 单独迁移按钮 if (entry.AlreadyMigrated) { GUI.color = kMuted; EditorGUILayout.LabelField("已迁移", EditorStyles.miniLabel, GUILayout.Width(46)); GUI.color = prev; } else { if (GUILayout.Button("迁移", GUILayout.Width(44), GUILayout.Height(16))) { MigrateZone(entry); _lastMigratedCount = 1; ScanZones(); } } // Ping 旧对象 if (GUILayout.Button("●", GUILayout.Width(20), GUILayout.Height(16))) EditorGUIUtility.PingObject(entry.ZoneObj); } } // ══ 扫描 ══════════════════════════════════════════════════════════════ private void ScanZones() { _entries.Clear(); _scanned = true; if (_sourcesParent == null) return; foreach (Transform child in _sourcesParent) { Debug.Log($"[root]:{child.name}"); // 旧 Zone 的标识:子节点直属,且挂有 BoxCollider2D var box = child.GetComponent(); if (box == null) continue; var entry = new ZoneEntry { ZoneObj = child.gameObject, VisibleBox = box }; // 收集触发多边形顶点——xxx_TriggerRegion 下每个子节点的世界坐标即一个顶点, // 按子节点顺序依次连线围成多边形触发区域。 Transform triggerRoot = FindChildContaining(child, "Trigger"); if (triggerRoot != null){ Debug.Log($"[trigger]:{triggerRoot.name}"); foreach (Transform pt in triggerRoot){ Debug.Log($"{pt.name}"); entry.TriggerWorldPts.Add((Vector2)pt.position); } } // 读取限位碰撞体(Zone_xxx_Confiner 上的 Collider2D) Transform confinerT = FindChildContaining(child, "Confiner"); if (confinerT != null) entry.ConfinerCollider = confinerT.GetComponent(); // 是否已经完成迁移(自身或子节点含 CameraArea) entry.AlreadyMigrated = child.GetComponent() != null || child.GetComponentInChildren(true) != null; _entries.Add(entry); } Repaint(); } private static Transform FindChildContaining(Transform parent, string keyword) { foreach (Transform child in parent) if (child.name.IndexOf(keyword, System.StringComparison.OrdinalIgnoreCase) >= 0) return child; return null; } // ══ 迁移 ══════════════════════════════════════════════════════════════ private void MigrateZone(ZoneEntry entry) { GameObject zoneGO = entry.ZoneObj; Transform parent = _targetParent != null ? _targetParent : zoneGO.transform.parent; Vector3 worldPos = zoneGO.transform.position; // ── 1. 计算本地可视 Rect(相对于新 CameraArea 的世界位置)──────── Rect localBounds; if (entry.VisibleBox != null) { // 注意:BoxCollider2D.bounds 在 inactive 对象上无效,必须手动计算 Bounds wb = GetColliderWorldBounds(entry.VisibleBox); localBounds = new Rect( wb.min.x - worldPos.x, wb.min.y - worldPos.y, wb.size.x, wb.size.y); } else { localBounds = new Rect(-12f, -6f, 24f, 12f); } // ── 2. 创建 CameraArea 根节点 ───────────────────────────────────── GameObject areaGO = new GameObject(zoneGO.name); Undo.RegisterCreatedObjectUndo(areaGO, "Migrate Camera Zone"); areaGO.transform.SetParent(parent, worldPositionStays: false); areaGO.transform.position = worldPos; // 紧跟旧对象之后(同级排列时保持顺序直观) if (parent == zoneGO.transform.parent) areaGO.transform.SetSiblingIndex(zoneGO.transform.GetSiblingIndex() + 1); CameraArea area = areaGO.AddComponent(); var soArea = new SerializedObject(area); soArea.FindProperty("_visibleBounds").rectValue = localBounds; if (_lensConfig != null) soArea.FindProperty("_lensConfig").objectReferenceValue = _lensConfig; soArea.ApplyModifiedProperties(); // ── 3. 创建 AreaBoundary(限位体积 BoxCollider)───────────────────────────── GameObject boundaryGO = new GameObject($"{zoneGO.name}_AreaBoundary"); Undo.RegisterCreatedObjectUndo(boundaryGO, "Migrate Camera Zone"); boundaryGO.transform.SetParent(areaGO.transform, worldPositionStays: false); boundaryGO.transform.localPosition = Vector3.zero; BoxCollider confinerBox = boundaryGO.AddComponent(); confinerBox.isTrigger = true; confinerBox.center = new Vector3(0f, 0f, -10f); // Z 占位符,SyncConfiner 会立即重算 confinerBox.size = new Vector3(localBounds.width, localBounds.height, 1f); // 绑定 _confinerCollider soArea.Update(); soArea.FindProperty("_confinerCollider").objectReferenceValue = confinerBox; soArea.ApplyModifiedProperties(); EditorUtility.SetDirty(area); // 绑定完成后立即按 FOV/Depth 公式同步限位多边形, // 确保与 LensConfig 参数一致(创建时不会自动同步) float syncFOV = _lensConfig != null ? _lensConfig.fieldOfView : 60f; float syncAspect = UnityEngine.Camera.main != null ? UnityEngine.Camera.main.aspect : 16f / 9f; CameraAreaEditor.SyncConfinerFromVisibleBounds(area, syncFOV, syncAspect); // ── 4. 创建 TriggerZone(相机激活触发器)─────────────────────── GameObject triggerGO = new GameObject($"{zoneGO.name}_TriggerZone"); Undo.RegisterCreatedObjectUndo(triggerGO, "Migrate Camera Zone"); triggerGO.transform.SetParent(areaGO.transform, worldPositionStays: false); triggerGO.transform.localPosition = Vector3.zero; // AddComponent 会因 [RequireComponent] 自动添加 PolygonCollider2D CameraTriggerZone triggerComp = triggerGO.AddComponent(); PolygonCollider2D triggerPoly = triggerGO.GetComponent(); // 将旧触发多边形路径(本地坐标,相对于新 CameraArea 世界位置)直接赋给 PolygonCollider2D Vector2[] triggerPath = BuildTriggerPath(entry, worldPos, localBounds); triggerPoly.isTrigger = true; triggerPoly.SetPath(0, triggerPath); // _targetArea → 指向刚创建的 CameraArea var soTrigger = new SerializedObject(triggerComp); soTrigger.FindProperty("_targetArea").objectReferenceValue = area; soTrigger.ApplyModifiedProperties(); EditorUtility.SetDirty(triggerComp); // ── 5. 创建专属 VCam(每区域独立相机)───────────────────────────── if (_createDedicatedVCam) CameraAreaEditor.CreateDedicatedVCamForArea(area); // ── 6. 处理旧对象 ────────────────────────────────────────────── // 先记录原始激活状态,再对旧对象做处理,避免 SetActive(false) 后误读 bool wasActive = zoneGO.activeSelf; // 同步旧区域的激活状态(旧 Zone_xxx 若为禁用,新对象同样禁用) if (!wasActive) areaGO.SetActive(false); EditorUtility.SetDirty(areaGO); EditorSceneManager.MarkSceneDirty(zoneGO.scene); EditorGUIUtility.PingObject(areaGO); Debug.Log($"[迁移工具] {zoneGO.name} → {areaGO.name} " + $"可视 {localBounds.width:F0}×{localBounds.height:F0} " + $"触发 PolygonCollider2D ({triggerPath.Length} 点) " + $"限位 BoxCollider ({confinerBox.size.x:F1}×{confinerBox.size.y:F1})"); } // ── 限位多边形路径 ──────────────────────────────────────────────────── /// /// 按优先级构建限位多边形路径(本地坐标,相对于新 CameraArea): /// 1. Zone_xxx_Confiner 上的 PolygonCollider2D → 直接转换 /// 2. Zone_xxx_Confiner 上的 BoxCollider2D → 取 AABB 四角 /// 3. 兜底 → 使用可视矩形 /// private static Vector2[] BuildConfinerPath(ZoneEntry entry, Vector3 areaWorldPos, Rect fallback) { if (entry.ConfinerCollider is PolygonCollider2D poly && poly.pathCount > 0) { var pts = new List(); poly.GetPath(0, pts); var result = new Vector2[pts.Count]; for (int i = 0; i < pts.Count; i++) result[i] = (Vector2)poly.transform.TransformPoint(pts[i]) - (Vector2)areaWorldPos; return result; } if (entry.ConfinerCollider is BoxCollider2D box) { Bounds b = GetColliderWorldBounds(box); return new Vector2[] { new Vector2(b.min.x - areaWorldPos.x, b.min.y - areaWorldPos.y), new Vector2(b.min.x - areaWorldPos.x, b.max.y - areaWorldPos.y), new Vector2(b.max.x - areaWorldPos.x, b.max.y - areaWorldPos.y), new Vector2(b.max.x - areaWorldPos.x, b.min.y - areaWorldPos.y), }; } // 兜底:可视矩形 return RectToPolygon(fallback); } /// /// 构建触发多边形路径(本地坐标,相对于新 CameraArea): /// 取 TriggerRegion 各点的世界坐标减去 areaWorldPos; /// 若点数不足 3 则兜底使用可视矩形。 /// private static Vector2[] BuildTriggerPath(ZoneEntry entry, Vector3 areaWorldPos, Rect fallback) { if (entry.TriggerWorldPts.Count >= 3) { // 将世界坐标转换为 areaGO 本地坐标 var localPts = new Vector2[entry.TriggerWorldPts.Count]; for (int i = 0; i < localPts.Length; i++) localPts[i] = entry.TriggerWorldPts[i] - (Vector2)areaWorldPos; // 按照质心角度排序,确保顶点顺序能够围成合法多边形 return SortPointsByAngle(localPts); } return RectToPolygon(fallback); } /// /// 将一组点按照围绕质心的角度(逆时针)排序,使其能够围成合法的简单多边形。 /// 适用于凸多边形及质心在多边形内部的凹多边形。 /// private static Vector2[] SortPointsByAngle(Vector2[] points) { // 计算质心 Vector2 centroid = Vector2.zero; foreach (var p in points) centroid += p; centroid /= points.Length; // 按照相对质心的极角升序排列(逆时针) var sorted = new System.Collections.Generic.List(points); sorted.Sort((a, b) => { float angleA = Mathf.Atan2(a.y - centroid.y, a.x - centroid.x); float angleB = Mathf.Atan2(b.y - centroid.y, b.x - centroid.x); return angleA.CompareTo(angleB); }); return sorted.ToArray(); } /// /// 手动计算 BoxCollider2D 的世界 AABB,不依赖 .bounds(inactive 对象上 .bounds 无效)。 /// private static Bounds GetColliderWorldBounds(BoxCollider2D box) { Vector2 worldCenter = (Vector2)box.transform.TransformPoint(box.offset); Vector2 worldSize = new Vector2( box.size.x * Mathf.Abs(box.transform.lossyScale.x), box.size.y * Mathf.Abs(box.transform.lossyScale.y)); return new Bounds(worldCenter, worldSize); } private static Vector2[] RectToPolygon(Rect r) { return new Vector2[] { new Vector2(r.xMin, r.yMin), new Vector2(r.xMin, r.yMax), new Vector2(r.xMax, r.yMax), new Vector2(r.xMax, r.yMin), }; } } }