Files
zeling_v2/Assets/_Game/Scripts/Camera/CameraFallBiasExtension.cs
2026-05-19 11:50:21 +08:00

137 lines
6.7 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;
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;
}
}
}
}