- Implemented WeaponFeedback class for handling weapon-related feedbacks such as hit effects and attack sounds. - Added meta file for AddressableManagerWindow to manage addressable assets. - Included a new jump.data file for profiler data.
657 lines
31 KiB
C#
657 lines
31 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 场景中。
|
||
///
|
||
/// 每个 <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"/> < 当前激活优先级时忽略。
|
||
/// <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;
|
||
}
|
||
|
||
// 触发区域进入:更新集合(同一区域去重后重新加入,保证最新优先级)
|
||
for (int i = _activeZones.Count - 1; i >= 0; i--)
|
||
if (_activeZones[i].area == area) _activeZones.RemoveAt(i);
|
||
_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 = 0;
|
||
for (int i = _activeZones.Count - 1; i >= 0; i--)
|
||
if (_activeZones[i].area == releasedArea) { _activeZones.RemoveAt(i); removed++; }
|
||
// 若区域本就不在栈中,且又不是当前激活区,则无需任何操作
|
||
if (removed == 0 && !wasActive) return;
|
||
|
||
if (!wasActive) return;
|
||
|
||
// 回退到当前最优区域(触发集合 → 房间基线 → fallback)
|
||
CameraArea next = GetEffectiveArea() ?? fallback;
|
||
if (next != null && next != _currentArea)
|
||
ActivateArea(next, instantCut: false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回当前应激活的区域:
|
||
/// <list type="bullet">
|
||
/// <item><see cref="_activeZones"/> 不为空时,返回其中优先级最高的(同优先级取最近进入的)。</item>
|
||
/// <item>触发集合为空时,返回距玩家最近的 <see cref="CameraArea"/>;
|
||
/// 无法确定玩家位置时回退到 <see cref="_roomBaselineArea"/>。</item>
|
||
/// </list>
|
||
/// </summary>
|
||
private CameraArea GetEffectiveArea()
|
||
{
|
||
CameraArea best = null;
|
||
int bestPriority = -1;
|
||
foreach (var (a, p) in _activeZones)
|
||
if (p >= bestPriority) { bestPriority = p; best = a; }
|
||
if (best != null) return best;
|
||
|
||
// 触发集合为空:动态寻找距玩家最近的区域
|
||
if (_currentFollowTarget != null)
|
||
{
|
||
CameraArea nearest = CameraArea.FindNearest(_currentFollowTarget.position);
|
||
if (nearest != null) return nearest;
|
||
}
|
||
|
||
return _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
|
||
}
|
||
}
|