137 lines
6.7 KiB
C#
137 lines
6.7 KiB
C#
using UnityEngine;
|
||
using Unity.Cinemachine;
|
||
|
||
namespace BaseGames.Camera
|
||
{
|
||
/// <summary>
|
||
/// 下坠视野偏置扩展。
|
||
///
|
||
/// 玩家持续高速下落超过 <see cref="_activationDelay"/> 秒后,
|
||
/// 将相机 Y 坐标平滑下移最多 <see cref="_maxShift"/> 世界单位,
|
||
/// 使玩家出现在画面上方区域,提前暴露落点地形。
|
||
/// 玩家减速或着地后快速复位。
|
||
///
|
||
/// <para>挂载顺序(三者顺序必须严格遵守):</para>
|
||
/// <list type="number">
|
||
/// <item><see cref="CameraAsymmetricDampingExtension"/> — 先对 Y 轴做非对称阻尼平滑;</item>
|
||
/// <item><b>本扩展(CameraFallBiasExtension)</b> — 将偏置叠加到平滑后的 Y 上;</item>
|
||
/// <item><c>CinemachineConfiner3D</c> — 最后将偏置后的位置裁剪回限位边界内。</item>
|
||
/// </list>
|
||
/// <para>如果顺序错误(本扩展在 Confiner 之后),偏置会导致相机超出限位边界且不被修正。</para>
|
||
/// </summary>
|
||
[AddComponentMenu("Cinemachine/Extensions/Camera Fall Bias")]
|
||
[DisallowMultipleComponent]
|
||
public class CameraFallBiasExtension : CinemachineExtension
|
||
{
|
||
[Tooltip("触发下坠偏置所需的最小下落速度(世界单位/秒,绝对值)。\n" +
|
||
"低于此值不激活(短跳不触发)。推荐 6~10。")]
|
||
[Range(1f, 20f)]
|
||
[SerializeField] private float _fallSpeedThreshold = 7f;
|
||
|
||
[Tooltip("持续下落超过此时长后开始偏置(秒)。\n" +
|
||
"避免短暂跳跃/下落触发偏移。推荐 0.25~0.40。")]
|
||
[Range(0f, 1f)]
|
||
[SerializeField] private float _activationDelay = 0.30f;
|
||
|
||
[Tooltip("最大相机 Y 偏移量(世界单位,相机向下移动 = 玩家在画面上方 = 视野暴露下方地形)。\n" +
|
||
"推荐 1.5~2.5 单位。")]
|
||
[Range(0f, 5f)]
|
||
[SerializeField] private float _maxShift = 2f;
|
||
|
||
[Tooltip("偏置增加速度(Lerp 系数)。越大偏移越快达到最大值。推荐 3~5。")]
|
||
[SerializeField] private float _shiftSpeed = 3f;
|
||
|
||
[Tooltip("偏置复位速度。着陆后应快速恢复以避免画面跳变。推荐 8~12。")]
|
||
[SerializeField] private float _resetSpeed = 10f;
|
||
|
||
// ── 内部状态 ──────────────────────────────────────────────────────────
|
||
private float _configuredMaxShift = -1f; // -1 = 不覆盖,使用检查器默认属性值
|
||
private float _lastFollowY;
|
||
private float _smoothedVY;
|
||
private bool _initialized;
|
||
private float _fallTimer;
|
||
private float _currentShift;
|
||
// ── 公开 API ──────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 由 <see cref="CameraStateController.ConfigureSlot"/> 调用:
|
||
/// 传入 0 表示禁用此区域的下坠偏置;-1 表示使用检查器默认属性值。
|
||
/// </summary>
|
||
public void SetConfiguredMax(float maxShift) => _configuredMaxShift = maxShift;
|
||
|
||
/// <summary>
|
||
/// 重置内部状态。在相机硬切(instantCut)时由 CameraStateController 调用,
|
||
/// 避免旧房间的下坠计时 / 偷移量带入新房间。
|
||
/// </summary>
|
||
public void ResetState()
|
||
{
|
||
_initialized = false;
|
||
_fallTimer = 0f;
|
||
_currentShift = 0f;
|
||
_smoothedVY = 0f;
|
||
}
|
||
// ── Extension ─────────────────────────────────────────────────────────
|
||
|
||
protected override void PostPipelineStageCallback(
|
||
CinemachineVirtualCameraBase vcam,
|
||
CinemachineCore.Stage stage,
|
||
ref CameraState state,
|
||
float deltaTime)
|
||
{
|
||
if (stage != CinemachineCore.Stage.Body) return;
|
||
|
||
if (deltaTime <= 0f)
|
||
{
|
||
_initialized = false;
|
||
_fallTimer = 0f;
|
||
_currentShift = 0f;
|
||
return;
|
||
}
|
||
|
||
Transform follow = vcam.Follow;
|
||
if (follow == null) return;
|
||
|
||
if (!_initialized)
|
||
{
|
||
_lastFollowY = follow.position.y;
|
||
_initialized = true;
|
||
return;
|
||
}
|
||
|
||
// ── 估算玩家 Y 轴速度(帧率无关指数平滑)─────────────────────
|
||
float rawVY = (follow.position.y - _lastFollowY) / deltaTime;
|
||
_lastFollowY = follow.position.y;
|
||
_smoothedVY = Mathf.Lerp(_smoothedVY, rawVY, 1f - Mathf.Exp(-deltaTime * 10f));
|
||
|
||
bool isFalling = _smoothedVY < -_fallSpeedThreshold;
|
||
|
||
// ── 下落计时器 ────────────────────────────────────────────────
|
||
if (isFalling)
|
||
_fallTimer = Mathf.Min(_fallTimer + deltaTime, _activationDelay + 1f);
|
||
else
|
||
_fallTimer = Mathf.Max(_fallTimer - deltaTime * 3f, 0f); // 快速衰减
|
||
|
||
// ── 目标偏置 ──────────────────────────────────────────────────
|
||
// 超过 activationDelay 后线性增加偏置;0.4s 达到最大
|
||
float effectiveMax = _configuredMaxShift >= 0f ? _configuredMaxShift : _maxShift;
|
||
float ratio = Mathf.Clamp01((_fallTimer - _activationDelay) / 0.4f);
|
||
float targetShift = -effectiveMax * ratio; // 负值:相机向下
|
||
|
||
// 使用指数衰减公式(帧率无关)替代 Lerp*deltaTime
|
||
float dampingTime = targetShift < _currentShift
|
||
? 1f / Mathf.Max(_shiftSpeed, 0.001f)
|
||
: 1f / Mathf.Max(_resetSpeed, 0.001f);
|
||
float t = 1f - Mathf.Exp(-deltaTime / dampingTime);
|
||
_currentShift = Mathf.Lerp(_currentShift, targetShift, t);
|
||
|
||
// ── 写入相机 Y 偏置 ───────────────────────────────────────────
|
||
if (Mathf.Abs(_currentShift) > 0.001f)
|
||
{
|
||
var pos = state.RawPosition;
|
||
pos.y += _currentShift;
|
||
state.RawPosition = pos;
|
||
}
|
||
}
|
||
}
|
||
}
|