Files
zeling_v2/Assets/_Game/Scripts/Editor/Camera/RoomCameraSetupTool.cs

867 lines
39 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using BaseGames.Camera;
using Unity.Cinemachine;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Tilemaps;
namespace BaseGames.Editor
{
/// <summary>
/// 相机区域配置工具窗口。
/// 扫描当前已加载场景中的所有 CameraArea / CameraTriggerZone / CameraStateController
/// 显示各组件的绑定状态,并提供一键修复快捷操作。
///
/// 菜单BaseGames → Camera → Camera Area Setup
/// </summary>
public class CameraAreaSetupTool : EditorWindow
{
// ── 状态 ──────────────────────────────────────────────────────────────
private Vector2 _scroll;
private List<CameraArea> _cameraAreas = new List<CameraArea>();
private List<CameraTriggerZone> _triggerZones = new List<CameraTriggerZone>();
private CameraStateController _controller;
// ── GUI 样式缓存 ──────────────────────────────────────────────────────
private GUIStyle _boxStyle;
// ══ 菜单入口 ══════════════════════════════════════════════════════════
[MenuItem("BaseGames/Camera/Camera Area Setup", priority = 100)]
public static void ShowWindow()
{
var win = GetWindow<CameraAreaSetupTool>("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<CameraArea>(true));
_triggerZones.AddRange(root.GetComponentsInChildren<CameraTriggerZone>(true));
if (_controller == null)
_controller = root.GetComponentInChildren<CameraStateController>(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("CameraStateControllerPersistent 场景)");
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;
}
}
// ══ 自动修复操作 ═══════════════════════════════════════════════════════
/// <summary>
/// 查找场景中 tag=Player 的 Player/CameraFollowTarget
/// 写入 CameraStateController._vcamA 和 _vcamB 的 Follow 字段。
/// </summary>
private static void AssignFollowToGlobalVCams(SerializedObject controllerSO)
{
GameObject player = GameObject.FindWithTag("Player");
if (player == null)
{
Debug.LogWarning("[CameraAreaSetupTool] 场景中未找到 tag=Player 的对象。" +
"请先放置 PlayerBaseGames → 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 中绑定。");
}
/// <summary>将子节点中找到的第一个 PolygonCollider2D 绑定到 CameraArea._confinerCollider。</summary>
private static void FixConfinerBinding(CameraArea area)
{
PolygonCollider2D poly = area.GetComponentInChildren<PolygonCollider2D>(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
{
/// <summary>
/// 区域相机配置工具窗口。
/// 扫描当前已加载场景中的所有 RoomCamera / CameraTriggerZone / CameraStateController
/// 显示各组件的绑定状态,并提供一键修复快捷操作。
///
/// 菜单BaseGames → Camera → Room Camera Setup
/// </summary>
public class RoomCameraSetupTool : EditorWindow
{
// ── 状态 ──────────────────────────────────────────────────────────────
private Vector2 _scroll;
private List<RoomCamera> _roomCameras = new List<RoomCamera>();
private List<CameraTriggerZone> _triggerZones = new List<CameraTriggerZone>();
private CameraStateController _controller;
// ── GUI 样式缓存 ──────────────────────────────────────────────────────
private GUIStyle _boxStyle;
// ══ 菜单入口 ══════════════════════════════════════════════════════════
[MenuItem("BaseGames/Camera/Room Camera Setup", priority = 100)]
public static void ShowWindow()
{
var win = GetWindow<RoomCameraSetupTool>("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<RoomCamera>(true));
_triggerZones.AddRange(root.GetComponentsInChildren<CameraTriggerZone>(true));
if (_controller == null)
_controller = root.GetComponentInChildren<CameraStateController>(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<CinemachineCamera>();
CinemachineConfiner2D confiner = cam.GetComponent<CinemachineConfiner2D>();
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;
}
}
// ══ 自动修复操作 ═══════════════════════════════════════════════════════
/// <summary>为所有未设置 Follow 的 RoomCamera 自动绑定场景中 tag=Player 的 Transform。</summary>
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无需修改。");
}
/// <summary>为所有 Confiner2D.m_BoundingShape2D 未绑定的相机自动绑定子节点 PolygonCollider2D。</summary>
private void BatchFixConfinerBinding()
{
int count = 0;
foreach (var cam in _roomCameras)
{
if (cam == null) continue;
var confiner = cam.GetComponent<CinemachineConfiner2D>();
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 绑定。");
}
/// <summary>
/// 在场景中查找 tag=Player 的 GameObject
/// 再在其下寻找名为 "CameraFollowTarget" 的子节点并赋给 CinemachineCamera.Follow。
/// 子节点不存在时会自动创建。
/// </summary>
private bool AssignFollowTarget(RoomCamera cam)
{
CinemachineCamera vcam = cam.GetComponent<CinemachineCamera>();
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;
}
/// <summary>将子节点中找到的第一个 PolygonCollider2D 绑定到 CinemachineConfiner2D。</summary>
private void FixConfinerBinding(RoomCamera cam, CinemachineConfiner2D confiner)
{
PolygonCollider2D poly = cam.GetComponentInChildren<PolygonCollider2D>(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}");
}
/// <summary>将子节点中找到的 RoomVisibleArea 绑定到 RoomCamera._visibleArea。</summary>
private void FixVisibleArea(RoomCamera cam)
{
RoomVisibleArea existing = cam.GetComponentInChildren<RoomVisibleArea>(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}");
}
/// <summary>
/// 以场景中所有 Ground 层 Tilemap 的世界空间包围盒(合并后)来调整
/// RoomCamera 子节点 RoomBoundary 的 PolygonCollider2D 顶点,实现一键适配房间边界。
/// </summary>
private void FitConfinerToGroundTilemaps(RoomCamera cam)
{
PolygonCollider2D poly = cam.GetComponentInChildren<PolygonCollider2D>(true);
if (poly == null)
{
Debug.LogWarning($"[RoomCameraSetupTool] {cam.name}:子节点中未找到 PolygonCollider2D无法适配。");
return;
}
int groundLayer = LayerMask.NameToLayer("Ground");
var tilemaps = FindObjectsOfType<Tilemap>();
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);
}
}
}
}