using UnityEngine; using Unheimlich.Core.Interaction; using Unheimlich.World.CameraSystem; namespace Unheimlich.Features.Player { /// /// 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. /// Both rolls are combined additively to avoid asymmetric drift. /// public class CameraHeadBob : MonoBehaviour { private float _timer = 0f; private Vector3 _startLocalPos; private float _currentBobSpeed = 0f; private float _currentBobAmplitude = 0f; private float _currentBobRoll = 0f; private float _currentTilt = 0f; private CameraEffects.HeadBobMode _overrideMode = CameraEffects.HeadBobMode.Normal; private bool _hasOverride = false; private Coroutine _resetCoroutine; private void Start() { _startLocalPos = transform.localPosition; } private void Update() { if (OctoCore.Components.OctoPlayer.Instance == null || PlayerController.Instance == null || PlayerController.Instance.CharController == null) return; var settings = PlayerController.Instance.CameraEffects?.Settings; if (settings == null) return; CameraEffects.HeadBobMode currentMode = _hasOverride ? _overrideMode : CameraEffects.HeadBobMode.Normal; if (currentMode == CameraEffects.HeadBobMode.Disabled) { SetBobDirect(settings.idleBobSpeed, 0f); _currentBobRoll = ApplySmoothedBob(settings.bobRollAmplitude); return; } if (currentMode == CameraEffects.HeadBobMode.Idle) { SetBobDirect(settings.idleBobSpeed, settings.idleBobAmplitude); _currentBobRoll = ApplySmoothedBob(settings.bobRollAmplitude); return; } bool isGrounded = PlayerController.Instance.CharController.isGrounded; if (!isGrounded) { SetBobDirect(settings.idleBobSpeed, 0f); _currentBobRoll = ApplySmoothedBob(settings.bobRollAmplitude); return; } float horizontalSpeed = new Vector3( PlayerController.Instance.CharController.velocity.x, 0f, PlayerController.Instance.CharController.velocity.z ).magnitude; bool isRunning = OctoCore.Components.OctoPlayer.Instance.State.IsRunning; 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); _currentBobRoll = ApplySmoothedBob(settings.bobRollAmplitude); } private void LateUpdate() { if (PlayerController.Instance == null || PlayerController.Instance.CharController == null) return; var settings = PlayerController.Instance.CameraEffects?.Settings; if (settings == null) return; Vector3 velocity = PlayerController.Instance.CharController.velocity; float strafeAmount = Vector3.Dot(velocity, PlayerController.Instance.transform.right); 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); // Combine bob roll + strafe tilt additively transform.localRotation = Quaternion.Euler(0f, 0f, _currentBobRoll + _currentTilt); } // ── Public API ──────────────────────────────────────────────────────────── public void SetMode(CameraEffects.HeadBobMode mode) { _overrideMode = mode; _hasOverride = true; } public void ClearOverride() { _hasOverride = false; if (_resetCoroutine != null) { StopCoroutine(_resetCoroutine); _resetCoroutine = null; } } public void LerpToZero(float duration) { if (_resetCoroutine != null) StopCoroutine(_resetCoroutine); _resetCoroutine = StartCoroutine(LerpToZeroCoroutine(duration)); } // ── Private helpers ─────────────────────────────────────────────────────── private void SetBobDirect(float speed, float amplitude) { _currentBobSpeed = speed; _currentBobAmplitude = amplitude; } /// /// Applies bob position and returns the Z roll value (degrees). /// Rotation is NOT set here — LateUpdate combines it with strafe tilt. /// private float ApplySmoothedBob(float rollAmplitude) { _timer += Time.deltaTime * _currentBobSpeed; float x = Mathf.Sin(_timer * 0.5f) * _currentBobAmplitude * 0.6f; float y = Mathf.Cos(_timer) * _currentBobAmplitude; 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; return roll; } 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; } } }