609 lines
28 KiB
C#
609 lines
28 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using Unity.Cinemachine;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Events;
|
||
|
||
namespace BaseGames.Camera
|
||
{
|
||
/// <summary>
|
||
/// 相机状态单例控制器。须放置在 Persistent 场景中。
|
||
///
|
||
/// 支持两种相机切换模式:
|
||
/// 1. 全局双 VCam 模式(推荐):<see cref="SwitchArea"/>
|
||
/// 两台全局 CinemachineCamera(<c>_vcamA</c> / <c>_vcamB</c>)交替承接各区域,
|
||
/// 通过优先级 ping-pong 触发 Cinemachine 混合过渡。场景内无需每个区域都放置 VCam。
|
||
///
|
||
/// 2. 专有 VCam 模式:<see cref="SwitchArea"/>(区域含 dedicatedCamera 时自动使用)
|
||
/// 激活该区域专属的 CinemachineCamera(优先级 > 全局 VCam),
|
||
/// 适用于需要独特相机参数的特殊区域。
|
||
/// </summary>
|
||
[DefaultExecutionOrder(-100)]
|
||
public class CameraStateController : MonoBehaviour, ICameraService
|
||
{
|
||
[Header("引用")]
|
||
[SerializeField] private CinemachineBrain _brain;
|
||
[SerializeField] private CinemachineImpulseSource _impulseSource;
|
||
[SerializeField] private CameraLookSystem _lookSystem;
|
||
|
||
[Header("全局双 VCam(Persistent 场景中放置两台通用虚拟相机)")]
|
||
[Tooltip("两台 VCam 交替承接各相机区域,通过优先级 ping-pong 触发混合过渡。\n" +
|
||
"须各自挂载 CinemachineCamera + CinemachineConfiner2D;\n" +
|
||
"Follow 指向 Player/CameraFollowTarget(或运行时调用 SetFollowTarget 赋值)。")]
|
||
[SerializeField] private CinemachineCamera _vcamA;
|
||
[SerializeField] private CinemachineCamera _vcamB;
|
||
|
||
[Tooltip("全局 VCam 激活时的优先级。专有 VCam 的 _dedicatedPriority 须高于此值。")]
|
||
[SerializeField] private int _globalActivePriority = 10;
|
||
|
||
[Tooltip("待机 VCam 的优先级。\n" +
|
||
"Cinemachine 3.x 中 Priority = 0 的 VCam 不会被 Brain 选中,导致主相机停止跟随。\n" +
|
||
"必须 > 0 且 < _globalActivePriority,确保 Brain 始终有可用 VCam,\n" +
|
||
"同时切换时两台 VCam 均在 Brain 视野内以完成正确的混合过渡。")]
|
||
[SerializeField] private int _standbyPriority = 1;
|
||
|
||
[Header("默认混合配置")]
|
||
[SerializeField] private CameraBlendProfileSO _defaultBlendProfile;
|
||
|
||
[Header("镜头配置")]
|
||
[Tooltip("全局镜头参数 SO。Awake 时将 fieldOfView 应用到 _vcamA / _vcamB。\n" +
|
||
"与各 CameraArea 引用同一资产,确保 FOV 参数一致。")]
|
||
[SerializeField] private CameraLensConfigSO _lensConfig;
|
||
|
||
[Header("玩家跟随")]
|
||
[Tooltip("PlayerController 生成时广播的事件频道(EVT_PlayerSpawned)。\n" +
|
||
"收到后自动查找 CameraFollowTarget 子节点并赋值给两台全局 VCam 的 Follow。")]
|
||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||
|
||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||
private int _activeSlot = -1; // -1 = 未初始化;0 = A;1 = B
|
||
private CameraArea _roomBaselineArea; // SwitchArea(priority=0) 写入的房间基线,不被触发事件删除
|
||
private readonly List<(CameraArea area, int priority)> _activeZones = new(); // 玩家当前所在的触发区域集合(priority>0)
|
||
private CameraArea _currentArea;
|
||
private CinemachineCamera _activeDedicatedCam;
|
||
private CinemachineConfiner2D _confinerA;
|
||
private CinemachineConfiner2D _confinerB;
|
||
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);
|
||
|
||
// 缓存 Confiner 引用
|
||
if (_vcamA != null) _confinerA = _vcamA.GetComponent<CinemachineConfiner2D>();
|
||
if (_vcamB != null) _confinerB = _vcamB.GetComponent<CinemachineConfiner2D>();
|
||
|
||
// 初始两台 VCam 均处于待机优先级(> 0)
|
||
// Cinemachine 3.x 中 Priority = 0 的 VCam 不被 Brain 选中,主相机会停止运动
|
||
if (_vcamA != null) _vcamA.Priority = _standbyPriority;
|
||
if (_vcamB != null) _vcamB.Priority = _standbyPriority;
|
||
|
||
// 将 SO 中的 FOV 应用到两台全局 VCam
|
||
ApplyLensConfig();
|
||
|
||
// 订阅 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 ApplyLensConfig()
|
||
{
|
||
if (_lensConfig == null) return;
|
||
float fov = _lensConfig.fieldOfView;
|
||
float depth = _lensConfig.cameraDepth;
|
||
ApplyLensToVcam(_vcamA, fov, depth);
|
||
ApplyLensToVcam(_vcamB, fov, depth);
|
||
}
|
||
|
||
private static void ApplyLensToVcam(CinemachineCamera vcam, float fov, float depth)
|
||
{
|
||
if (vcam == null) return;
|
||
var lens = vcam.Lens;
|
||
lens.FieldOfView = fov;
|
||
vcam.Lens = lens;
|
||
// CinemachinePositionComposer.CameraDistance 是运行时真正控制 Z 距离的属性,
|
||
// 必须同步,否则 Transform Z 被 Cinemachine Pipeline 覆盖
|
||
var composer = vcam.GetComponent<CinemachinePositionComposer>();
|
||
if (composer != null)
|
||
composer.CameraDistance = depth;
|
||
// 同步 Transform Z,保证编辑器预览与运行时一致
|
||
var pos = vcam.transform.position;
|
||
pos.z = -depth;
|
||
vcam.transform.position = pos;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
private void OnValidate() => ApplyLensConfig();
|
||
#endif
|
||
|
||
// ── 公开 API ──────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 切换到目标相机区域。<paramref name="priority"/> < 当前激活优先级时忽略。
|
||
/// <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));
|
||
|
||
// 仅当此区域是当前最优且尚未激活时才切换,避免不必要的 ping-pong
|
||
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) 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 扩展的内部状态,防止旧房间的速度/阻尼估算带入新房间
|
||
ResetVCamExtensions(_vcamA);
|
||
ResetVCamExtensions(_vcamB);
|
||
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
|
||
ActivateGlobalSlot(area);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运行时为全局双 VCam 设置跟随目标。
|
||
/// 若存在 <see cref="CameraLookSystem"/>,VCam 跟随系统输出的虚拟目标(含窥视偏移)。
|
||
/// </summary>
|
||
public void SetFollowTarget(Transform followTarget)
|
||
{
|
||
Transform actual = followTarget;
|
||
if (_lookSystem != null)
|
||
{
|
||
_lookSystem.SetBaseTarget(followTarget);
|
||
actual = _lookSystem.VirtualTarget;
|
||
}
|
||
_currentFollowTarget = actual; // 缓存供后续激活 VCam 时同步
|
||
if (_vcamA != null) _vcamA.Follow = actual;
|
||
if (_vcamB != null) _vcamB.Follow = actual;
|
||
}
|
||
|
||
/// <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 时瞬间切换。
|
||
/// 区域进入时由 <see cref="CameraArea"/> 自动调用;游戏代码也可直接调用。
|
||
/// </summary>
|
||
public void SetLensSize(float orthographicSize, float duration = 0f)
|
||
{
|
||
if (_lensCoroutine != null) StopCoroutine(_lensCoroutine);
|
||
if (duration <= 0f) { ApplyLensSizeToAll(orthographicSize); return; }
|
||
_lensCoroutine = StartCoroutine(LensSizeCo(orthographicSize, duration));
|
||
}
|
||
|
||
private Coroutine _lensCoroutine;
|
||
|
||
private void ApplyLensSizeToAll(float size)
|
||
{
|
||
SetVcamLens(_vcamA, size);
|
||
SetVcamLens(_vcamB, size);
|
||
if (_activeDedicatedCam != null) SetVcamLens(_activeDedicatedCam, size);
|
||
}
|
||
|
||
private static void SetVcamLens(CinemachineCamera vcam, float size)
|
||
{
|
||
if (vcam == null) return;
|
||
var lens = vcam.Lens;
|
||
lens.OrthographicSize = size;
|
||
vcam.Lens = lens;
|
||
}
|
||
|
||
private IEnumerator LensSizeCo(float target, float duration)
|
||
{
|
||
CinemachineCamera active = GetActiveVcam();
|
||
if (active == null) { _lensCoroutine = null; yield break; }
|
||
float start = active.Lens.OrthographicSize;
|
||
float elapsed = 0f;
|
||
while (elapsed < duration)
|
||
{
|
||
elapsed += Time.deltaTime;
|
||
ApplyLensSizeToAll(Mathf.Lerp(start, target, elapsed / duration));
|
||
yield return null;
|
||
}
|
||
ApplyLensSizeToAll(target);
|
||
_lensCoroutine = null;
|
||
}
|
||
|
||
private CinemachineCamera GetActiveVcam()
|
||
{
|
||
if (_activeDedicatedCam != null) return _activeDedicatedCam;
|
||
return _activeSlot == 0 ? _vcamA : (_vcamB != null ? _vcamB : _vcamA);
|
||
}
|
||
|
||
// ── 内部方法 ──────────────────────────────────────────────────────────
|
||
|
||
/// <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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用全局 VCam ping-pong 切换到新区域。
|
||
/// 配置非活跃 VCam 的 Confiner → 提升其优先级 → 降低旧 VCam 优先级。
|
||
/// Cinemachine Brain 检测到优先级变化后自动触发混合。
|
||
/// </summary>
|
||
private void ActivateGlobalSlot(CameraArea area)
|
||
{
|
||
// 收回专有 VCam
|
||
if (_activeDedicatedCam != null)
|
||
{
|
||
_activeDedicatedCam.Priority = 0;
|
||
_activeDedicatedCam = null;
|
||
}
|
||
|
||
bool noVCams = _vcamA == null && _vcamB == null;
|
||
if (noVCams)
|
||
{
|
||
Debug.LogWarning("[CameraStateController] 全局 VCam A / B 均未绑定,无法切换相机区域。");
|
||
return;
|
||
}
|
||
|
||
// 首次调用:直接激活 VCamA(场景淡入阶段,无需混合动画)
|
||
if (_activeSlot < 0)
|
||
{
|
||
var cam = _vcamA ?? _vcamB;
|
||
var confiner = _vcamA != null ? _confinerA : _confinerB;
|
||
ConfigureSlot(cam, confiner, area);
|
||
SyncFollowToVCam(cam);
|
||
cam.Priority = _globalActivePriority;
|
||
_activeSlot = _vcamA != null ? 0 : 1;
|
||
return;
|
||
}
|
||
|
||
// 只有一台 VCam 时:直接重新配置,不做优先级 ping-pong
|
||
// (之前的 null 保护令 inactiveCam == activeCam,导致先升后降为 0 自毁)
|
||
if (_vcamA == null || _vcamB == null)
|
||
{
|
||
var cam = _vcamA ?? _vcamB;
|
||
var confiner = _vcamA != null ? _confinerA : _confinerB;
|
||
ConfigureSlot(cam, confiner, area);
|
||
SyncFollowToVCam(cam);
|
||
cam.Priority = _globalActivePriority; // 保持激活,不改变 _activeSlot
|
||
return;
|
||
}
|
||
|
||
// 双 VCam ping-pong:配置非活跃槽 → 升级其优先级 → 降低活跃槽优先级
|
||
bool nextIsA = _activeSlot != 0;
|
||
var inactiveCam = nextIsA ? _vcamA : _vcamB;
|
||
var activeCam = nextIsA ? _vcamB : _vcamA;
|
||
var inactiveConfiner = nextIsA ? _confinerA : _confinerB;
|
||
|
||
ConfigureSlot(inactiveCam, inactiveConfiner, area);
|
||
SyncFollowToVCam(inactiveCam); // 确保 Follow 正确(防止 SetFollowTarget 未被调用)
|
||
inactiveCam.Priority = _globalActivePriority;
|
||
activeCam.Priority = _standbyPriority; // 降到待机但仍 > 0,Brain 可在混合期间读取其状态
|
||
_activeSlot = nextIsA ? 0 : 1;
|
||
}
|
||
|
||
/// <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, CinemachineConfiner2D confiner, CameraArea area)
|
||
{
|
||
// 1. Confiner
|
||
if (confiner != null && area.ConfinerCollider != null)
|
||
{
|
||
confiner.BoundingShape2D = area.ConfinerCollider;
|
||
// 限位多边形已在编辑器中预收缩(可视区域 - 视口半尺寸 = 相机中心运动范围)。
|
||
// OversizeWindow.MaxWindowSize = 0.001f(极小正值):
|
||
// 使 Cinemachine 将实际视口高度裁剪至 0.001,几乎不再对多边形额外收缩,
|
||
// 从而以预收缩后的多边形直接作为相机中心约束边界。
|
||
// 对于小于视口的房间(预收缩后多边形退化为点),仍正确固定相机于中心。
|
||
confiner.OversizeWindow = new CinemachineConfiner2D.OversizeWindowSettings
|
||
{
|
||
Enabled = true,
|
||
MaxWindowSize = 0.001f,
|
||
Padding = 0.1f,
|
||
};
|
||
// BoundingShape2D 变更后必须刷新内部缓存路径,否则限位仍使用旧边界
|
||
confiner.InvalidateLensCache();
|
||
}
|
||
else if (confiner != null && area.ConfinerCollider == null)
|
||
{
|
||
Debug.LogError(
|
||
$"[CameraStateController] {area.name} 未绑定 ConfinerCollider!" +
|
||
"请将子节点 AreaBoundary 的 PolygonCollider2D 拖入 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);
|
||
}
|
||
|
||
private static void ResetVCamExtensions(CinemachineCamera vcam)
|
||
{
|
||
if (vcam == null) return;
|
||
vcam.GetComponent<CameraAsymmetricDampingExtension>()?.ResetState();
|
||
vcam.GetComponent<CameraFallBiasExtension>()?.ResetState();
|
||
vcam.GetComponent<CameraAdaptiveLookaheadExtension>()?.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 slotLabel = _activeSlot < 0 ? "未初始化"
|
||
: _activeSlot == 0 ? "VCam A"
|
||
: "VCam B";
|
||
string followLabel = _currentFollowTarget != null
|
||
? _currentFollowTarget.name
|
||
: "<未设置>";
|
||
|
||
bool warnFollow = _currentFollowTarget == null;
|
||
bool warnNoVCam = _vcamA == null && _vcamB == 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 = 5 + 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 槽:{slotLabel}", _debugRowStyle); cy += rowH;
|
||
|
||
string vcamALabel = _vcamA != null ? $"{_vcamA.name} (P={_vcamA.Priority})" : "<未绑定>";
|
||
string vcamBLabel = _vcamB != null ? $"{_vcamB.name} (P={_vcamB.Priority})" : "<未绑定>";
|
||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"VCam A:{vcamALabel}", _debugRowStyle); cy += rowH;
|
||
GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), $"VCam B:{vcamBLabel}", _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 A/B 均未绑定", _debugWarnStyle); cy += rowH; }
|
||
if (warnNoBrain)
|
||
{ GUI.Label(new Rect(x + 8f, cy, w - 16f, rowH), "⚠ CinemachineBrain 未绑定", _debugWarnStyle); cy += rowH; }
|
||
}
|
||
|
||
#endif
|
||
}
|
||
}
|