196 lines
8.6 KiB
C#
196 lines
8.6 KiB
C#
using UnityEngine;
|
||
|
||
namespace BaseGames.Camera
|
||
{
|
||
/// <summary>
|
||
/// 双轴窥视系统(Look Up / Down / Left / Right)。
|
||
///
|
||
/// - <b>垂直窥视</b>:玩家按住垂直方向超过 _holdDelayV 秒后,
|
||
/// 相机在 Y 轴漂移最多 _lookDistanceV 个世界单位,松开后平滑回弹。
|
||
/// - <b>水平窥视</b>:玩家静止时按住左/右超过 _holdDelayH 秒后,
|
||
/// 相机在 X 轴漂移最多 _lookDistanceH 个单位。
|
||
/// - <b>速度门控</b>:玩家速度超过 _speedGateThreshold 时窥视不再激活。
|
||
///
|
||
/// 挂载位置:Persistent 场景 [Camera] 节点下。
|
||
/// CameraStateController 持有此组件的引用(_lookSystem 字段),
|
||
/// 在 SetFollowTarget 时自动注册玩家目标,VCam.Follow 指向 VirtualTarget。
|
||
///
|
||
/// 玩家输入接入:由 PlayerController 调用 SetLookInput(float, float)。
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// VCam 应跟随此 Transform(玩家位置 + 窥视偏移)。
|
||
/// 由 <see cref="CameraStateController"/> 赋值给 VCam.Follow。
|
||
/// </summary>
|
||
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 ──────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 注册玩家的 CameraFollowTarget。通常由 <see cref="CameraStateController.SetFollowTarget"/> 调用。
|
||
/// </summary>
|
||
public void SetBaseTarget(Transform target)
|
||
{
|
||
_baseTarget = target;
|
||
_lastBasePosition = target != null ? target.position : Vector3.zero;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 传入归一化输入(x = 水平,y = 垂直)。
|
||
/// 由 PlayerController / InputReader 在 Update 中调用。
|
||
/// </summary>
|
||
public void SetLookInput(float horizontal, float vertical)
|
||
{
|
||
_inputX = Mathf.Clamp(horizontal, -1f, 1f);
|
||
_inputY = Mathf.Clamp(vertical, -1f, 1f);
|
||
}
|
||
|
||
/// <summary>仅设置垂直窥视输入(向后兼容旧调用方式)。</summary>
|
||
public void SetLookInput(float vertical) => SetLookInput(0f, vertical);
|
||
|
||
/// <summary>
|
||
/// 重置窥视状态。房间切换(即时硬切)时调用,
|
||
/// 避免旧房间的窥视偏移残留影响新房间的相机初始位置。
|
||
/// </summary>
|
||
/// <param name="snap">
|
||
/// true = 立即将当前偏移归零(硬切时推荐);
|
||
/// false = 仅清除目标偏移,让当前偏移通过正常 Update 平滑回弹。
|
||
/// </param>
|
||
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, 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, 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, dt * speedH);
|
||
|
||
// ── 虚拟目标 = 玩家位置 + 双轴偏移 ────────────────────────────────
|
||
_virtualTargetTransform.position = _baseTarget.position
|
||
+ new Vector3(_currentOffsetX, _currentOffsetY, 0f);
|
||
}
|
||
}
|
||
} |