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;
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 _areaFoldouts = new Dictionary();
// ── 创建 CameraArea 输入状态 ─────────────────────────────────────
private string _newAreaName = "CameraArea";
private Transform _newAreaParent;
private CameraLensConfigSO _newLensConfig;
// ── SerializedObject 缓存(避免 OnGUI 每帧重复居复)────────────────
private readonly Dictionary _soCache = new Dictionary();
// ══ 菜单入口 ══════════════════════════════════════════════════════════
[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;
_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(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("↺ 全部同步限位区域", EditorStyles.toolbarButton))
SyncAllConfiners();
if (GUILayout.Button("✔ 批量创建专属 VCam", EditorStyles.toolbarButton))
BatchCreateDedicatedVCams();
}
// ── 创建 CameraArea 面板 ───────────────────────────────────────
DrawCreateAreaSection();
_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();
}
// ── 创建 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();
if (created != null)
{
var so = new SerializedObject(created);
so.FindProperty("_lensConfig").objectReferenceValue = lensToUse;
so.ApplyModifiedProperties();
}
}
RescanScene();
}
}
EditorGUILayout.Space(4f);
}
/// 扩展场景中已有 CameraArea,返回第一个非空的 LensConfig。
private CameraLensConfigSO DetectLensConfigFromScene()
{
foreach (var area in _cameraAreas)
{
if (area == null) continue;
if (area.LensConfig != null) return area.LensConfig;
}
return null;
}
// ── SerializedObject 缓存(避免 OnGUI 每帧重复创建,改善大列表滚动性能)──
///
/// 返回目标对象的缓存 SerializedObject,若不存在则创建。
/// 已缓存实例会调用 Update() 刷新,避免每次 OnGUI 都分配新对象。
///
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 的限位区域。");
}
///
/// 为所有尚未配置专有 VCam 的 CameraArea 批量创建专有 CinemachineCamera。
/// 已有 _dedicatedCamera 的区域将跳过。
///
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} 缺少 CinemachinePositionComposer(Body 组件)。\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() == 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() == null)
{
Undo.AddComponent(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;
}
}
// ══ 自动修复操作 ═══════════════════════════════════════════════════════
///
/// 在 AssetDatabase 中查找 EVT_PlayerSpawned(TransformEventChannelSO),
/// 写入 CameraStateController._onPlayerSpawned。
///
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(path);
if (asset == null) return;
controllerSO.Update();
controllerSO.FindProperty("_onPlayerSpawned").objectReferenceValue = asset;
controllerSO.ApplyModifiedProperties();
EditorUtility.SetDirty(controllerSO.targetObject);
Debug.Log($"[CameraAreaSetupTool] 已绑定 {assetName} → CameraStateController._onPlayerSpawned。");
}
///
/// 查找场景中 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 中绑定。");
}
/// 将子节点中找到的第一个不含 CameraTriggerZone 的 BoxCollider 绑定到 CameraArea._confinerCollider。
private static void FixConfinerBinding(CameraArea area)
{
BoxCollider box = FindBoundaryBox(area)
?? area.GetComponentInChildren(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}");
}
/// 返回 area 子节点中第一个不含 CameraTriggerZone 的 BoxCollider(即 AreaBoundary 限位体)。
private static BoxCollider FindBoundaryBox(CameraArea area)
{
foreach (var b in area.GetComponentsInChildren(true))
if (b.GetComponent() == null) return b;
return null;
}
///
/// 为指定 CameraArea 创建 AreaBoundary 子节点(默认 BoxCollider 限位体)并绑定到 _confinerCollider。
///
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()
?? childGo.AddComponent();
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} 创建 AreaBoundary(BoxCollider 默认 24 × 12)。");
}
/// 返回所有以此 area 为激活目标的 CameraTriggerZone。
private List FindTriggerZonesForArea(CameraArea area)
{
var result = new List();
foreach (var z in _triggerZones)
{
if (z == null) continue;
var so = GetOrCreateSO(z);
if (so.FindProperty("_targetArea").objectReferenceValue == area)
result.Add(z);
}
return result;
}
/// 为指定 CameraArea 创建配对的 CameraTriggerZone,自动匹配 Confiner 范围。
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
// 再通过 GetComponent 引用,避免顺序依赖问题
var zone = go.AddComponent();
var col = go.GetComponent();
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);
}
}
}
}