Files
zeling_v2/Assets/_Game/Scripts/Camera/CameraLookSystem.cs
2026-05-17 07:56:12 +08:00

196 lines
8.6 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;
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);
}
}
}