Files
zeling_v2/Assets/_Game/Scripts/Editor/Camera/RoomCameraSetupTool.cs
2026-05-19 11:50:21 +08:00

809 lines
38 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;
private static GUIStyle _headerLabelStyle;
// ── 颜色 ─────────────────────────────────────────────────────────────
private static readonly Color kOk = new Color(0.30f, 0.82f, 0.30f, 1f);
private static readonly Color kError = new Color(0.90f, 0.28f, 0.28f, 1f);
private static readonly Color kMuted = new Color(0.55f, 0.55f, 0.60f, 1f);
// ── 折叠状态 ─────────────────────────────────────────────────────────
private readonly Dictionary<int, bool> _areaFoldouts = new Dictionary<int, bool>();
// ── 创建 CameraArea 输入状态 ─────────────────────────────────────
private string _newAreaName = "CameraArea";
private Transform _newAreaParent;
private CameraLensConfigSO _newLensConfig;
// ── SerializedObject 缓存(避免 OnGUI 每帧重复居复)────────────────
private readonly Dictionary<int, SerializedObject> _soCache = new Dictionary<int, SerializedObject>();
// ══ 菜单入口 ══════════════════════════════════════════════════════════
[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;
_soCache.Clear();
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("↺ 全部同步限位区域", EditorStyles.toolbarButton))
SyncAllConfiners();
if (GUILayout.Button("✔ 批量创建专属 VCam", EditorStyles.toolbarButton))
BatchCreateDedicatedVCams();
}
// ── 创建 CameraArea 面板 ───────────────────────────────────────
DrawCreateAreaSection();
_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();
}
// ── 创建 CameraArea 面板 ──────────────────────────────────────────
private void DrawCreateAreaSection()
{
EnsureStyles();
using (new EditorGUILayout.VerticalScope(_boxStyle))
{
EditorGUILayout.LabelField("创建 Camera Area含配对 TriggerZone", EditorStyles.boldLabel);
_newAreaName = EditorGUILayout.TextField("名称", _newAreaName);
_newAreaParent = (Transform)EditorGUILayout.ObjectField(
"父节点(可选)", _newAreaParent, typeof(Transform), true);
_newLensConfig = (CameraLensConfigSO)EditorGUILayout.ObjectField(
new GUIContent("镜头配置 SO", "指定该区域使用的镜头配置,留空则自动继承场景中其他 CameraArea 的配置"),
_newLensConfig, typeof(CameraLensConfigSO), false);
if (GUILayout.Button("创建", GUILayout.Height(24f)))
{
if (string.IsNullOrWhiteSpace(_newAreaName)) _newAreaName = "CameraArea";
// 自动继承场景中已有区域的镜头配置
CameraLensConfigSO lensToUse = _newLensConfig ?? DetectLensConfigFromScene();
SceneObjectPlacerTool.PlaceCameraArea(_newAreaName, _newAreaParent);
// 将镜头配置写入新创建的 CameraArea
if (lensToUse != null)
{
var created = Selection.activeGameObject?.GetComponent<CameraArea>();
if (created != null)
{
var so = new SerializedObject(created);
so.FindProperty("_lensConfig").objectReferenceValue = lensToUse;
so.ApplyModifiedProperties();
}
}
RescanScene();
}
}
EditorGUILayout.Space(4f);
}
/// <summary>扩展场景中已有 CameraArea返回第一个非空的 LensConfig。</summary>
private CameraLensConfigSO DetectLensConfigFromScene()
{
foreach (var area in _cameraAreas)
{
if (area == null) continue;
if (area.LensConfig != null) return area.LensConfig;
}
return null;
}
// ── SerializedObject 缓存(避免 OnGUI 每帧重复创建,改善大列表滚动性能)──
/// <summary>
/// 返回目标对象的缓存 SerializedObject若不存在则创建。
/// 已缓存实例会调用 Update() 刷新,避免每次 OnGUI 都分配新对象。
/// </summary>
private SerializedObject GetOrCreateSO(UnityEngine.Object obj)
{
int id = obj.GetInstanceID();
if (!_soCache.TryGetValue(id, out var so) || so == null)
{
so = new SerializedObject(obj);
_soCache[id] = so;
}
else
{
so.Update();
}
return so;
}
// ── 全部同步限位区域 ────────────────────────────────────────
private void SyncAllConfiners()
{
if (_cameraAreas.Count == 0)
{
Debug.LogWarning("[CameraAreaSetupTool] 场景中无 CameraArea跳过同步。");
return;
}
int count = 0;
foreach (var area in _cameraAreas)
{
if (area == null || area.ConfinerCollider == null) continue;
CameraAreaEditor.SyncConfinerAuto(area);
count++;
}
Debug.Log($"[CameraAreaSetupTool] 已同步 {count} 个 CameraArea 的限位区域。");
}
/// <summary>
/// 为所有尚未配置专有 VCam 的 CameraArea 批量创建专有 CinemachineCamera。
/// 已有 _dedicatedCamera 的区域将跳过。
/// </summary>
private void BatchCreateDedicatedVCams()
{
if (_cameraAreas.Count == 0)
{
Debug.LogWarning("[CameraAreaSetupTool] 场景中无 CameraArea跳过批量创建。");
return;
}
int count = 0;
foreach (var area in _cameraAreas)
{
if (area == null) continue;
if (area.DedicatedCamera != null) continue; // 已有专有 VCam跳过
CameraAreaEditor.CreateDedicatedVCamForArea(area);
count++;
}
if (count > 0)
{
RescanScene();
Debug.Log($"[CameraAreaSetupTool] 已为 {count} 个 CameraArea 创建专有 VCam。");
}
else
{
Debug.Log("[CameraAreaSetupTool] 所有 CameraArea 均已有专有 VCam无需创建。");
}
}
// ── 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, "_onPlayerSpawned", "玩家生成事件 (EVT_PlayerSpawned) → VCam 自动绑 Follow");
DrawFieldCheck(so, "_impulseSource", "CinemachineImpulseSource", optional: true);
DrawFieldCheck(so, "_defaultBlendProfile","默认混合配置 (CameraBlendProfileSO)", optional: true);
EditorGUILayout.Space(4f);
// VCam 组件完整性检查
var vcamAProp = so.FindProperty("_vcamA");
var vcamBProp = so.FindProperty("_vcamB");
bool vcamAMissingComposer = IsMissingPositionComposer(vcamAProp);
bool vcamBMissingComposer = IsMissingPositionComposer(vcamBProp);
if (vcamAMissingComposer || vcamBMissingComposer)
{
string which = (vcamAMissingComposer && vcamBMissingComposer) ? "VCam A / B"
: vcamAMissingComposer ? "VCam A" : "VCam B";
EditorGUILayout.HelpBox(
$"{which} 缺少 CinemachinePositionComposerBody 组件)。\n" +
"没有该组件,相机不会跟随 Follow 目标移动,将固定在 Transform 位置。",
MessageType.Error);
if (GUILayout.Button($"为 {which} 添加 CinemachinePositionComposer", GUILayout.Height(24f)))
{
AddPositionComposerToVCams(so);
RescanScene();
}
}
bool needsEventAssign = so.FindProperty("_onPlayerSpawned").objectReferenceValue == null;
if (needsEventAssign)
{
if (GUILayout.Button("自动绑定 EVT_PlayerSpawned 事件频道", GUILayout.Height(24f)))
{
AssignPlayerSpawnedEvent(so);
RescanScene();
}
}
if (GUILayout.Button("为全局 VCam 赋值 Follow 目标Player/CameraFollowTarget", GUILayout.Height(24f)))
AssignFollowToGlobalVCams(so);
}
}
private static bool IsMissingPositionComposer(SerializedProperty vcamProp)
{
if (vcamProp?.objectReferenceValue is CinemachineCamera vcam)
return vcam.GetComponent<CinemachinePositionComposer>() == null;
return false;
}
private static void AddPositionComposerToVCams(SerializedObject controllerSO)
{
foreach (string fieldName in new[] { "_vcamA", "_vcamB" })
{
var prop = controllerSO.FindProperty(fieldName);
if (prop?.objectReferenceValue is CinemachineCamera vcam
&& vcam.GetComponent<CinemachinePositionComposer>() == null)
{
Undo.AddComponent<CinemachinePositionComposer>(vcam.gameObject);
Debug.Log($"[CameraAreaSetupTool] 已为 {vcam.name} 添加 CinemachinePositionComposer。");
}
}
}
// ── 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)
{
int id = area.GetInstanceID();
bool expanded = _areaFoldouts.TryGetValue(id, out bool v) && v;
SerializedObject so = GetOrCreateSO(area);
bool confinerOk = so.FindProperty("_confinerCollider").objectReferenceValue != null;
var boundZones = FindTriggerZonesForArea(area);
bool hasZone = boundZones.Count > 0;
bool hasVCam = so.FindProperty("_dedicatedCamera").objectReferenceValue != null;
bool allOk = confinerOk && hasZone && hasVCam;
using (new EditorGUILayout.VerticalScope(_boxStyle))
{
// ── 标题行 ──────────────────────────────────────────────────
using (new EditorGUILayout.HorizontalScope())
{
Color prevC = GUI.color;
GUI.color = allOk ? kOk : kError;
GUILayout.Label(allOk ? "●" : "✗", GUILayout.Width(16f));
GUI.color = prevC;
bool newExpanded = EditorGUILayout.Foldout(expanded, area.gameObject.name, true, EditorStyles.boldLabel);
if (newExpanded != expanded) _areaFoldouts[id] = newExpanded;
expanded = newExpanded;
GUI.color = hasZone ? kOk : kError;
GUILayout.Label(hasZone ? $"[{boundZones.Count} 触发器]" : "[无触发器]",
EditorStyles.miniLabel, GUILayout.Width(74f));
GUI.color = hasVCam ? kOk : kError;
GUILayout.Label(hasVCam ? "[VCam ✔]" : "[VCam ✗]",
EditorStyles.miniLabel, GUILayout.Width(54f));
GUI.color = prevC;
if (GUILayout.Button("⊙", GUILayout.Width(24f)))
EditorGUIUtility.PingObject(area.gameObject);
if (GUILayout.Button("选中", GUILayout.Width(42f)))
Selection.activeGameObject = area.gameObject;
}
if (!expanded) return;
EditorGUILayout.Space(3f);
// ── 绑定字段 ────────────────────────────────────────────────
DrawCheckRow("_confinerCollider可视边界 BoxCollider", confinerOk);
// ── 专有 VCam 状态行(创建 / Ping 按鈕)────────────────────────
using (new EditorGUILayout.HorizontalScope())
{
Color prevC2 = GUI.color;
GUI.color = hasVCam ? kOk : kError;
GUILayout.Label(hasVCam ? "●" : "✗", GUILayout.Width(16f));
GUI.color = prevC2;
if (hasVCam)
{
var vcamObj = so.FindProperty("_dedicatedCamera").objectReferenceValue;
EditorGUILayout.LabelField($"_dedicatedCamera{vcamObj.name}",
GUILayout.ExpandWidth(true));
if (GUILayout.Button("⊙", GUILayout.Width(24f)))
EditorGUIUtility.PingObject(vcamObj);
if (GUILayout.Button("选中", GUILayout.Width(36f)))
Selection.activeObject = vcamObj;
}
else
{
EditorGUILayout.LabelField("_dedicatedCamera专有 VCam未创建",
GUILayout.ExpandWidth(true));
if (GUILayout.Button("创建专有 VCam", GUILayout.Width(90f), GUILayout.Height(18f)))
{
CameraAreaEditor.CreateDedicatedVCamForArea(area);
RescanScene();
}
}
}
DrawCheckRow("_blendProfile可选未设则用全局默认",
so.FindProperty("_blendProfile").objectReferenceValue != null, optional: true);
// ── VCam 扩展组件顺序检查 ────────────────────────────────────
// AsymmetricDamping/FallBias/FacingBias 必须在 CinemachineConfiner3D 之前;
// AxisLock 必须在之后。顺序错误会使相机逃出限位区域。
if (hasVCam)
{
var vcam = so.FindProperty("_dedicatedCamera").objectReferenceValue as CinemachineCamera;
string issue = CameraAreaEditor.CheckVCamExtensionOrderIssue(vcam);
if (issue != null)
{
EditorGUILayout.HelpBox(
$"VCam 扩展组件顺序错误!相机会逃出限位区域:\n{issue}",
MessageType.Error);
if (GUILayout.Button("⚙ 自动修正组件顺序", GUILayout.Height(22f)))
{
CameraAreaEditor.FixVCamExtensionOrder(vcam);
RescanScene();
}
}
}
// ── 触发区域列表 ─────────────────────────────────────────────
EditorGUILayout.Space(3f);
using (new EditorGUILayout.HorizontalScope())
{
Color prevC = GUI.color;
GUI.color = hasZone ? kOk : kError;
GUILayout.Label(hasZone ? "● 触发区域" : "✗ 无触发区域",
EditorStyles.miniLabel, GUILayout.Width(90f));
GUI.color = prevC;
if (!hasZone)
{
if (GUILayout.Button("创建配对 TriggerZone", GUILayout.Height(20f)))
{
CreateTriggerZoneForArea(area);
RescanScene();
}
}
}
foreach (var z in boundZones)
{
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Space(16f);
Color prevC = GUI.color;
GUI.color = kMuted;
GUILayout.Label("→", GUILayout.Width(14f));
GUI.color = prevC;
if (GUILayout.Button(z.gameObject.name, EditorStyles.label))
Selection.activeGameObject = z.gameObject;
if (GUILayout.Button("⊙", GUILayout.Width(24f)))
EditorGUIUtility.PingObject(z.gameObject);
}
}
// ── 操作按钮 ────────────────────────────────────────────────
EditorGUILayout.Space(3f);
if (!confinerOk)
{
// 区分:有 BoxCollider 可直接绑定 vs 完全没有 AreaBoundary
var existingBoundary = FindBoundaryBox(area);
if (existingBoundary != null)
{
if (GUILayout.Button("修复:绑定子节点 BoxCollider", GUILayout.Height(22f)))
FixConfinerBinding(area);
}
else
{
if (GUILayout.Button("创建 AreaBoundary限位体积默认 24 × 12", GUILayout.Height(22f)))
{
CreateAreaBoundary(area);
RescanScene();
}
}
}
// ── 提示 ─────────────────────────────────────────────────────
Color helpC = GUI.color;
GUI.color = kMuted;
EditorGUILayout.LabelField(
"★ 限位体积:选中子节点的 BoxCollider在 Inspector 中编辑 Center / Size。",
EditorStyles.miniLabel);
GUI.color = helpC;
}
}
// ── 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 = GetOrCreateSO(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>
/// 在 AssetDatabase 中查找 EVT_PlayerSpawnedTransformEventChannelSO
/// 写入 CameraStateController._onPlayerSpawned。
/// </summary>
private static void AssignPlayerSpawnedEvent(SerializedObject controllerSO)
{
const string assetName = "EVT_PlayerSpawned";
string[] guids = AssetDatabase.FindAssets($"t:TransformEventChannelSO {assetName}");
if (guids.Length == 0)
{
Debug.LogWarning(
$"[CameraAreaSetupTool] 未找到 {assetName} 资产。" +
"请先通过 BaseGames → Events → Create Default Event Channels 生成事件频道资产。");
return;
}
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
if (asset == null) return;
controllerSO.Update();
controllerSO.FindProperty("_onPlayerSpawned").objectReferenceValue = asset;
controllerSO.ApplyModifiedProperties();
EditorUtility.SetDirty(controllerSO.targetObject);
Debug.Log($"[CameraAreaSetupTool] 已绑定 {assetName} → CameraStateController._onPlayerSpawned。");
}
/// <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>将子节点中找到的第一个不含 CameraTriggerZone 的 BoxCollider 绑定到 CameraArea._confinerCollider。</summary>
private static void FixConfinerBinding(CameraArea area)
{
BoxCollider box = FindBoundaryBox(area)
?? area.GetComponentInChildren<BoxCollider>(true);
if (box == null)
{
Debug.LogWarning($"[CameraAreaSetupTool] {area.name}:子节点中未找到 BoxCollider。");
return;
}
SerializedObject so = new SerializedObject(area);
so.FindProperty("_confinerCollider").objectReferenceValue = box;
so.ApplyModifiedProperties();
Debug.Log($"[CameraAreaSetupTool] {area.name}_confinerCollider → {box.gameObject.name}");
}
/// <summary>返回 area 子节点中第一个不含 CameraTriggerZone 的 BoxCollider即 AreaBoundary 限位体)。</summary>
private static BoxCollider FindBoundaryBox(CameraArea area)
{
foreach (var b in area.GetComponentsInChildren<BoxCollider>(true))
if (b.GetComponent<CameraTriggerZone>() == null) return b;
return null;
}
/// <summary>
/// 为指定 CameraArea 创建 AreaBoundary 子节点(默认 BoxCollider 限位体)并绑定到 _confinerCollider。
/// </summary>
private static void CreateAreaBoundary(CameraArea area)
{
Transform existing = area.transform.Find("AreaBoundary");
GameObject childGo;
if (existing != null)
{
childGo = existing.gameObject;
}
else
{
childGo = new GameObject("AreaBoundary");
Undo.RegisterCreatedObjectUndo(childGo, "Create AreaBoundary");
childGo.transform.SetParent(area.transform);
childGo.transform.localPosition = Vector3.zero;
}
BoxCollider box = childGo.GetComponent<BoxCollider>()
?? childGo.AddComponent<BoxCollider>();
box.center = new Vector3(0f, 0f, -10f); // Z 占位符,绑定 LensConfig 后点击「同步限位区域」更新
box.size = new Vector3(24f, 12f, 1f); // 默认 24 × 12 占位符
EditorUtility.SetDirty(childGo);
SerializedObject so = new SerializedObject(area);
so.Update();
so.FindProperty("_confinerCollider").objectReferenceValue = box;
so.ApplyModifiedProperties();
EditorUtility.SetDirty(area);
EditorGUIUtility.PingObject(childGo);
Debug.Log($"[CameraAreaSetupTool] 已为 {area.name} 创建 AreaBoundaryBoxCollider 默认 24 × 12。");
}
/// <summary>返回所有以此 area 为激活目标的 CameraTriggerZone。</summary>
private List<CameraTriggerZone> FindTriggerZonesForArea(CameraArea area)
{
var result = new List<CameraTriggerZone>();
foreach (var z in _triggerZones)
{
if (z == null) continue;
var so = GetOrCreateSO(z);
if (so.FindProperty("_targetArea").objectReferenceValue == area)
result.Add(z);
}
return result;
}
/// <summary>为指定 CameraArea 创建配对的 CameraTriggerZone自动匹配 Confiner 范围。</summary>
private static void CreateTriggerZoneForArea(CameraArea area)
{
// 用 VisibleBounds 作为放置中心和初始多边形范围
Rect visible = area.VisibleBounds;
Vector3 center = new Vector3(visible.center.x, visible.center.y, area.transform.position.z);
Vector2 half = visible.size * 0.5f;
var go = new GameObject($"{area.gameObject.name}_TriggerZone");
Undo.RegisterCreatedObjectUndo(go, "Create CameraTriggerZone");
// 归入 CameraArea 节点,与 AreaBoundary 同级,方便统一调整与查找
go.transform.SetParent(area.transform);
go.transform.position = center;
// [RequireComponent] 会自动附加 PolygonCollider2D先 AddComponent<CameraTriggerZone>
// 再通过 GetComponent 引用,避免顺序依赖问题
var zone = go.AddComponent<CameraTriggerZone>();
var col = go.GetComponent<PolygonCollider2D>();
col.isTrigger = true;
// 以 VisibleBounds 矩形四角为默认路径(可在 Inspector 中进一步编辑顶点)
col.SetPath(0, new Vector2[]
{
new Vector2(-half.x, -half.y),
new Vector2(-half.x, half.y),
new Vector2( half.x, half.y),
new Vector2( half.x, -half.y),
});
var so = new SerializedObject(zone);
so.Update();
so.FindProperty("_targetArea").objectReferenceValue = area;
so.ApplyModifiedProperties();
Selection.activeGameObject = go;
EditorGUIUtility.PingObject(go);
Debug.Log($"[CameraAreaSetupTool] 已为 {area.name} 创建配对 TriggerZone{go.name}");
}
// ══ GUI 辅助 ═══════════════════════════════════════════════════════════
private void EnsureStyles()
{
if (_boxStyle == null)
_boxStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(6, 6, 4, 4) };
if (_headerLabelStyle == null)
_headerLabelStyle = new GUIStyle(EditorStyles.boldLabel)
{
normal = { textColor = Color.white },
hover = { textColor = Color.white },
};
}
private static void DrawSectionHeader(string title)
{
EditorGUILayout.Space(4f);
Rect r = EditorGUILayout.GetControlRect(false, 22f);
EditorGUI.DrawRect(r, new Color(0.22f, 0.22f, 0.28f, 1f));
var style = _headerLabelStyle ?? EditorStyles.boldLabel;
EditorGUI.LabelField(new Rect(r.x + 8f, r.y + 3f, r.width - 8f, 18f), title, style);
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);
}
}
}
}