using UnityEngine; using UnityEngine.InputSystem; using Unheimlich.Core; using Unheimlich.Gameplay.Portals; using InputManager = Unheimlich.Managers.PlayerInputManager; namespace Unheimlich.Gameplay.Player { /// /// Handles first-person camera rotation and look mechanics for the player. /// Supports both mouse and gamepad input with adaptive sensitivity. /// Includes aim assist for dialogue choices and camera shake effects. /// public class PlayerLook : Singleton { private const float INPUT_DEADZONE = 0.1f; private const float MOUSE_DELTA_MULTIPLIER = 0.001f; [Header("Components")] [SerializeField] private Transform _playerCamera, _playerCameraPivot; [Header("Settings")] [SerializeField] private int _frameRate = 120; [SerializeField] private float _mouseSensitivity = 0.2f; [SerializeField] private float _gamepadSensitivity = 100f; [SerializeField] private float _minLookAngle = -60f; [SerializeField] private float _maxLookAngle = 60f; [Header("Aim Assist")] [SerializeField] private float _aimAssistStrength = 25f; [SerializeField] private float _aimAssistMaxAngle = 10f; public bool HasActiveLookInput { get; private set; } public bool IsAimLocked { get; private set; } = false; [Header("Shake Control")] [SerializeField] private float _shakeFrequency = 40f; private float _shakeInterval; private float _shakeTimer = 0f; private Vector3 _shakeTarget = Vector3.zero; private Vector3 _shakeOriginalPos = Vector3.zero; private float _shakeSmoothness = 0f; private float _horizontalRotation; private float _verticalRotation; private bool _isUsingGamepad = false; protected override void OnAwake() { QualitySettings.vSyncCount = 0; Application.targetFrameRate = _frameRate; } private void Start() { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; _shakeInterval = 1f / _shakeFrequency; } private void Update() { if(!InputManager.IsLookingEnabled) return; HandleLook(); } /// /// Processes look input from mouse or gamepad and updates camera rotation. /// Automatically detects input device and applies appropriate sensitivity. /// Clamps vertical rotation to prevent over-rotation. /// private void HandleLook() { Vector2 lookInput = InputManager.Instance.LookInput; HasActiveLookInput = lookInput.magnitude > INPUT_DEADZONE; if (Gamepad.current != null && Gamepad.current.rightStick.ReadValue().magnitude > PortalConstants.GAMEPAD_DEADZONE) _isUsingGamepad = true; else if (Mouse.current != null && Mouse.current.delta.ReadValue().magnitude > PortalConstants.GAMEPAD_DEADZONE) _isUsingGamepad = false; float sensitivity = _isUsingGamepad ? _gamepadSensitivity : _mouseSensitivity; float deltaMultiplier = _isUsingGamepad ? Time.deltaTime : MOUSE_DELTA_MULTIPLIER; _horizontalRotation += lookInput.x * sensitivity * deltaMultiplier; transform.rotation = Quaternion.Euler(0, _horizontalRotation, 0f); _verticalRotation -= lookInput.y * sensitivity * deltaMultiplier; _verticalRotation = Mathf.Clamp(_verticalRotation, _minLookAngle, _maxLookAngle); _playerCamera.localRotation = Quaternion.Euler(_verticalRotation, 0f, 0f); } /// /// Smoothly rotates the player camera toward a target using spherical interpolation. /// Assists player aiming by gently pulling the view toward dialogue choices when looking near them. /// Automatically unlocks if player provides manual input. /// public void ApplyAimAssist(Vector3 targetCenter) { if (IsAimLocked && HasActiveLookInput) IsAimLocked = false; Vector3 directionToTarget = (targetCenter - _playerCamera.position).normalized; float angleError = Vector3.Angle(_playerCamera.forward, directionToTarget); if (IsAimLocked || (angleError > 0.01f && angleError < _aimAssistMaxAngle)) { Quaternion targetRotation = Quaternion.LookRotation(directionToTarget); Quaternion correctedRotation = Quaternion.Slerp( _playerCamera.rotation, targetRotation, _aimAssistStrength * Time.deltaTime ); Quaternion newBodyRotation = Quaternion.Euler(0, correctedRotation.eulerAngles.y, 0); _horizontalRotation = newBodyRotation.eulerAngles.y; transform.rotation = newBodyRotation; float newVerticalAngle = correctedRotation.eulerAngles.x; if (newVerticalAngle > 180) newVerticalAngle -= 360; _verticalRotation = Mathf.Clamp(newVerticalAngle, _minLookAngle, _maxLookAngle); _playerCamera.localRotation = Quaternion.Euler(_verticalRotation, 0f, 0f); } } /// /// Creates a random camera shake effect for feedback (e.g., impacts, events). /// Uses a timer-based interval to control shake frequency. /// Amplitude controls both the intensity and duration of the shake. /// Smoothness controls how quickly the camera interpolates to the shake position (0 = instant, higher = smoother). /// public void Shake(float amplitude, float smoothness = 0f) { _shakeSmoothness = smoothness; _shakeTimer -= Time.deltaTime; if (_shakeTimer <= 0f) { _shakeTimer = _shakeInterval * amplitude; _shakeOriginalPos = _playerCameraPivot.position; float x = Random.Range(-1f, 1f) * amplitude; float y = Random.Range(-1f, 1f) * amplitude; _shakeTarget = _shakeOriginalPos + new Vector3(x, y, 0f); } if (_shakeSmoothness > 0f) { _playerCameraPivot.position = Vector3.Lerp(_playerCameraPivot.position, _shakeTarget, Time.deltaTime * _shakeSmoothness); } else { _playerCameraPivot.position = _shakeTarget; } } public void LockAim(bool locked) { IsAimLocked = locked; } } }