using UnityEngine; using Unity.Cinemachine; namespace BaseGames.Camera { /// /// 下坠视野偏置扩展。 /// /// 玩家持续高速下落超过 秒后, /// 将相机 Y 坐标平滑下移最多 世界单位, /// 使玩家出现在画面上方区域,提前暴露落点地形。 /// 玩家减速或着地后快速复位。 /// /// 挂载顺序(三者顺序必须严格遵守): /// /// — 先对 Y 轴做非对称阻尼平滑; /// 本扩展(CameraFallBiasExtension) — 将偏置叠加到平滑后的 Y 上; /// CinemachineConfiner3D — 最后将偏置后的位置裁剪回限位边界内。 /// /// 如果顺序错误(本扩展在 Confiner 之后),偏置会导致相机超出限位边界且不被修正。 /// [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 ────────────────────────────────────────────────────────────────── /// /// 由 调用: /// 传入 0 表示禁用此区域的下坠偏置;-1 表示使用检查器默认属性值。 /// public void SetConfiguredMax(float maxShift) => _configuredMaxShift = maxShift; /// /// 重置内部状态。在相机硬切(instantCut)时由 CameraStateController 调用, /// 避免旧房间的下坠计时 / 偷移量带入新房间。 /// 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; } } } }