using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Cinemachine; using BaseGames.Core; using BaseGames.Core.Events; namespace BaseGames.Camera { /// /// 相机状态单例控制器。须放置在 Persistent 场景中。 /// /// 每个 均拥有自己专属的 DedicatedCamera, /// 进入区域时调用 激活对应 VCam, /// Cinemachine Brain 自动处理混合过渡。 /// [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() != null) { Destroy(gameObject); return; } ServiceLocator.Register(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(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(FindObjectsSortMode.None); foreach (var vcam in allVCams) { var confiner = vcam.GetComponent(); if (confiner != null) ValidateVCamExtensionOrder(vcam, confiner); } } // ── 公开 API ────────────────────────────────────────────────────────── /// /// 切换到目标相机区域。 < 当前激活优先级时忽略。 /// priority = 0:始终执行(适合 RoomController 入场初始化)。 /// 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); } /// /// 释放 的权限。 /// 从优先级栈中移除该区域;若它是当前激活区域,则激活新栈顶(或 fallback)。 /// 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); } /// /// 返回当前应激活的区域: 中优先级最高的 /// (同优先级取最近进入的),若触发集合为空则回退到 。 /// 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。"); } /// /// 运行时设置跟随目标。 /// 若存在 ,VCam 跟随系统输出的虚拟目标(含窗斥偏移)。 /// public void SetFollowTarget(Transform followTarget) { Transform actual = followTarget; if (_lookSystem != null) { _lookSystem.SetBaseTarget(followTarget); actual = _lookSystem.VirtualTarget; } _currentFollowTarget = actual; SyncFollowToVCam(_activeDedicatedCam); // 立即同步到当前活跃专有 VCam } /// 触发屏幕抖动。 public void TriggerImpulse(Vector3 velocity) { if (_impulseSource != null) _impulseSource.GenerateImpulse(velocity); } /// 以默认强度触发屏幕抖动。 public void TriggerImpulse(float strength = 0.3f) => TriggerImpulse(Vector3.down * strength); /// /// 平滑过渡视野尺寸(可视半高,世界单位)。 = 0 时瞬间切换。 /// 透视相机下自动换算为 FOV;语义等价于正交相机 OrthographicSize。 /// 区域进入时由 自动调用;游戏代码也可直接调用。 /// public void SetLensSize(float visibleHalfHeight, float duration = 0f) { if (_lensCoroutine != null) StopCoroutine(_lensCoroutine); if (duration <= 0f) { ApplyLensSizeToAll(visibleHalfHeight); return; } _lensCoroutine = StartCoroutine(LensSizeCo(visibleHalfHeight, duration)); } /// /// 进入过场模式,将指定 VCam 提升至最高优先级,Brain 自动混合到它。 /// /// 过场 VCam 由设计者在 Inspector 中预先配置(位置、Follow、LookAt、Lens 等); /// 此方法不强制覆写 Follow,保留 Inspector 配置不变。 /// /// 适用场景:Boss 登场固定镜头、对话拉近、剧情事件全景等。 /// public void EnterCutsceneMode(CinemachineCamera cutsceneCamera) { if (cutsceneCamera == null) return; if (_cutsceneCamera != null && _cutsceneCamera != cutsceneCamera) _cutsceneCamera.Priority = 0; _cutsceneCamera = cutsceneCamera; _cutsceneCamera.Priority = CutscenePriority; } /// /// 退出过场模式,撤销过场 VCam 的优先级,Brain 自动混合回当前区域相机。 /// public void ExitCutsceneMode() { if (_cutsceneCamera == null) return; _cutsceneCamera.Priority = 0; _cutsceneCamera = null; } /// /// 通知相机系统玩家的面朝方向,转发至所有活跃 VCam 的方向偏置扩展。 /// 由 PlayerController 在精灵翻转时调用:ICameraService.SetPlayerFacing(翻转方向 ? +1 : -1)。 /// public void SetPlayerFacing(int direction) { _lastExternalFacing = direction; SetFacingOnVCam(_activeDedicatedCam, direction); } private static void SetFacingOnVCam(CinemachineCamera vcam, int direction) => vcam?.GetComponent()?.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; // ── 内部方法 ────────────────────────────────────────────────────────── /// 激活区域的专有 VCam(高优先级)。 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(); ValidateVCamExtensionOrder(_activeDedicatedCam, dedicatedConfiner); ConfigureSlot(_activeDedicatedCam, dedicatedConfiner, area); } /// /// 检查 VCam 上各扩展组件的挂载顺序是否正确。 /// /// FallBiasExtension / FacingBiasExtension 必须在 CinemachineConfiner3D 之前 /// AxisLockExtension 必须在 CinemachineConfiner3D 之后 /// /// 顺序错误时相机会在应用偏置后逃出限位区域,或轴锁被 Confiner 覆盖失效。 /// private static void ValidateVCamExtensionOrder(CinemachineCamera vcam, CinemachineConfiner3D confiner) { if (vcam == null) return; if (confiner == null) { Debug.LogWarning( $"[CameraStateController] VCam {vcam.name} 缺少 CinemachineConfiner3D 组件!" + "相机将不受任何限位约束,请通过 CameraAreaEditor 重新生成此 VCam 或手动添加。"); return; } Component[] comps = vcam.GetComponents(); 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 {vcam.name}:" + "CameraAsymmetricDampingExtension 必须在 CinemachineConfiner3D 之前!" + "当前顺序导致Y轴阻尼平滑值将相机推出限位区域而不被重新裁剪。" + "请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。"); if (fallBiasIdx >= 0 && fallBiasIdx > confinerIdx) Debug.LogError( $"[CameraStateController] VCam {vcam.name}:" + "CameraFallBiasExtension 必须在 CinemachineConfiner3D 之前!" + "当前顺序导致下坠偏置将相机推出限位区域而不被重新裁剪。" + "请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。"); if (facingBiasIdx >= 0 && facingBiasIdx > confinerIdx) Debug.LogError( $"[CameraStateController] VCam {vcam.name}:" + "CameraFacingBiasExtension 必须在 CinemachineConfiner3D 之前!" + "当前顺序导致朝向偏置将相机推出限位区域而不被重新裁剪。" + "请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。"); if (adaptiveLahIdx >= 0 && adaptiveLahIdx > confinerIdx) Debug.LogError( $"[CameraStateController] VCam {vcam.name}:" + "CameraAdaptiveLookaheadExtension 必须在 CinemachineConfiner3D 之前!" + "请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。"); if (axisLockIdx >= 0 && axisLockIdx < confinerIdx) Debug.LogError( $"[CameraStateController] VCam {vcam.name}:" + "CameraAxisLockExtension 必须在 CinemachineConfiner3D 之后!" + "当前顺序导致轴向锁定被 Confiner 覆盖而失效。" + "请在 RoomCameraSetupTool 中点击「自动修正组件顺序」按钮。"); } /// /// 将最后已知的 Follow 目标同步到指定 VCam(若其 Follow 尚未设置或已过期)。 /// 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(); 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(); 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(); if (adaptiveLah != null) adaptiveLah.SetConfiguredMax(area.LookaheadTime); } } // 3. 轴向约束 var axisLock = vcam.GetComponent(); 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(); if (fallBias != null) fallBias.SetConfiguredMax(area.DisableFallBias ? 0f : -1f); // 5. 方向感知水平偏置 // X 轴已锁定时强制关闭偏置:两者均在 Body Stage 执行,若偏置后于锁定运行会破坏轴锁。 var facingBias = vcam.GetComponent(); if (facingBias != null) { if (area.LockHorizontal) facingBias.FacingBias = 0f; else if (area.OverrideFacingBias) facingBias.FacingBias = area.FacingBiasOverride; } // 6. 相机噪音(区域氛围震动:洞穴、水下、机械等) var noise = vcam.GetComponent(); 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()?.ResetState(); vcam.GetComponent()?.ResetState(); vcam.GetComponent()?.ResetState(); vcam.GetComponent()?.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 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 } }