181 lines
8.1 KiB
C#
181 lines
8.1 KiB
C#
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("全局双 VCam(Persistent 场景中放置两台通用虚拟相机)")]
|
||
[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 = A;1 = 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 → 配置非活跃全局 VCam,ping-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();
|
||
}
|
||
}
|
||
}
|