734 lines
34 KiB
C#
734 lines
34 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();
|
||
}
|
||
|
||
// ── 创建 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 的限位区域。");
|
||
}
|
||
// ── CameraStateController ──────────────────────────────────────────
|
||
|
||
private void DrawControllerSection()
|
||
{
|
||
if (_controller == null)
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
"当前已加载场景中未找到 CameraStateController(正常)。\n" +
|
||
"该组件位于 Persistent 场景,单独编辑房间场景时不会加载。\n" +
|
||
"进入 Play Mode 前请确保 Persistent 场景已一同加载。",
|
||
MessageType.Info);
|
||
return;
|
||
}
|
||
|
||
using (new EditorGUILayout.VerticalScope(_boxStyle))
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
EditorGUILayout.LabelField("GameObject", GUILayout.Width(120f));
|
||
EditorGUILayout.ObjectField(_controller.gameObject, typeof(GameObject), true);
|
||
}
|
||
|
||
SerializedObject so = new SerializedObject(_controller);
|
||
DrawFieldCheck(so, "_vcamA", "全局 VCam A (CinemachineCamera)");
|
||
DrawFieldCheck(so, "_vcamB", "全局 VCam B (CinemachineCamera)");
|
||
DrawFieldCheck(so, "_brain", "CinemachineBrain");
|
||
DrawFieldCheck(so, "_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 allOk = confinerOk && hasZone;
|
||
|
||
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 = 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(可视边界 PolygonCollider2D)", confinerOk);
|
||
DrawCheckRow("_dedicatedCamera(专有 VCam,可选)",
|
||
so.FindProperty("_dedicatedCamera").objectReferenceValue != null, optional: true);
|
||
DrawCheckRow("_blendProfile(可选,未设则用全局默认)",
|
||
so.FindProperty("_blendProfile").objectReferenceValue != null, optional: true);
|
||
|
||
// ── 触发区域列表 ─────────────────────────────────────────────
|
||
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)
|
||
{
|
||
// 区分:有非 Trigger 的 PolygonCollider2D 可直接绑定 vs 完全没有 AreaBoundary
|
||
var existingBoundary = FindBoundaryPoly(area);
|
||
if (existingBoundary != null)
|
||
{
|
||
if (GUILayout.Button("修复:绑定子节点 PolygonCollider2D", 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(
|
||
"★ 可视边界:选中子节点的 PolygonCollider2D,在 Scene 视图中编辑顶点。",
|
||
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 的 PolygonCollider2D 绑定到 CameraArea._confinerCollider。</summary>
|
||
private static void FixConfinerBinding(CameraArea area)
|
||
{
|
||
PolygonCollider2D poly = FindBoundaryPoly(area)
|
||
?? area.GetComponentInChildren<PolygonCollider2D>(true);
|
||
if (poly == null)
|
||
{
|
||
Debug.LogWarning($"[CameraAreaSetupTool] {area.name}:子节点中未找到 PolygonCollider2D。");
|
||
return;
|
||
}
|
||
|
||
SerializedObject so = new SerializedObject(area);
|
||
so.FindProperty("_confinerCollider").objectReferenceValue = poly;
|
||
so.ApplyModifiedProperties();
|
||
|
||
Debug.Log($"[CameraAreaSetupTool] {area.name}:_confinerCollider → {poly.gameObject.name}");
|
||
}
|
||
|
||
/// <summary>返回 area 子节点中第一个不含 CameraTriggerZone 的 PolygonCollider2D(即 AreaBoundary 限位体)。</summary>
|
||
private static PolygonCollider2D FindBoundaryPoly(CameraArea area)
|
||
{
|
||
foreach (var p in area.GetComponentsInChildren<PolygonCollider2D>(true))
|
||
if (p.GetComponent<CameraTriggerZone>() == null) return p;
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为指定 CameraArea 创建 AreaBoundary 子节点(默认矩形限位多边形,isTrigger = false)并绑定到 _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;
|
||
}
|
||
|
||
PolygonCollider2D poly = childGo.GetComponent<PolygonCollider2D>()
|
||
?? childGo.AddComponent<PolygonCollider2D>();
|
||
poly.isTrigger = true; // 限位多边形,仅作为相机约束边界,不产生物理碰撞
|
||
poly.pathCount = 1;
|
||
poly.SetPath(0, new Vector2[]
|
||
{
|
||
new Vector2(-12f, -6f),
|
||
new Vector2(-12f, 6f),
|
||
new Vector2( 12f, 6f),
|
||
new Vector2( 12f, -6f),
|
||
});
|
||
EditorUtility.SetDirty(childGo);
|
||
|
||
SerializedObject so = new SerializedObject(area);
|
||
so.Update();
|
||
so.FindProperty("_confinerCollider").objectReferenceValue = poly;
|
||
so.ApplyModifiedProperties();
|
||
EditorUtility.SetDirty(area);
|
||
|
||
EditorGUIUtility.PingObject(childGo);
|
||
Debug.Log($"[CameraAreaSetupTool] 已为 {area.name} 创建 AreaBoundary(矩形 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)
|
||
{
|
||
// 用 PolygonCollider2D 包围盒作为放置中心和尺寸;没有则退回到 area 自身位置
|
||
Vector3 center = area.transform.position;
|
||
Vector2 size = new Vector2(4f, 4f);
|
||
|
||
var poly = area.GetComponentInChildren<PolygonCollider2D>(true);
|
||
if (poly != null)
|
||
{
|
||
Bounds b = poly.bounds;
|
||
center = b.center;
|
||
center.z = area.transform.position.z;
|
||
size = new Vector2(b.size.x, b.size.y);
|
||
}
|
||
|
||
var go = new GameObject($"{area.gameObject.name}_TriggerZone");
|
||
Undo.RegisterCreatedObjectUndo(go, "Create CameraTriggerZone");
|
||
// 归入 CameraArea 节点,与 AreaBoundary 同级,方便统一调整与查找
|
||
go.transform.SetParent(area.transform);
|
||
go.transform.position = center;
|
||
|
||
var col = go.AddComponent<PolygonCollider2D>();
|
||
col.isTrigger = true;
|
||
float hw = size.x * 0.5f;
|
||
float hh = size.y * 0.5f;
|
||
col.SetPath(0, new Vector2[]
|
||
{
|
||
new Vector2(-hw, -hh),
|
||
new Vector2(-hw, hh),
|
||
new Vector2( hw, hh),
|
||
new Vector2( hw, -hh),
|
||
});
|
||
|
||
var zone = go.AddComponent<CameraTriggerZone>();
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|