Files
zeling_v2/Assets/_Game/Scripts/Camera/CameraStateController.cs

181 lines
8.1 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 UnityEngine;
using Unity.Cinemachine;
using BaseGames.Core;
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;
[Header("全局双 VCamPersistent 场景中放置两台通用虚拟相机)")]
[Tooltip("两台 VCam 交替承接各相机区域,通过优先级 ping-pong 触发混合过渡。\n" +
"须各自挂载 CinemachineCamera + CinemachineConfiner2D\n" +
"Follow 指向 Player/CameraFollowTarget或运行时调用 SetFollowTarget 赋值)。")]
[SerializeField] private CinemachineCamera _vcamA;
[SerializeField] private CinemachineCamera _vcamB;
[Tooltip("全局 VCam 激活时的优先级(非活跃时为 0。专有 VCam 的 _dedicatedPriority 须高于此值。")]
[SerializeField] private int _globalActivePriority = 10;
[Header("默认混合配置")]
[SerializeField] private CameraBlendProfileSO _defaultBlendProfile;
// ── 状态 ──────────────────────────────────────────────────────────────
private int _activeSlot = -1; // -1 = 未初始化0 = A1 = B
private CinemachineCamera _activeDedicatedCam;
private CinemachineConfiner2D _confinerA;
private CinemachineConfiner2D _confinerB;
// ── 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 均处于非活跃优先级
if (_vcamA != null) _vcamA.Priority = 0;
if (_vcamB != null) _vcamB.Priority = 0;
}
private void OnDestroy()
{
ServiceLocator.Unregister<ICameraService>(this);
}
// ── 公开 API ──────────────────────────────────────────────────────────
/// <summary>
/// 切换到目标相机区域。
/// <list type="bullet">
/// <item>区域有专有 VCam → 激活它(高优先级),全局 VCam 保持当前状态。</item>
/// <item>区域无专有 VCam → 配置非活跃全局 VCamping-pong 切换优先级触发混合。</item>
/// </list>
/// </summary>
public void SwitchArea(CameraArea targetArea)
{
if (targetArea == null) return;
ApplyBlendProfile(targetArea.BlendProfile ?? _defaultBlendProfile);
if (targetArea.HasDedicated)
ActivateDedicated(targetArea);
else
ActivateGlobalSlot(targetArea);
}
/// <summary>
/// 运行时为两台全局 VCam 统一设置跟随目标(如 Player/CameraFollowTarget
/// 可在 Player 生成后由任意系统调用。
/// </summary>
public void SetFollowTarget(Transform followTarget)
{
if (_vcamA != null) _vcamA.Follow = followTarget;
if (_vcamB != null) _vcamB.Follow = followTarget;
}
/// <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>激活区域的专有 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);
cam.Priority = _globalActivePriority;
_activeSlot = _vcamA != null ? 0 : 1;
return;
}
// Ping-pong配置非活跃槽 → 升级其优先级 → 降低活跃槽优先级
bool nextIsA = _activeSlot != 0;
var inactiveCam = nextIsA ? _vcamA : _vcamB;
var activeCam = nextIsA ? _vcamB : _vcamA;
var inactiveConfiner = nextIsA ? _confinerA : _confinerB;
// 只有一台 VCam 时降级处理(仍能工作,但无混合动画)
if (inactiveCam == null) inactiveCam = activeCam;
ConfigureSlot(inactiveCam, inactiveConfiner, area);
inactiveCam.Priority = _globalActivePriority;
activeCam.Priority = 0;
_activeSlot = nextIsA ? 0 : 1;
}
private static void ConfigureSlot(
CinemachineCamera vcam, CinemachineConfiner2D confiner, CameraArea area)
{
if (confiner != null && area.ConfinerCollider != null)
confiner.BoundingShape2D = area.ConfinerCollider;
}
private void ApplyBlendProfile(CameraBlendProfileSO profile)
{
if (_brain != null && profile != null)
_brain.DefaultBlend = profile.ToBlendDefinition();
}
}
}