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. /// /// Sprint bob is automatically disabled when OctoPlayer.CanRun is false, /// since IsRunning already returns false in that case. /// 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 (OctoCore.Components.OctoPlayer.Instance == null || 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. float horizontalSpeed = new Vector3( PlayerController.Instance.CharController.velocity.x, 0f, PlayerController.Instance.CharController.velocity.z ).magnitude; // IsRunning already returns false when OctoPlayer.CanRun is off — // so the bob falls back to walk params automatically. 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); ApplySmoothedBob(settings.bobRollAmplitude); } /// /// Applies strafe tilt in LateUpdate so it composites correctly over OctoPlayer's pitch rotation. /// 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); Quaternion baseRot = transform.localRotation; transform.localRotation = baseRot * Quaternion.Euler(0f, 0f, _currentTilt); } // ── Public API ──────────────────────────────────────────────────────────── /// Overrides the head bob mode (Idle, Normal, Disabled). public void SetMode(HeadBobMode mode) { _overrideMode = mode; _hasOverride = true; } /// Clears any active mode override and returns to Normal. public void ClearOverride() { _hasOverride = false; if (_resetCoroutine != null) { StopCoroutine(_resetCoroutine); _resetCoroutine = null; } } /// Lerps the camera local position back to origin over seconds. 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; } 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; 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; transform.localRotation = Quaternion.Euler(0f, 0f, 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; } } }