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);
}
}
}
}