using System.Collections.Generic; using BaseGames.Camera; using Unity.Cinemachine; using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.Tilemaps; namespace BaseGames.Editor { /// /// 相机区域配置工具窗口。 /// 扫描当前已加载场景中的所有 CameraArea / CameraTriggerZone / CameraStateController, /// 显示各组件的绑定状态,并提供一键修复快捷操作。 /// /// 菜单:BaseGames → Camera → Camera Area Setup /// public class CameraAreaSetupTool : EditorWindow { // ── 状态 ────────────────────────────────────────────────────────────── private Vector2 _scroll; private List _cameraAreas = new List(); private List _triggerZones = new List(); private CameraStateController _controller; // ── GUI 样式缓存 ────────────────────────────────────────────────────── private GUIStyle _boxStyle; // ══ 菜单入口 ══════════════════════════════════════════════════════════ [MenuItem("BaseGames/Camera/Camera Area Setup", priority = 100)] public static void ShowWindow() { var win = GetWindow("Camera Area Setup"); win.minSize = new Vector2(420f, 300f); win.Show(); } // ══ EditorWindow 生命周期 ═════════════════════════════════════════════ private void OnEnable() => RescanScene(); private void OnHierarchyChange() => RescanScene(); private void OnFocus() => RescanScene(); // ══ 场景扫描 ══════════════════════════════════════════════════════════ private void RescanScene() { _cameraAreas.Clear(); _triggerZones.Clear(); _controller = null; for (int i = 0; i < SceneManager.sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); if (!scene.isLoaded) continue; foreach (GameObject root in scene.GetRootGameObjects()) { _cameraAreas.AddRange(root.GetComponentsInChildren(true)); _triggerZones.AddRange(root.GetComponentsInChildren(true)); if (_controller == null) _controller = root.GetComponentInChildren(true); } } Repaint(); } // ══ GUI ═══════════════════════════════════════════════════════════════ private void OnGUI() { EnsureStyles(); // ── 工具栏 ───────────────────────────────────────────────────── using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { if (GUILayout.Button("↻ 刷新", EditorStyles.toolbarButton, GUILayout.Width(56))) RescanScene(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Place Camera Area", EditorStyles.toolbarButton)) EditorApplication.ExecuteMenuItem("BaseGames/Scene/Place/Camera Area"); if (GUILayout.Button("Place Trigger Zone", EditorStyles.toolbarButton)) EditorApplication.ExecuteMenuItem("BaseGames/Scene/Place/Camera Trigger Zone"); } _scroll = EditorGUILayout.BeginScrollView(_scroll); // ── CameraStateController ─────────────────────────────────────── DrawSectionHeader("CameraStateController(Persistent 场景)"); DrawControllerSection(); EditorGUILayout.Space(8f); // ── CameraArea 列表 ───────────────────────────────────────────── DrawSectionHeader($"Camera Areas [{_cameraAreas.Count}]"); DrawCameraAreasSection(); EditorGUILayout.Space(8f); // ── CameraTriggerZone 列表 ────────────────────────────────────── DrawSectionHeader($"Camera Trigger Zones [{_triggerZones.Count}]"); DrawTriggerZonesSection(); EditorGUILayout.EndScrollView(); } // ── CameraStateController ────────────────────────────────────────── private void DrawControllerSection() { if (_controller == null) { EditorGUILayout.HelpBox( "当前已加载场景中未找到 CameraStateController(正常)。\n" + "该组件位于 Persistent 场景,单独编辑房间场景时不会加载。\n" + "进入 Play Mode 前请确保 Persistent 场景已一同加载。", MessageType.Info); return; } using (new EditorGUILayout.VerticalScope(_boxStyle)) { using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("GameObject", GUILayout.Width(120f)); EditorGUILayout.ObjectField(_controller.gameObject, typeof(GameObject), true); } SerializedObject so = new SerializedObject(_controller); DrawFieldCheck(so, "_vcamA", "全局 VCam A (CinemachineCamera)"); DrawFieldCheck(so, "_vcamB", "全局 VCam B (CinemachineCamera)"); DrawFieldCheck(so, "_brain", "CinemachineBrain"); DrawFieldCheck(so, "_impulseSource", "CinemachineImpulseSource", optional: true); DrawFieldCheck(so, "_defaultBlendProfile","默认混合配置 (CameraBlendProfileSO)", optional: true); EditorGUILayout.Space(4f); if (GUILayout.Button("为全局 VCam 赋值 Follow 目标(Player/CameraFollowTarget)", GUILayout.Height(24f))) AssignFollowToGlobalVCams(so); } } // ── CameraArea 列表 ──────────────────────────────────────────────── private void DrawCameraAreasSection() { if (_cameraAreas.Count == 0) { EditorGUILayout.HelpBox( "场景中未找到 CameraArea 组件。\n使用工具栏 \"Place Camera Area\" 快速生成。", MessageType.Info); return; } foreach (var area in _cameraAreas) { if (area == null) continue; DrawCameraAreaEntry(area); EditorGUILayout.Space(2f); } } private void DrawCameraAreaEntry(CameraArea area) { SerializedObject so = new SerializedObject(area); bool confinerOk = so.FindProperty("_confinerCollider").objectReferenceValue != null; bool dedicatedSet = so.FindProperty("_dedicatedCamera").objectReferenceValue != null; bool allOk = confinerOk; using (new EditorGUILayout.VerticalScope(_boxStyle)) { // 标题行 using (new EditorGUILayout.HorizontalScope()) { GUILayout.Label(allOk ? "✅" : "⚠", GUILayout.Width(20f)); if (GUILayout.Button(area.gameObject.name, EditorStyles.boldLabel, GUILayout.ExpandWidth(true))) Selection.activeGameObject = area.gameObject; if (GUILayout.Button("选中", GUILayout.Width(40f))) Selection.activeGameObject = area.gameObject; } EditorGUILayout.Space(2f); DrawCheckRow("_confinerCollider (PolygonCollider2D)", confinerOk); DrawCheckRow("_dedicatedCamera(专有 VCam,可选)", dedicatedSet, optional: true); DrawCheckRow("_blendProfile(可选,未设则用全局默认)", so.FindProperty("_blendProfile").objectReferenceValue != null, optional: true); if (!confinerOk) { EditorGUILayout.Space(2f); if (GUILayout.Button("修复:绑定子节点 PolygonCollider2D", GUILayout.Height(20f))) FixConfinerBinding(area); } } } // ── CameraTriggerZone 列表 ───────────────────────────────────────── private void DrawTriggerZonesSection() { if (_triggerZones.Count == 0) { EditorGUILayout.HelpBox( "场景中未找到 CameraTriggerZone。\n" + "至少需要一个触发器来在运行时激活 CameraArea。\n" + "使用工具栏 \"Place Trigger Zone\" 快速生成。", MessageType.Info); return; } foreach (var zone in _triggerZones) { if (zone == null) continue; DrawTriggerZoneEntry(zone); } } private void DrawTriggerZoneEntry(CameraTriggerZone zone) { SerializedObject so = new SerializedObject(zone); bool hasTarget = so.FindProperty("_targetArea").objectReferenceValue != null; using (new EditorGUILayout.HorizontalScope(_boxStyle)) { GUILayout.Label(hasTarget ? "✅" : "❌", GUILayout.Width(20f)); if (GUILayout.Button(zone.gameObject.name, EditorStyles.label, GUILayout.ExpandWidth(true))) Selection.activeGameObject = zone.gameObject; if (!hasTarget) EditorGUILayout.LabelField("⚠ _targetArea 未绑定!", GUILayout.Width(160f)); if (GUILayout.Button("选中", GUILayout.Width(40f))) Selection.activeGameObject = zone.gameObject; } } // ══ 自动修复操作 ═══════════════════════════════════════════════════════ /// /// 查找场景中 tag=Player 的 Player/CameraFollowTarget, /// 写入 CameraStateController._vcamA 和 _vcamB 的 Follow 字段。 /// private static void AssignFollowToGlobalVCams(SerializedObject controllerSO) { GameObject player = GameObject.FindWithTag("Player"); if (player == null) { Debug.LogWarning("[CameraAreaSetupTool] 场景中未找到 tag=Player 的对象。" + "请先放置 Player(BaseGames → Scene → Place → Player)。"); return; } const string followNodeName = "CameraFollowTarget"; Transform followTarget = player.transform.Find(followNodeName); if (followTarget == null) { var go = new GameObject(followNodeName); Undo.RegisterCreatedObjectUndo(go, "Create CameraFollowTarget"); Undo.SetTransformParent(go.transform, player.transform, "Parent CameraFollowTarget"); go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one; followTarget = go.transform; Debug.Log($"[CameraAreaSetupTool] 已在 Player 下自动创建 {followNodeName} 子节点。"); } int count = 0; foreach (string fieldName in new[] { "_vcamA", "_vcamB" }) { var vcamProp = controllerSO.FindProperty(fieldName); if (vcamProp?.objectReferenceValue is CinemachineCamera vcam) { Undo.RecordObject(vcam, "Assign Camera Follow Target"); vcam.Follow = followTarget; EditorUtility.SetDirty(vcam); count++; } } if (count > 0) Debug.Log($"[CameraAreaSetupTool] 已为 {count} 台全局 VCam 赋值 Follow → {followTarget.name}。"); else Debug.LogWarning("[CameraAreaSetupTool] _vcamA/_vcamB 均未绑定,无法赋值 Follow。请先在 Inspector 中绑定。"); } /// 将子节点中找到的第一个 PolygonCollider2D 绑定到 CameraArea._confinerCollider。 private static void FixConfinerBinding(CameraArea area) { PolygonCollider2D poly = area.GetComponentInChildren(true); if (poly == null) { Debug.LogWarning($"[CameraAreaSetupTool] {area.name}:子节点中未找到 PolygonCollider2D。"); return; } SerializedObject so = new SerializedObject(area); so.FindProperty("_confinerCollider").objectReferenceValue = poly; so.ApplyModifiedProperties(); Debug.Log($"[CameraAreaSetupTool] {area.name}:_confinerCollider → {poly.gameObject.name}"); } // ══ GUI 辅助 ═══════════════════════════════════════════════════════════ private void EnsureStyles() { if (_boxStyle == null) { _boxStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(6, 6, 4, 4), }; } } private static void DrawSectionHeader(string title) { EditorGUILayout.Space(4f); using (new EditorGUILayout.HorizontalScope()) { GUILayout.Label(title, EditorStyles.boldLabel); } Rect r = EditorGUILayout.GetControlRect(false, 1f); EditorGUI.DrawRect(r, new Color(0.4f, 0.4f, 0.4f, 1f)); EditorGUILayout.Space(2f); } private static void DrawFieldCheck(SerializedObject so, string propName, string displayName, bool optional = false) { var prop = so.FindProperty(propName); bool ok = prop != null && prop.objectReferenceValue != null; DrawCheckRow(displayName, ok, optional); } private static void DrawCheckRow(string label, bool ok, bool optional = false) { using (new EditorGUILayout.HorizontalScope()) { Color prev = GUI.color; GUI.color = ok ? new Color(0.4f, 1f, 0.4f) : (optional ? new Color(0.8f, 0.8f, 0.4f) : new Color(1f, 0.4f, 0.4f)); GUILayout.Label(ok ? "●" : (optional ? "◌" : "✗"), GUILayout.Width(16f)); GUI.color = prev; EditorGUILayout.LabelField(label); } } } } namespace BaseGames.Editor { /// /// 区域相机配置工具窗口。 /// 扫描当前已加载场景中的所有 RoomCamera / CameraTriggerZone / CameraStateController, /// 显示各组件的绑定状态,并提供一键修复快捷操作。 /// /// 菜单:BaseGames → Camera → Room Camera Setup /// public class RoomCameraSetupTool : EditorWindow { // ── 状态 ────────────────────────────────────────────────────────────── private Vector2 _scroll; private List _roomCameras = new List(); private List _triggerZones = new List(); private CameraStateController _controller; // ── GUI 样式缓存 ────────────────────────────────────────────────────── private GUIStyle _boxStyle; // ══ 菜单入口 ══════════════════════════════════════════════════════════ [MenuItem("BaseGames/Camera/Room Camera Setup", priority = 100)] public static void ShowWindow() { var win = GetWindow("Room Camera Setup"); win.minSize = new Vector2(420f, 300f); win.Show(); } // ══ EditorWindow 生命周期 ═════════════════════════════════════════════ private void OnEnable() => RescanScene(); private void OnHierarchyChange() => RescanScene(); private void OnFocus() => RescanScene(); // ══ 场景扫描 ══════════════════════════════════════════════════════════ private void RescanScene() { _roomCameras.Clear(); _triggerZones.Clear(); _controller = null; for (int i = 0; i < SceneManager.sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); if (!scene.isLoaded) continue; foreach (GameObject root in scene.GetRootGameObjects()) { _roomCameras.AddRange(root.GetComponentsInChildren(true)); _triggerZones.AddRange(root.GetComponentsInChildren(true)); if (_controller == null) _controller = root.GetComponentInChildren(true); } } Repaint(); } // ══ GUI ═══════════════════════════════════════════════════════════════ private void OnGUI() { EnsureStyles(); // ── 工具栏 ───────────────────────────────────────────────────── using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { if (GUILayout.Button("↻ 刷新", EditorStyles.toolbarButton, GUILayout.Width(56))) RescanScene(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Place Room Camera", EditorStyles.toolbarButton)) EditorApplication.ExecuteMenuItem("BaseGames/Scene/Place/Room Camera"); if (GUILayout.Button("Place Trigger Zone", EditorStyles.toolbarButton)) EditorApplication.ExecuteMenuItem("BaseGames/Scene/Place/Camera Trigger Zone"); } _scroll = EditorGUILayout.BeginScrollView(_scroll); // ── CameraStateController ─────────────────────────────────────── DrawSectionHeader("CameraStateController(持久场景)"); DrawControllerSection(); EditorGUILayout.Space(8f); // ── RoomCamera 列表 ───────────────────────────────────────────── DrawSectionHeader($"Room Cameras [{_roomCameras.Count}]"); DrawRoomCamerasSection(); EditorGUILayout.Space(8f); // ── CameraTriggerZone 列表 ────────────────────────────────────── DrawSectionHeader($"Camera Trigger Zones [{_triggerZones.Count}]"); DrawTriggerZonesSection(); EditorGUILayout.EndScrollView(); } // ── CameraStateController ────────────────────────────────────────── private void DrawControllerSection() { if (_controller == null) { EditorGUILayout.HelpBox( "当前已加载场景中未找到 CameraStateController(正常)。\n" + "该组件位于 Persistent 场景,单独编辑房间场景时不会加载。\n" + "进入 Play Mode 前请确保 Persistent 场景已一同加载。", MessageType.Info); return; } using (new EditorGUILayout.VerticalScope(_boxStyle)) { using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("GameObject", GUILayout.Width(120f)); EditorGUILayout.ObjectField(_controller.gameObject, typeof(GameObject), true); } SerializedObject so = new SerializedObject(_controller); DrawFieldCheck(so, "_brain", "CinemachineBrain"); DrawFieldCheck(so, "_impulseSource", "CinemachineImpulseSource"); DrawFieldCheck(so, "_defaultBlendProfile","默认混合配置 (CameraBlendProfileSO)", optional: true); } } // ── RoomCamera 列表 ──────────────────────────────────────────────── private void DrawRoomCamerasSection() { if (_roomCameras.Count == 0) { EditorGUILayout.HelpBox( "场景中未找到 RoomCamera 组件。\n使用工具栏 \"Place Room Camera\" 快速生成。", MessageType.Info); return; } // 批量操作 using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("批量赋值 Follow (Player)", GUILayout.Height(22f))) BatchAssignFollowTarget(); if (GUILayout.Button("批量修复 Confiner 绑定", GUILayout.Height(22f))) BatchFixConfinerBinding(); } EditorGUILayout.Space(4f); foreach (var cam in _roomCameras) { if (cam == null) continue; DrawRoomCameraEntry(cam); EditorGUILayout.Space(2f); } } private void DrawRoomCameraEntry(RoomCamera cam) { if (cam == null) return; SerializedObject camSO = new SerializedObject(cam); CinemachineCamera vcam = cam.GetComponent(); CinemachineConfiner2D confiner = cam.GetComponent(); bool visibleAreaOk = camSO.FindProperty("_visibleArea").objectReferenceValue != null; bool confinerCompOk = confiner != null; bool confinerBoundOk = confiner != null && new SerializedObject(confiner).FindProperty("m_BoundingShape2D").objectReferenceValue != null; bool followOk = vcam != null && vcam.Follow != null; bool blendOk = camSO.FindProperty("_blendProfile").objectReferenceValue != null; bool allOk = visibleAreaOk && confinerCompOk && confinerBoundOk && followOk; using (new EditorGUILayout.VerticalScope(_boxStyle)) { // 标题行 using (new EditorGUILayout.HorizontalScope()) { GUILayout.Label(allOk ? "✅" : "⚠", GUILayout.Width(20f)); if (GUILayout.Button(cam.gameObject.name, EditorStyles.boldLabel, GUILayout.ExpandWidth(true))) Selection.activeGameObject = cam.gameObject; if (GUILayout.Button("选中", GUILayout.Width(40f))) Selection.activeGameObject = cam.gameObject; } EditorGUILayout.Space(2f); // 状态行 DrawCheckRow("_visibleArea (RoomVisibleArea)", visibleAreaOk); DrawCheckRow("CinemachineConfiner2D 组件", confinerCompOk); DrawCheckRow("Confiner2D.m_BoundingShape2D 已绑定", confinerBoundOk); DrawCheckRow("CinemachineCamera.Follow (Player/CameraFollowTarget)", followOk); DrawCheckRow("_blendProfile (未设则用全局默认,可选)", blendOk, optional: true); // 修复按钮区 bool needFix = !followOk || !confinerBoundOk || !visibleAreaOk; if (needFix) { EditorGUILayout.Space(2f); using (new EditorGUILayout.HorizontalScope()) { if (!followOk) if (GUILayout.Button("赋值 Follow", GUILayout.Height(20f))) AssignFollowTarget(cam); if (!confinerBoundOk && confiner != null) if (GUILayout.Button("修复 Confiner 绑定", GUILayout.Height(20f))) FixConfinerBinding(cam, confiner); if (!visibleAreaOk) if (GUILayout.Button("修复 VisibleArea", GUILayout.Height(20f))) FixVisibleArea(cam); } } // Tilemap 适配按钮(始终可见,因为有时需要重新适配) EditorGUILayout.Space(2f); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("以 Ground Tilemap 范围调整边界", GUILayout.Height(20f))) FitConfinerToGroundTilemaps(cam); if (!blendOk) { EditorGUILayout.HelpBox( "_blendProfile 未设置,切换时使用控制器全局默认混合配置。", MessageType.None); } } } } // ── CameraTriggerZone 列表 ───────────────────────────────────────── private void DrawTriggerZonesSection() { if (_triggerZones.Count == 0) { EditorGUILayout.HelpBox( "场景中未找到 CameraTriggerZone。\n" + "至少需要一个触发器来在运行时激活 RoomCamera。\n" + "使用工具栏 \"Place Trigger Zone\" 快速生成。", MessageType.Info); return; } foreach (var zone in _triggerZones) { if (zone == null) continue; DrawTriggerZoneEntry(zone); } } private void DrawTriggerZoneEntry(CameraTriggerZone zone) { SerializedObject so = new SerializedObject(zone); bool hasTarget = so.FindProperty("_targetCamera").objectReferenceValue != null; using (new EditorGUILayout.HorizontalScope(_boxStyle)) { GUILayout.Label(hasTarget ? "✅" : "❌", GUILayout.Width(20f)); if (GUILayout.Button(zone.gameObject.name, EditorStyles.label, GUILayout.ExpandWidth(true))) Selection.activeGameObject = zone.gameObject; if (!hasTarget) EditorGUILayout.LabelField("⚠ _targetCamera 未绑定!", GUILayout.Width(160f)); if (GUILayout.Button("选中", GUILayout.Width(40f))) Selection.activeGameObject = zone.gameObject; } } // ══ 自动修复操作 ═══════════════════════════════════════════════════════ /// 为所有未设置 Follow 的 RoomCamera 自动绑定场景中 tag=Player 的 Transform。 private void BatchAssignFollowTarget() { int count = 0; foreach (var cam in _roomCameras) { if (cam == null) continue; if (AssignFollowTarget(cam)) count++; } if (count > 0) Debug.Log($"[RoomCameraSetupTool] 批量赋值:已为 {count} 台 RoomCamera 赋值 Follow 目标。"); else Debug.Log("[RoomCameraSetupTool] 批量赋值:所有 RoomCamera 均已设置 Follow,无需修改。"); } /// 为所有 Confiner2D.m_BoundingShape2D 未绑定的相机自动绑定子节点 PolygonCollider2D。 private void BatchFixConfinerBinding() { int count = 0; foreach (var cam in _roomCameras) { if (cam == null) continue; var confiner = cam.GetComponent(); if (confiner == null) continue; var so = new SerializedObject(confiner); if (so.FindProperty("m_BoundingShape2D").objectReferenceValue == null) { FixConfinerBinding(cam, confiner); count++; } } Debug.Log($"[RoomCameraSetupTool] 批量修复:已修复 {count} 台 RoomCamera 的 Confiner 绑定。"); } /// /// 在场景中查找 tag=Player 的 GameObject, /// 再在其下寻找名为 "CameraFollowTarget" 的子节点并赋给 CinemachineCamera.Follow。 /// 子节点不存在时会自动创建。 /// private bool AssignFollowTarget(RoomCamera cam) { CinemachineCamera vcam = cam.GetComponent(); if (vcam == null || vcam.Follow != null) return false; GameObject player = GameObject.FindWithTag("Player"); if (player == null) { Debug.LogWarning("[RoomCameraSetupTool] 场景中未找到 tag=Player 的对象,无法自动赋值 Follow。" + "请先放置 Player 对象(BaseGames → Scene → Place → Player)。"); return false; } const string followNodeName = "CameraFollowTarget"; Transform followTarget = player.transform.Find(followNodeName); if (followTarget == null) { // 子节点不存在则自动创建,位置归零 var go = new GameObject(followNodeName); Undo.RegisterCreatedObjectUndo(go, "Create CameraFollowTarget"); Undo.SetTransformParent(go.transform, player.transform, "Parent CameraFollowTarget"); go.transform.localPosition = Vector3.zero; go.transform.localRotation = Quaternion.identity; go.transform.localScale = Vector3.one; followTarget = go.transform; Debug.Log($"[RoomCameraSetupTool] 已在 Player 下自动创建 {followNodeName} 子节点。"); } Undo.RecordObject(vcam, "Assign Camera Follow Target"); vcam.Follow = followTarget; EditorUtility.SetDirty(vcam); return true; } /// 将子节点中找到的第一个 PolygonCollider2D 绑定到 CinemachineConfiner2D。 private void FixConfinerBinding(RoomCamera cam, CinemachineConfiner2D confiner) { PolygonCollider2D poly = cam.GetComponentInChildren(true); if (poly == null) { Debug.LogWarning($"[RoomCameraSetupTool] {cam.name}:子节点中未找到 PolygonCollider2D。" + "请确保 RoomBoundary 子对象存在(使用 Place Room Camera 创建)。"); return; } SerializedObject so = new SerializedObject(confiner); so.FindProperty("m_BoundingShape2D").objectReferenceValue = poly; so.ApplyModifiedProperties(); Debug.Log($"[RoomCameraSetupTool] {cam.name}:Confiner2D.m_BoundingShape2D → {poly.gameObject.name}"); } /// 将子节点中找到的 RoomVisibleArea 绑定到 RoomCamera._visibleArea。 private void FixVisibleArea(RoomCamera cam) { RoomVisibleArea existing = cam.GetComponentInChildren(true); if (existing == null) { Debug.LogWarning($"[RoomCameraSetupTool] {cam.name}:子节点中未找到 RoomVisibleArea。" + "请确保 RoomBoundary 子对象存在(使用 Place Room Camera 创建)。"); return; } SerializedObject so = new SerializedObject(cam); so.FindProperty("_visibleArea").objectReferenceValue = existing; so.ApplyModifiedProperties(); Debug.Log($"[RoomCameraSetupTool] {cam.name}:_visibleArea → {existing.gameObject.name}"); } /// /// 以场景中所有 Ground 层 Tilemap 的世界空间包围盒(合并后)来调整 /// RoomCamera 子节点 RoomBoundary 的 PolygonCollider2D 顶点,实现一键适配房间边界。 /// private void FitConfinerToGroundTilemaps(RoomCamera cam) { PolygonCollider2D poly = cam.GetComponentInChildren(true); if (poly == null) { Debug.LogWarning($"[RoomCameraSetupTool] {cam.name}:子节点中未找到 PolygonCollider2D,无法适配。"); return; } int groundLayer = LayerMask.NameToLayer("Ground"); var tilemaps = FindObjectsOfType(); Bounds? combined = null; foreach (var tm in tilemaps) { if (tm.gameObject.layer != groundLayer) continue; tm.CompressBounds(); Bounds worldBounds = TransformBounds(tm.transform, tm.localBounds); combined = combined.HasValue ? Combine(combined.Value, worldBounds) : worldBounds; } if (!combined.HasValue) { Debug.LogWarning("[RoomCameraSetupTool] 场景中未找到 Ground 层 Tilemap,无法自动适配。"); return; } Bounds b = combined.Value; // Convert to local space of PolygonCollider2D's transform Transform polyT = poly.transform; Vector2 LocalPt(Vector3 world) => polyT.InverseTransformPoint(world); Undo.RecordObject(poly, "Fit Confiner to Tilemap Bounds"); poly.SetPath(0, new Vector2[] { LocalPt(new Vector3(b.min.x, b.min.y)), LocalPt(new Vector3(b.min.x, b.max.y)), LocalPt(new Vector3(b.max.x, b.max.y)), LocalPt(new Vector3(b.max.x, b.min.y)), }); EditorUtility.SetDirty(poly); Debug.Log($"[RoomCameraSetupTool] {cam.name}:RoomBoundary 已适配至 Ground Tilemap 合并范围 " + $"({b.min.x:F1},{b.min.y:F1}) ~ ({b.max.x:F1},{b.max.y:F1})。"); } // ══ 工具方法 ═══════════════════════════════════════════════════════════ private static Bounds TransformBounds(Transform t, Bounds localBounds) { Bounds world = new Bounds(t.TransformPoint(localBounds.center), Vector3.zero); // 变换 8 个角点取包围 foreach (Vector3 corner in new[] { localBounds.min, localBounds.max, new Vector3(localBounds.min.x, localBounds.max.y, 0f), new Vector3(localBounds.max.x, localBounds.min.y, 0f), }) world.Encapsulate(t.TransformPoint(corner)); return world; } private static Bounds Combine(Bounds a, Bounds b) { a.Encapsulate(b.min); a.Encapsulate(b.max); return a; } // ══ GUI 辅助 ═══════════════════════════════════════════════════════════ private void EnsureStyles() { if (_boxStyle == null) { _boxStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(6, 6, 4, 4), }; } } private static void DrawSectionHeader(string title) { EditorGUILayout.Space(4f); using (new EditorGUILayout.HorizontalScope()) { GUILayout.Label(title, EditorStyles.boldLabel); } Rect r = EditorGUILayout.GetControlRect(false, 1f); EditorGUI.DrawRect(r, new Color(0.4f, 0.4f, 0.4f, 1f)); EditorGUILayout.Space(2f); } private static void DrawFieldCheck(SerializedObject so, string propName, string displayName, bool optional = false) { var prop = so.FindProperty(propName); bool ok = prop != null && prop.objectReferenceValue != null; DrawCheckRow(displayName, ok, optional); } private static void DrawCheckRow(string label, bool ok, bool optional = false) { using (new EditorGUILayout.HorizontalScope()) { Color prev = GUI.color; GUI.color = ok ? new Color(0.4f, 1f, 0.4f) : (optional ? new Color(0.8f, 0.8f, 0.4f) : new Color(1f, 0.4f, 0.4f)); GUILayout.Label(ok ? "●" : (optional ? "◌" : "✗"), GUILayout.Width(16f)); GUI.color = prev; EditorGUILayout.LabelField(label); } } } }