199 lines
8.1 KiB
C#
199 lines
8.1 KiB
C#
using UnityEngine;
|
|
using Unheimlich.Core.Interaction;
|
|
using Unheimlich.World.CameraSystem;
|
|
|
|
namespace Unheimlich.Features.Player
|
|
{
|
|
/// <summary>
|
|
/// Simulates camera head bobbing motion during player movement and idle states.
|
|
/// Smoothly transitions between idle, walk and run bob parameters.
|
|
/// Adds a subtle Z-axis roll to the bob and a strafe tilt applied in LateUpdate.
|
|
/// </summary>
|
|
public class CameraHeadBob : MonoBehaviour
|
|
{
|
|
private float _timer = 0f;
|
|
private Vector3 _startLocalPos;
|
|
|
|
// Smoothly interpolated bob parameters to avoid brutal state switches.
|
|
private float _currentBobSpeed = 0f;
|
|
private float _currentBobAmplitude = 0f;
|
|
|
|
// Strafe tilt (Z roll applied in LateUpdate, after OctoPlayer sets camera pitch).
|
|
private float _currentTilt = 0f;
|
|
|
|
private HeadBobMode _overrideMode = HeadBobMode.Normal;
|
|
private bool _hasOverride = false;
|
|
private Coroutine _resetCoroutine;
|
|
|
|
private void Start()
|
|
{
|
|
_startLocalPos = transform.localPosition;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (PlayerController.Instance == null || PlayerController.Instance.CharController == null)
|
|
return;
|
|
|
|
var settings = PlayerController.Instance.CameraEffects?.Settings;
|
|
if (settings == null) return;
|
|
|
|
HeadBobMode currentMode = _hasOverride ? _overrideMode : HeadBobMode.Normal;
|
|
|
|
if (currentMode == HeadBobMode.Disabled)
|
|
{
|
|
SetBobDirect(settings.idleBobSpeed, 0f);
|
|
ApplySmoothedBob(settings.bobRollAmplitude);
|
|
return;
|
|
}
|
|
|
|
bool forceIdle = currentMode == HeadBobMode.Idle
|
|
|| (PlayerInspectionHandler.Instance != null && PlayerInspectionHandler.Instance.IsInspecting);
|
|
|
|
if (forceIdle)
|
|
{
|
|
SetBobDirect(settings.idleBobSpeed, settings.idleBobAmplitude);
|
|
ApplySmoothedBob(settings.bobRollAmplitude);
|
|
return;
|
|
}
|
|
|
|
bool isGrounded = PlayerController.Instance.CharController.isGrounded;
|
|
if (!isGrounded)
|
|
{
|
|
SetBobDirect(settings.idleBobSpeed, 0f);
|
|
ApplySmoothedBob(settings.bobRollAmplitude);
|
|
return;
|
|
}
|
|
|
|
// Drive bob parameters directly from the actual physics velocity.
|
|
// This is naturally in sync with OctoPlayer's inertia — no extra lerp needed.
|
|
float horizontalSpeed = new Vector3(
|
|
PlayerController.Instance.CharController.velocity.x,
|
|
0f,
|
|
PlayerController.Instance.CharController.velocity.z).magnitude;
|
|
|
|
bool isRunning = PlayerController.Instance.IsRunning;
|
|
|
|
// Normalise: 0 = stopped, 1 = full walk/run speed (m/s converted from km/h ÷ 3.6).
|
|
const float WalkSpeedMs = 10f / 3.6f;
|
|
const float RunSpeedMs = 20f / 3.6f;
|
|
|
|
float maxSpeed = isRunning ? RunSpeedMs : WalkSpeedMs;
|
|
float t = Mathf.Clamp01(horizontalSpeed / maxSpeed);
|
|
|
|
float targetSpeed = Mathf.Lerp(settings.idleBobSpeed, isRunning ? settings.runBobSpeed : settings.walkBobSpeed, t);
|
|
float targetAmplitude = Mathf.Lerp(settings.idleBobAmplitude, isRunning ? settings.runBobAmplitude : settings.walkBobAmplitude, t);
|
|
|
|
SetBobDirect(targetSpeed, targetAmplitude);
|
|
ApplySmoothedBob(settings.bobRollAmplitude);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies strafe tilt in LateUpdate so it composites correctly over OctoPlayer's pitch rotation.
|
|
/// </summary>
|
|
private void LateUpdate()
|
|
{
|
|
if (PlayerController.Instance == null || PlayerController.Instance.CharController == null)
|
|
return;
|
|
|
|
var settings = PlayerController.Instance.CameraEffects?.Settings;
|
|
if (settings == null) return;
|
|
|
|
// Strafe direction in local space using actual character velocity.
|
|
Vector3 velocity = PlayerController.Instance.CharController.velocity;
|
|
float strafeAmount = Vector3.Dot(velocity, PlayerController.Instance.transform.right);
|
|
|
|
// Normalise by run speed (km/h → m/s: / 3.6) to get a -1..1 factor.
|
|
float runSpeedMs = PlayerController.Instance.Movement != null ? 20f / 3.6f : 5.5f;
|
|
float strafeFactor = Mathf.Clamp(strafeAmount / runSpeedMs, -1f, 1f);
|
|
|
|
float targetTilt = -strafeFactor * settings.strafeTiltAngle;
|
|
_currentTilt = Mathf.Lerp(_currentTilt, targetTilt, Time.deltaTime * settings.strafeTiltSpeed);
|
|
|
|
// Compose with the existing local rotation (OctoPlayer already wrote pitch into it).
|
|
Quaternion baseRot = transform.localRotation;
|
|
transform.localRotation = baseRot * Quaternion.Euler(0f, 0f, _currentTilt);
|
|
}
|
|
|
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Overrides the head bob mode (Idle, Normal, Disabled).</summary>
|
|
public void SetMode(HeadBobMode mode)
|
|
{
|
|
_overrideMode = mode;
|
|
_hasOverride = true;
|
|
}
|
|
|
|
/// <summary>Clears any active mode override and returns to Normal.</summary>
|
|
public void ClearOverride()
|
|
{
|
|
_hasOverride = false;
|
|
|
|
if (_resetCoroutine != null)
|
|
{
|
|
StopCoroutine(_resetCoroutine);
|
|
_resetCoroutine = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>Lerps the camera local position back to origin over <paramref name="duration"/> seconds.</summary>
|
|
public void LerpToZero(float duration)
|
|
{
|
|
if (_resetCoroutine != null)
|
|
StopCoroutine(_resetCoroutine);
|
|
|
|
_resetCoroutine = StartCoroutine(LerpToZeroCoroutine(duration));
|
|
}
|
|
|
|
// ── Private helpers ───────────────────────────────────────────────────────
|
|
|
|
/// <summary>Sets bob speed and amplitude directly without lerp — velocity already carries the inertia.</summary>
|
|
private void SetBobDirect(float speed, float amplitude)
|
|
{
|
|
_currentBobSpeed = speed;
|
|
_currentBobAmplitude = amplitude;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advances the bob timer and writes the resulting local position offset.
|
|
/// Includes a subtle Z roll (Lissajous third axis) for organic feel.
|
|
/// </summary>
|
|
private void ApplySmoothedBob(float rollAmplitude)
|
|
{
|
|
_timer += Time.deltaTime * _currentBobSpeed;
|
|
|
|
float x = Mathf.Sin(_timer * 0.5f) * _currentBobAmplitude * 0.6f;
|
|
float y = Mathf.Cos(_timer) * _currentBobAmplitude;
|
|
|
|
// Micro Z roll: half frequency of Y so it drifts independently.
|
|
float roll = Mathf.Sin(_timer * 0.5f + Mathf.PI * 0.25f) * rollAmplitude * (_currentBobAmplitude / Mathf.Max(0.001f, 0.03f));
|
|
|
|
Vector3 newPos = _startLocalPos;
|
|
newPos.x += x;
|
|
newPos.y += y;
|
|
transform.localPosition = newPos;
|
|
|
|
// Store roll so LateUpdate can composite it with the strafe tilt.
|
|
// We write only the bob-driven roll here; LateUpdate adds strafe tilt on top.
|
|
Quaternion bobRot = Quaternion.Euler(0f, 0f, roll);
|
|
transform.localRotation = bobRot;
|
|
}
|
|
|
|
private System.Collections.IEnumerator LerpToZeroCoroutine(float duration)
|
|
{
|
|
Vector3 startPos = transform.localPosition;
|
|
float elapsed = 0f;
|
|
|
|
while (elapsed < duration)
|
|
{
|
|
elapsed += Time.deltaTime;
|
|
float t = Mathf.Clamp01(elapsed / duration);
|
|
transform.localPosition = Vector3.Lerp(startPos, _startLocalPos, t);
|
|
yield return null;
|
|
}
|
|
|
|
transform.localPosition = _startLocalPos;
|
|
_resetCoroutine = null;
|
|
}
|
|
}
|
|
} |