using UnityEngine; namespace BaseGames.Camera { /// /// 双轴窥视系统(Look Up / Down / Left / Right)。 /// /// - 垂直窥视:玩家按住垂直方向超过 _holdDelayV 秒后, /// 相机在 Y 轴漂移最多 _lookDistanceV 个世界单位,松开后平滑回弹。 /// - 水平窥视:玩家静止时按住左/右超过 _holdDelayH 秒后, /// 相机在 X 轴漂移最多 _lookDistanceH 个单位。 /// - 速度门控:玩家速度超过 _speedGateThreshold 时窥视不再激活。 /// /// 挂载位置:Persistent 场景 [Camera] 节点下。 /// CameraStateController 持有此组件的引用(_lookSystem 字段), /// 在 SetFollowTarget 时自动注册玩家目标,VCam.Follow 指向 VirtualTarget。 /// /// 玩家输入接入:由 PlayerController 调用 SetLookInput(float, float)。 /// public class CameraLookSystem : MonoBehaviour { [Header("窥视参数 —— 垂直")] [Tooltip("持续按住垂直方向键多少秒后触发垂直窥视。0 = 立即触发。推荐 0.8。")] [SerializeField] private float _holdDelayV = 0.8f; [Tooltip("最大垂直偏移量(世界单位)。推荐 3~4 单位。")] [SerializeField] private float _lookDistanceV = 3.5f; [Tooltip("垂直偏移过渡速度(越大收敛越快)。")] [SerializeField] private float _lookSpeedV = 2.5f; [Tooltip("垂直回弹速度(建议稍快于 _lookSpeedV,避免回弹拖沓)。")] [SerializeField] private float _resetSpeedV = 5f; [Header("窥视参数 —— 水平")] [Tooltip("静止后持续按住水平方向键多少秒后触发水平窥视。推荐 0.5。")] [SerializeField] private float _holdDelayH = 0.5f; [Tooltip("最大水平偏移量(世界单位)。水平面比垂直面小,避免与 Lookahead 叠加过度。推荐 2.5。")] [SerializeField] private float _lookDistanceH = 2.5f; [Tooltip("水平偏移过渡速度。")] [SerializeField] private float _lookSpeedH = 2.0f; [Tooltip("水平回弹速度。")] [SerializeField] private float _resetSpeedH = 5f; [Header("速度门控")] [Tooltip("玩家移动速度超过此值时窥视系统不再新增偏移,已有偏移平滑回弹。推荐 2.5。")] [SerializeField] private float _speedGateThreshold = 2.5f; // ── 内部状态 ────────────────────────────────────────────────────────── private Transform _baseTarget; private Transform _virtualTargetTransform; private Vector3 _lastBasePosition; private float _estimatedSpeed; // 垂直窥视 private float _holdTimerV; private float _inputY; private float _currentOffsetY; private float _targetOffsetY; // 水平窥视 private float _holdTimerH; private float _inputX; private float _currentOffsetX; private float _targetOffsetX; /// /// VCam 应跟随此 Transform(玩家位置 + 窥视偏移)。 /// 由 赋值给 VCam.Follow。 /// public Transform VirtualTarget => _virtualTargetTransform; // ── Lifecycle ───────────────────────────────────────────────────────── private void Awake() { var go = new GameObject("[CameraLookTarget]") { hideFlags = HideFlags.HideInHierarchy }; DontDestroyOnLoad(go); _virtualTargetTransform = go.transform; } private void OnDestroy() { if (_virtualTargetTransform != null) Destroy(_virtualTargetTransform.gameObject); } // ── 公开 API ────────────────────────────────────────────────────────── /// /// 注册玩家的 CameraFollowTarget。通常由 调用。 /// public void SetBaseTarget(Transform target) { _baseTarget = target; _lastBasePosition = target != null ? target.position : Vector3.zero; } /// /// 传入归一化输入(x = 水平,y = 垂直)。 /// 由 PlayerController / InputReader 在 Update 中调用。 /// public void SetLookInput(float horizontal, float vertical) { _inputX = Mathf.Clamp(horizontal, -1f, 1f); _inputY = Mathf.Clamp(vertical, -1f, 1f); } /// 仅设置垂直窥视输入(向后兼容旧调用方式)。 public void SetLookInput(float vertical) => SetLookInput(0f, vertical); /// /// 重置窥视状态。房间切换(即时硬切)时调用, /// 避免旧房间的窥视偏移残留影响新房间的相机初始位置。 /// /// /// true = 立即将当前偏移归零(硬切时推荐); /// false = 仅清除目标偏移,让当前偏移通过正常 Update 平滑回弹。 /// public void ResetOffsets(bool snap = false) { _holdTimerV = 0f; _holdTimerH = 0f; _targetOffsetY = 0f; _targetOffsetX = 0f; if (snap) { _currentOffsetY = 0f; _currentOffsetX = 0f; if (_baseTarget != null && _virtualTargetTransform != null) _virtualTargetTransform.position = _baseTarget.position; } } // ── Update ──────────────────────────────────────────────────────────── private void LateUpdate() { if (_baseTarget == null || _virtualTargetTransform == null) return; // ── 速度估算(速度门控基准)──────────────────────────────────────── float dt = Time.deltaTime; if (dt > 0f) { float rawSpeed = (_baseTarget.position - _lastBasePosition).magnitude / dt; _estimatedSpeed = Mathf.Lerp(_estimatedSpeed, rawSpeed, 1f - Mathf.Exp(-dt * 8f)); } _lastBasePosition = _baseTarget.position; bool withinGate = _estimatedSpeed < _speedGateThreshold; // ── 垂直窥视 ────────────────────────────────────────────────────── if (withinGate && Mathf.Abs(_inputY) > 0.5f) { _holdTimerV += dt; if (_holdTimerV >= _holdDelayV) _targetOffsetY = _inputY * _lookDistanceV; } else { _holdTimerV = 0f; _targetOffsetY = 0f; } float speedV = Mathf.Abs(_targetOffsetY) < 0.01f ? _resetSpeedV : _lookSpeedV; _currentOffsetY = Mathf.Lerp(_currentOffsetY, _targetOffsetY, 1f - Mathf.Exp(-dt * speedV)); // ── 水平窥视 ────────────────────────────────────────────────────── if (withinGate && Mathf.Abs(_inputX) > 0.5f) { _holdTimerH += dt; if (_holdTimerH >= _holdDelayH) _targetOffsetX = _inputX * _lookDistanceH; } else { _holdTimerH = 0f; _targetOffsetX = 0f; } float speedH = Mathf.Abs(_targetOffsetX) < 0.01f ? _resetSpeedH : _lookSpeedH; _currentOffsetX = Mathf.Lerp(_currentOffsetX, _targetOffsetX, 1f - Mathf.Exp(-dt * speedH)); // ── 虚拟目标 = 玩家位置 + 双轴偏移 ──────────────────────────────── _virtualTargetTransform.position = _baseTarget.position + new Vector3(_currentOffsetX, _currentOffsetY, 0f); } } }