809 lines
38 KiB
C#
809 lines
38 KiB
C#
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("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<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} 缺少 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<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_PlayerSpawned(TransformEventChannelSO),
|
||
/// 写入 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 的对象。" +
|
||
"请先放置 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 中绑定。");
|
||
}
|
||
|
||
/// <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} 创建 AreaBoundary(BoxCollider 默认 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);
|
||
}
|
||
}
|
||
}
|
||
}
|