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