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

641 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Cinemachine;
using BaseGames.Core;
using BaseGames.Core.Events;
namespace BaseGames.Camera
{
/// <summary>
/// 相机状态单例控制器。须放置在 Persistent 场景中。
///
/// 每个 <see cref="CameraArea"/> 均拥有自己专属的 <c>DedicatedCamera</c>
/// 进入区域时调用 <see cref="ActivateDedicated"/> 激活对应 VCam
/// Cinemachine Brain 自动处理混合过渡。
/// </summary>
[DefaultExecutionOrder(-100)]
public class CameraStateController : MonoBehaviour, ICameraService
{
[Header("引用")]
[SerializeField] private CinemachineBrain _brain;
[SerializeField] private CinemachineImpulseSource _impulseSource;
[SerializeField] private CameraLookSystem _lookSystem;
[Header("默认混合配置")]
[SerializeField] private CameraBlendProfileSO _defaultBlendProfile;
[Header("镜头配置")]
[Tooltip("相机镜头参数 SO提供 FOV 与相机深度。\n" +
"与各 CameraArea 引用同一资产,确保 SetLensSize 换算结果与 VCam 配置一致。")]
[SerializeField] private CameraLensConfigSO _lensConfig;
[Header("玩家跟随")]
[Tooltip("PlayerController 生成时广播的事件频道EVT_PlayerSpawned。\n" +
"收到后自动查找 CameraFollowTarget 子节点并作为 VCam Follow 赋值。")]
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
// ── 状态 ──────────────────────────────────────────────────────────────
private CameraArea _roomBaselineArea; // SwitchArea(priority=0) 写入的房间基线,不被触发事件删除
private readonly List<(CameraArea area, int priority)> _activeZones = new(); // 玩家当前所在的触发区域集合priority>0
private CameraArea _currentArea;
private CinemachineCamera _activeDedicatedCam;
private CinemachineCamera _cutsceneCamera; // 过场模式专用高优先级 VCam
private const int CutscenePriority = 100; // 高于专有区域 VCam默认 20
private int _lastExternalFacing = 0; // 最近一次 SetPlayerFacing 的值,用于新激活 VCam 的初始化
private Transform _currentFollowTarget; // 最后一次 SetFollowTarget 设置的目标,激活 VCam 时自动同步
private readonly CompositeDisposable _subs = new();
// ── Lifecycle ────────────────────────────────────────────────────────
private void Awake()
{
if (ServiceLocator.GetOrDefault<ICameraService>() != null) { Destroy(gameObject); return; }
ServiceLocator.Register<ICameraService>(this);
// 重置运行时状态,防止禁用 Domain Reload 时上一次 Play Mode 的数据残留。
// 非序列化字段在 Domain Reload 禁用时不会自动清零。
_roomBaselineArea = null;
_activeZones.Clear();
_currentArea = null;
_activeDedicatedCam = null;
_lastExternalFacing = 0;
_currentFollowTarget = null;
// 订阅 PlayerSpawned 事件,运行时自动为 VCam 赋值 Follow
_onPlayerSpawned?.Subscribe(OnPlayerSpawned).AddTo(_subs);
}
private void OnDestroy()
{
_subs.Dispose();
ServiceLocator.Unregister<ICameraService>(this);
}
private void OnPlayerSpawned(Transform playerRoot)
{
const string followNodeName = "CameraFollowTarget";
Transform follow = playerRoot.Find(followNodeName) ?? playerRoot;
SetFollowTarget(follow);
}
private void Start()
{
// 场景启动时扫描全部 VCam提前暴露组件顺序错误
// 无需等待各区域被激活才触发检测。
var allVCams = FindObjectsByType<CinemachineCamera>(FindObjectsSortMode.None);
foreach (var vcam in allVCams)
{
var confiner = vcam.GetComponent<CinemachineConfiner3D>();
if (confiner != null)
ValidateVCamExtensionOrder(vcam, confiner);
}
}
// ── 公开 API ──────────────────────────────────────────────────────────
/// <summary>
/// 切换到目标相机区域。<paramref name="priority"/> &lt; 当前激活优先级时忽略。
/// <para>priority = 0始终执行适合 RoomController 入场初始化)。</para>
/// </summary>
public void SwitchArea(CameraArea area, int priority = 0, bool instantCut = false)
{
if (area == null) return;
if (priority == 0)
{
// 房间初始化 / 无条件切换:记录基线并清空触发集合
_roomBaselineArea = area;
_activeZones.Clear();
ActivateArea(area, instantCut);
return;
}
// 触发区域进入:更新集合(同一区域去重后重新加入,保证最新优先级)
_activeZones.RemoveAll(e => e.area == area);
_activeZones.Add((area, priority));
// 仅当此区域是当前最优且尚未激活时才切换
CameraArea best = GetEffectiveArea();
if (best == area && area != _currentArea)
ActivateArea(area, instantCut);
}
/// <summary>
/// 释放 <paramref name="releasedArea"/> 的权限。
/// 从优先级栈中移除该区域;若它是当前激活区域,则激活新栈顶(或 fallback
/// </summary>
public void ReleaseArea(CameraArea releasedArea, CameraArea fallback)
{
if (releasedArea == null) return;
bool wasActive = releasedArea == _currentArea;
int removed = _activeZones.RemoveAll(e => e.area == releasedArea);
// 若区域本就不在栈中,且又不是当前激活区,则无需任何操作
if (removed == 0 && !wasActive) return;
if (!wasActive) return;
// 回退到当前最优区域(触发集合 → 房间基线 → fallback
CameraArea next = GetEffectiveArea() ?? fallback;
if (next != null && next != _currentArea)
ActivateArea(next, instantCut: false);
}
/// <summary>
/// 返回当前应激活的区域:<see cref="_activeZones"/> 中优先级最高的
/// (同优先级取最近进入的),若触发集合为空则回退到 <see cref="_roomBaselineArea"/>。
/// </summary>
private CameraArea GetEffectiveArea()
{
CameraArea best = null;
int bestPriority = -1;
foreach (var (a, p) in _activeZones)
if (p >= bestPriority) { bestPriority = p; best = a; }
return best ?? _roomBaselineArea;
}
private void ActivateArea(CameraArea area, bool instantCut = false)
{
_currentArea = area;
if (instantCut)
{
// 房间入口硬切:相机立即跳到新房间位置,无混合动画
if (_brain != null)
_brain.DefaultBlend = new CinemachineBlendDefinition
{
Style = CinemachineBlendDefinition.Styles.Cut,
Time = 0f,
};
// 重置窥视偏移,避免旧房间的窥视状态残留
_lookSystem?.ResetOffsets(snap: true);
// 重置专属 VCam 扩展的内部状态,防止旧房间的速度/阻尼估算带入新房间
if (area.HasDedicated) ResetVCamExtensions(area.DedicatedCamera);
}
else
{
ApplyBlendProfile(area.BlendProfile ?? _defaultBlendProfile);
}
if (area.LensSize > 0f)
SetLensSize(area.LensSize, area.LensSizeDuration);
if (area.HasDedicated)
ActivateDedicated(area);
else
Debug.LogError($"[CameraStateController] {area.name} 缺少专属 VCam请通过 Camera Area Setup 工具为此区域创建 DedicatedCamera。");
}
/// <summary>
/// 运行时设置跟随目标。
/// 若存在 <see cref="CameraLookSystem"/>VCam 跟随系统输出的虚拟目标(含窗斥偏移)。
/// </summary>
public void SetFollowTarget(Transform followTarget)
{
Transform actual = followTarget;
if (_lookSystem != null)
{
_lookSystem.SetBaseTarget(followTarget);
actual = _lookSystem.VirtualTarget;
}
_currentFollowTarget = actual;
SyncFollowToVCam(_activeDedicatedCam); // 立即同步到当前活跃专有 VCam
}
/// <summary>触发屏幕抖动。</summary>
public void TriggerImpulse(Vector3 velocity)
{
if (_impulseSource != null) _impulseSource.GenerateImpulse(velocity);
}
/// <summary>以默认强度触发屏幕抖动。</summary>
public void TriggerImpulse(float strength = 0.3f)
=> TriggerImpulse(Vector3.down * strength);
/// <summary>
/// 平滑过渡视野尺寸(可视半高,世界单位)。<paramref name="duration"/> = 0 时瞬间切换。
/// 透视相机下自动换算为 FOV语义等价于正交相机 OrthographicSize。
/// 区域进入时由 <see cref="CameraArea"/> 自动调用;游戏代码也可直接调用。
/// </summary>
public void SetLensSize(float visibleHalfHeight, float duration = 0f)
{
if (_lensCoroutine != null) StopCoroutine(_lensCoroutine);
if (duration <= 0f) { ApplyLensSizeToAll(visibleHalfHeight); return; }
_lensCoroutine = StartCoroutine(LensSizeCo(visibleHalfHeight, duration));
}
/// <summary>
/// 进入过场模式,将指定 VCam 提升至最高优先级Brain 自动混合到它。
/// <para>
/// 过场 VCam 由设计者在 Inspector 中预先配置位置、Follow、LookAt、Lens 等);
/// 此方法不强制覆写 Follow保留 Inspector 配置不变。
/// </para>
/// <para>适用场景Boss 登场固定镜头、对话拉近、剧情事件全景等。</para>
/// </summary>
public void EnterCutsceneMode(CinemachineCamera cutsceneCamera)
{
if (cutsceneCamera == null) return;
if (_cutsceneCamera != null && _cutsceneCamera != cutsceneCamera)
_cutsceneCamera.Priority = 0;
_cutsceneCamera = cutsceneCamera;
_cutsceneCamera.Priority = CutscenePriority;
}
/// <summary>
/// 退出过场模式,撤销过场 VCam 的优先级Brain 自动混合回当前区域相机。
/// </summary>
public void ExitCutsceneMode()
{
if (_cutsceneCamera == null) return;
_cutsceneCamera.Priority = 0;
_cutsceneCamera = null;
}
/// <summary>
/// 通知相机系统玩家的面朝方向,转发至所有活跃 VCam 的方向偏置扩展。
/// 由 PlayerController 在精灵翻转时调用:<c>ICameraService.SetPlayerFacing(翻转方向 ? +1 : -1)</c>。
/// </summary>
public void SetPlayerFacing(int direction)
{
_lastExternalFacing = direction;
SetFacingOnVCam(_activeDedicatedCam, direction);
}
private static void SetFacingOnVCam(CinemachineCamera vcam, int direction)
=> vcam?.GetComponent<CameraFacingBiasExtension>()?.SetExternalFacing(direction);
private Coroutine _lensCoroutine;
private void ApplyLensSizeToAll(float size)
{
if (_activeDedicatedCam != null) SetVcamLens(_activeDedicatedCam, size);
}
// size = 可见半高(世界单位),透视相机下等效于正交 OrthographicSize。
// 换算公式FOV = 2 * atan(size / depth)
private void SetVcamLens(CinemachineCamera vcam, float size)
{
if (vcam == null) return;
var lens = vcam.Lens;
float depth = _lensConfig != null ? _lensConfig.cameraDepth : 10f;
lens.FieldOfView = 2f * Mathf.Atan(size / depth) * Mathf.Rad2Deg;
vcam.Lens = lens;
}
private IEnumerator LensSizeCo(float target, float duration)
{
CinemachineCamera active = GetActiveVcam();
if (active == null) { _lensCoroutine = null; yield break; }
// 透视相机:从当前 FOV 反算等效可见半高,作为插值起点
float depth = _lensConfig != null ? _lensConfig.cameraDepth : 10f;
float start = depth * Mathf.Tan(active.Lens.FieldOfView * 0.5f * Mathf.Deg2Rad);
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
// 使用平滑过渡曲线ease-in-out视野缩放手感更自然。
// 线性插值会让镜头拉远感觉机械;平滑步多出平稳的起笔和收尾弹性。
float t = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(elapsed / duration));
ApplyLensSizeToAll(Mathf.Lerp(start, target, t));
yield return null;
}
ApplyLensSizeToAll(target);
_lensCoroutine = null;
}
private CinemachineCamera GetActiveVcam() => _activeDedicatedCam;
// ── 内部方法 ──────────────────────────────────────────────────────────
/// <summary>激活区域的专有 VCam高优先级。</summary>
private void ActivateDedicated(CameraArea area)
{
// 降低前一个专有 VCam若与新的不同
if (_activeDedicatedCam != null && _activeDedicatedCam != area.DedicatedCamera)
_activeDedicatedCam.Priority = 0;
_activeDedicatedCam = area.DedicatedCamera;
_activeDedicatedCam.Priority = area.DedicatedPriority;
SyncFollowToVCam(_activeDedicatedCam); // 确保专有 VCam 的 Follow 指向当前跟随目标
SetFacingOnVCam(_activeDedicatedCam, _lastExternalFacing); // 应用当前玩家朝向
// 应用 CameraArea 参数Confiner、Composer、扩展组件等
var dedicatedConfiner = _activeDedicatedCam.GetComponent<CinemachineConfiner3D>();
ValidateVCamExtensionOrder(_activeDedicatedCam, dedicatedConfiner);
ConfigureSlot(_activeDedicatedCam, dedicatedConfiner, area);
}
/// <summary>
/// 检查 VCam 上各扩展组件的挂载顺序是否正确。
/// <list type="bullet">
/// <item>FallBiasExtension / FacingBiasExtension 必须在 CinemachineConfiner3D 之前</item>
/// <item>AxisLockExtension 必须在 CinemachineConfiner3D 之后</item>
/// </list>
/// 顺序错误时相机会在应用偏置后逃出限位区域,或轴锁被 Confiner 覆盖失效。
/// </summary>
private static void ValidateVCamExtensionOrder(CinemachineCamera vcam, CinemachineConfiner3D confiner)
{
if (vcam == null) return;
if (confiner == null)
{
Debug.LogWarning(
$"[CameraStateController] VCam <b>{vcam.name}</b> 缺少 CinemachineConfiner3D 组件!" +
"相机将不受任何限位约束,请通过 CameraAreaEditor 重新生成此 VCam 或手动添加。");
return;
}
Component[] comps = vcam.GetComponents<Component>();
int confinerIdx = -1;
int fallBiasIdx = -1;
int facingBiasIdx = -1;
int axisLockIdx = -1;
int asymDampIdx = -1;
int adaptiveLahIdx = -1;
for (int i = 0; i < comps.Length; i++)
{
switch (comps[i])
{
case CinemachineConfiner3D _: confinerIdx = i; break;
case CameraFallBiasExtension _: fallBiasIdx = i; break;
case CameraFacingBiasExtension _: facingBiasIdx = i; break;
case CameraAxisLockExtension _: axisLockIdx = i; break;
case CameraAsymmetricDampingExtension _: asymDampIdx = i; break;
case CameraAdaptiveLookaheadExtension _: adaptiveLahIdx = i; break;
}
}
if (asymDampIdx >= 0 && asymDampIdx > confinerIdx)
Debug.LogError(
$"[CameraStateController] VCam <b>{vcam.name}</b>" +
"CameraAsymmetricDampingExtension 必须在 CinemachineConfiner3D 之前!" +
"当前顺序导致Y轴阻尼平滑值将相机推出限位区域而不被重新裁剪。" +
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
if (fallBiasIdx >= 0 && fallBiasIdx > confinerIdx)
Debug.LogError(
$"[CameraStateController] VCam <b>{vcam.name}</b>" +
"CameraFallBiasExtension 必须在 CinemachineConfiner3D 之前!" +
"当前顺序导致下坠偏置将相机推出限位区域而不被重新裁剪。" +
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
if (facingBiasIdx >= 0 && facingBiasIdx > confinerIdx)
Debug.LogError(
$"[CameraStateController] VCam <b>{vcam.name}</b>" +
"CameraFacingBiasExtension 必须在 CinemachineConfiner3D 之前!" +
"当前顺序导致朝向偏置将相机推出限位区域而不被重新裁剪。" +
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
if (adaptiveLahIdx >= 0 && adaptiveLahIdx > confinerIdx)
Debug.LogError(
$"[CameraStateController] VCam <b>{vcam.name}</b>" +
"CameraAdaptiveLookaheadExtension 必须在 CinemachineConfiner3D 之前!" +
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
if (axisLockIdx >= 0 && axisLockIdx < confinerIdx)
Debug.LogError(
$"[CameraStateController] VCam <b>{vcam.name}</b>" +
"CameraAxisLockExtension 必须在 CinemachineConfiner3D 之后!" +
"当前顺序导致轴向锁定被 Confiner 覆盖而失效。" +
"请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。");
}
/// <summary>
/// 将最后已知的 Follow 目标同步到指定 VCam若其 Follow 尚未设置或已过期)。
/// </summary>
private void SyncFollowToVCam(CinemachineCamera vcam)
{
if (vcam == null || _currentFollowTarget == null) return;
if (vcam.Follow != _currentFollowTarget)
vcam.Follow = _currentFollowTarget;
}
private static void ConfigureSlot(
CinemachineCamera vcam, CinemachineConfiner3D confiner, CameraArea area)
{
// 1. Confiner
if (confiner != null && area.ConfinerCollider != null)
{
confiner.BoundingVolume = area.ConfinerCollider;
}
else if (confiner != null && area.ConfinerCollider == null)
{
Debug.LogError(
$"[CameraStateController] {area.name} 未绑定 ConfinerCollider" +
"请将子节点 AreaBoundary 的 BoxCollider 拖入 CameraArea._confinerCollider 字段。");
}
// 2. 跟随行为覆盖
if (area.OverrideFollowBehaviour)
{
var composer = vcam.GetComponent<CinemachinePositionComposer>();
if (composer != null)
{
// 屏幕位置Y 偏下 → 玩家稍低于中心,上方更多视野)
var comp = composer.Composition;
comp.ScreenPosition = area.ScreenPosition;
comp.DeadZone.Enabled = true;
comp.DeadZone.Size = area.DeadZoneSize;
composer.Composition = comp;
// 阻尼
var d = composer.Damping;
d.x = area.Damping.x;
d.y = area.Damping.y;
composer.Damping = d;
// 非对称 Y 阻尼:若扩展存在,将其按区域配置并清零 Composer Y 阻尼防止叠加
var asymDamp = vcam.GetComponent<CameraAsymmetricDampingExtension>();
if (asymDamp != null)
{
asymDamp.DampingDown = area.DampingDown;
asymDamp.DampingUp = area.DampingUp;
var yd = composer.Damping;
yd.y = 0f;
composer.Damping = yd;
}
// 引领预测
var lah = composer.Lookahead;
lah.Enabled = area.LookaheadTime > 0f;
lah.Time = area.LookaheadTime;
lah.Smoothing = area.LookaheadSmoothing;
lah.IgnoreY = true; // 平台跳跃中 Y 轴 Lookahead 会在起跳时猛拉镜头,应关闭
composer.Lookahead = lah;
// 自适应 Lookahead通知扩展当前区域配置的最大 Lookahead 值
var adaptiveLah = vcam.GetComponent<CameraAdaptiveLookaheadExtension>();
if (adaptiveLah != null)
adaptiveLah.SetConfiguredMax(area.LookaheadTime);
}
}
// 3. 轴向约束
var axisLock = vcam.GetComponent<CameraAxisLockExtension>();
if (axisLock != null)
{
axisLock.LockX = area.LockHorizontal;
axisLock.LockY = area.LockVertical;
if (area.ConfinerCollider != null)
{
var center = area.ConfinerCollider.bounds.center;
axisLock.LockedX = center.x;
axisLock.LockedY = center.y;
}
}
// 4. 下坠视野偏置(无论是否覆写跟随行为,始终按区域配置)
var fallBias = vcam.GetComponent<CameraFallBiasExtension>();
if (fallBias != null)
fallBias.SetConfiguredMax(area.DisableFallBias ? 0f : -1f);
// 5. 方向感知水平偏置
// X 轴已锁定时强制关闭偏置:两者均在 Body Stage 执行,若偏置后于锁定运行会破坏轴锁。
var facingBias = vcam.GetComponent<CameraFacingBiasExtension>();
if (facingBias != null)
{
if (area.LockHorizontal)
facingBias.FacingBias = 0f;
else if (area.OverrideFacingBias)
facingBias.FacingBias = area.FacingBiasOverride;
}
// 6. 相机噪音(区域氛围震动:洞穴、水下、机械等)
var noise = vcam.GetComponent<CinemachineBasicMultiChannelPerlin>();
if (noise != null)
{
noise.NoiseProfile = area.NoiseProfile;
noise.AmplitudeGain = area.NoiseProfile != null ? area.NoiseAmplitude : 0f;
noise.FrequencyGain = area.NoiseFrequency;
}
}
private static void ResetVCamExtensions(CinemachineCamera vcam)
{
if (vcam == null) return;
vcam.GetComponent<CameraAsymmetricDampingExtension>()?.ResetState();
vcam.GetComponent<CameraFallBiasExtension>()?.ResetState();
vcam.GetComponent<CameraAdaptiveLookaheadExtension>()?.ResetState();
vcam.GetComponent<CameraFacingBiasExtension>()?.ResetState();
}
private void ApplyBlendProfile(CameraBlendProfileSO profile)
{
if (_brain != null && profile != null)
_brain.DefaultBlend = profile.ToBlendDefinition();
}
// ── 运行时调试覆盖层 ──────────────────────────────────────────────────
#if UNITY_EDITOR || DEVELOPMENT_BUILD
[Header("调试")]
[Tooltip("运行时在屏幕左上角显示当前相机区域信息。\n仅在 Editor 和 Development Build 中有效。")]
[SerializeField] private bool _showDebugOverlay = false;
private GUIStyle _debugBoxStyle;
private GUIStyle _debugTitleStyle;
private GUIStyle _debugRowStyle;
private GUIStyle _debugWarnStyle;
private void InitDebugStyles()
{
if (_debugBoxStyle != null) return;
var bg = new Texture2D(1, 1);
bg.SetPixel(0, 0, new Color(0f, 0f, 0f, 0.72f));
bg.Apply();
_debugBoxStyle = new GUIStyle(GUI.skin.box)
{
normal = { background = bg, textColor = Color.white },
padding = new RectOffset(10, 10, 8, 8),
alignment = TextAnchor.UpperLeft,
fontSize = 12,
};
_debugTitleStyle = new GUIStyle(GUI.skin.label)
{
normal = { textColor = new Color(1f, 0.85f, 0.25f) },
fontStyle = FontStyle.Bold,
fontSize = 13,
};
_debugRowStyle = new GUIStyle(GUI.skin.label)
{
normal = { textColor = new Color(0.88f, 0.88f, 0.88f) },
fontSize = 12,
};
_debugWarnStyle = new GUIStyle(GUI.skin.label)
{
normal = { textColor = new Color(1f, 0.45f, 0.35f) },
fontSize = 12,
};
}
private void OnGUI()
{
if (!Application.isPlaying || !_showDebugOverlay) return;
InitDebugStyles();
float x = 12f, y = 12f, w = 320f;
// 计算高度(先收集内容)
string areaName = _currentArea != null ? _currentArea.name : "<无>";
string dedicatedLabel = _activeDedicatedCam != null
? $"{_activeDedicatedCam.name} (P={_activeDedicatedCam.Priority})"
: "<无激活 VCam>";
string followLabel = _currentFollowTarget != null
? _currentFollowTarget.name
: "<未设置>";
bool warnFollow = _currentFollowTarget == null;
bool warnNoVCam = _activeDedicatedCam == null;
bool warnNoBrain = _brain == null;
// 区域状态(基线 + 触发区域集合)
var zoneLines = new System.Collections.Generic.List<string>();
string baselineName = _roomBaselineArea != null ? _roomBaselineArea.name : "<未设置>";
string baselineMarker = (_currentArea == _roomBaselineArea && _activeZones.Count == 0) ? " ◄ 激活" : "";
zoneLines.Add($" [基线] {baselineName}{baselineMarker}");
for (int i = _activeZones.Count - 1; i >= 0; i--)
{
var e = _activeZones[i];
string marker = (e.area == _currentArea) ? " ◄ 激活" : "";
zoneLines.Add($" [{e.priority}] {(e.area != null ? e.area.name : "null")}{marker}");
}
int lineCount = 4 + zoneLines.Count + (warnFollow ? 1 : 0) + (warnNoVCam ? 1 : 0) + (warnNoBrain ? 1 : 0);
float rowH = 19f;
float h = 28f + lineCount * rowH + 8f;
GUI.Box(new Rect(x, y, w, h), GUIContent.none, _debugBoxStyle);
float cy = y + 8f;
GUI.Label(new Rect(x + 8f, cy, w - 16f, 22f), "[ Camera State Controller ]", _debugTitleStyle);
cy += 22f;
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"当前区域:{areaName}", _debugRowStyle); cy += rowH;
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"专有 VCam{dedicatedLabel}", warnNoVCam ? _debugWarnStyle : _debugRowStyle); cy += rowH;
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"Follow 目标:{followLabel}", warnFollow ? _debugWarnStyle : _debugRowStyle); cy += rowH;
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "区域状态(基线 + 触发区域):", _debugRowStyle); cy += rowH;
foreach (var line in zoneLines)
{
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), line, _debugRowStyle);
cy += rowH;
}
if (warnFollow)
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ Follow 目标未设置(检查 _onPlayerSpawned", _debugWarnStyle); cy += rowH; }
if (warnNoVCam)
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ 当前区域无激活专有 VCam检查 DedicatedCamera 绑定)", _debugWarnStyle); cy += rowH; }
if (warnNoBrain)
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ CinemachineBrain 未绑定", _debugWarnStyle); cy += rowH; }
}
#endif
}
}