using System.Collections; using UnityEngine; using UnityEngine.InputSystem; using Unheimlich.Data.CameraSettings; using PlayerInput = Unheimlich.Managers.PlayerInputManager; namespace Unheimlich.Gameplay.CameraControl { /// /// Handles camera panning and player/camera alignment to targets. /// Uses CameraPanSettings ScriptableObject for configuration. /// Supports two modes: /// - Pan: Smooth panning with edge detection (for object inspection) /// - FreeLook: Direct mouse/stick control with angle clamping (for detective board) /// public class CameraPanController : MonoBehaviour { [Header("References")] [SerializeField] private UnityEngine.Camera _camera; [SerializeField] private Transform _playerTransform; [Header("Default Settings")] [SerializeField] private CameraPanSettings _defaultSettings; private CameraPanSettings _settings; private Vector2 _originalCameraRotation; private Vector3 _originalCameraLocalPosition; private Vector3 _originalPlayerPosition; private Vector3 _originalPlayerRotation; private Vector3 _originalCameraLocalRotation; private Coroutine _alignmentCoroutine; private Coroutine _returnCoroutine; private float _timeSinceLastInput; private float _targetYaw; private float _targetPitch; private float _currentYaw; private float _currentPitch; private float _lerpSpeed; private bool _needsUpdate; private CameraPanMode _mode = CameraPanMode.Pan; private Vector2 _lastMousePosition; public bool IsPanning { get; private set; } public CameraPanMode Mode => _mode; public CameraPanSettings Settings { get => _settings; set => _settings = value; } private void Awake() { if (_camera == null) _camera = UnityEngine.Camera.main; if (_playerTransform == null) _playerTransform = transform; if (_camera != null) { _originalCameraLocalPosition = _camera.transform.localPosition; _originalCameraLocalRotation = _camera.transform.localEulerAngles; } _originalPlayerPosition = _playerTransform.position; _originalPlayerRotation = _playerTransform.eulerAngles; if (_settings == null && _defaultSettings != null) _settings = _defaultSettings; } private void LateUpdate() { if (IsPanning && _needsUpdate) { if (_mode == CameraPanMode.FreeLook) { float normalizedYaw = Mathf.Repeat(_currentYaw, 360f); float normalizedPitch = _currentPitch; _playerTransform.eulerAngles = new Vector3(0f, normalizedYaw, 0f); _camera.transform.localEulerAngles = new Vector3(normalizedPitch, 0f, 0f); } else { _currentYaw = Mathf.LerpAngle(_currentYaw, _targetYaw, Time.deltaTime * _lerpSpeed); _currentPitch = Mathf.LerpAngle(_currentPitch, _targetPitch, Time.deltaTime * _lerpSpeed); float normalizedYaw = Mathf.Repeat(_currentYaw, 360f); float normalizedPitch = _currentPitch; _playerTransform.eulerAngles = new Vector3(0f, normalizedYaw, 0f); _camera.transform.localEulerAngles = new Vector3(normalizedPitch, 0f, 0f); } } } /// /// Starts camera pan mode. The mode (Pan or FreeLook) is determined by the current settings. /// public void StartPan() { if (_settings == null) { Debug.LogError("[CameraPanController] Settings not assigned!"); return; } IsPanning = true; _mode = _settings.PanMode; _timeSinceLastInput = 0f; if (Mouse.current != null) _lastMousePosition = Mouse.current.position.ReadValue(); float currentPitch = _camera.transform.localEulerAngles.x; if (currentPitch > 180f) currentPitch -= 360f; float currentYaw = _playerTransform.eulerAngles.y; if (currentYaw > 180f) currentYaw -= 360f; _originalCameraRotation = new Vector2(currentYaw, currentPitch); _currentYaw = currentYaw; _currentPitch = currentPitch; _targetYaw = currentYaw; _targetPitch = currentPitch; _needsUpdate = false; } /// /// Stops camera pan mode and returns to original state. /// public void StopPan() { IsPanning = false; } /// /// Updates camera pan based on input (gamepad or mouse). /// Call this from Update when pan is active. /// public void UpdatePan() { if (!IsPanning || _camera == null || _settings == null) return; if (_mode == CameraPanMode.FreeLook) { UpdateFreeLook(); } else { UpdatePanMode(); } } private void UpdateFreeLook() { float lookX = 0f; float lookY = 0f; bool hasInput = false; var lookAction = PlayerInput.GetLookAction(); if (lookAction != null) { Vector2 lookInput = lookAction.ReadValue(); if (lookInput.sqrMagnitude > 0.01f) { lookX = lookInput.x; lookY = lookInput.y; hasInput = true; } } if (hasInput) { float sensitivity = _settings.PanSpeed * 0.1f; _currentYaw += lookX * sensitivity; _currentPitch -= lookY * sensitivity; _currentYaw = Mathf.Clamp(_currentYaw, _originalCameraRotation.x - _settings.MaxPanAngleX, _originalCameraRotation.x + _settings.MaxPanAngleX); _currentPitch = Mathf.Clamp(_currentPitch, _originalCameraRotation.y - _settings.MaxPanAngleY, _originalCameraRotation.y + _settings.MaxPanAngleY); _needsUpdate = true; } } private void UpdatePanMode() { if (!IsPanning || _camera == null || _settings == null) return; float panX = 0f; float panY = 0f; bool hasInput = false; var lookAction = PlayerInput.GetLookAction(); if (lookAction != null) { Vector2 lookInput = lookAction.ReadValue(); if (lookInput.sqrMagnitude > 0.01f) { panX = lookInput.x; panY = lookInput.y; hasInput = true; } } if (!hasInput && Mouse.current != null) { Vector2 mousePos = Mouse.current.position.ReadValue(); bool mouseHasMoved = Vector2.Distance(mousePos, _lastMousePosition) > 0.1f; _lastMousePosition = mousePos; if (mouseHasMoved) { float screenWidth = Screen.width; float screenHeight = Screen.height; float borderPixels = _settings.PanBorderSize * Mathf.Min(screenWidth, screenHeight); if (mousePos.x < borderPixels) { panX = -(borderPixels - mousePos.x) / borderPixels; hasInput = true; } else if (mousePos.x > screenWidth - borderPixels) { panX = (mousePos.x - (screenWidth - borderPixels)) / borderPixels; hasInput = true; } if (mousePos.y < borderPixels) { panY = -(borderPixels - mousePos.y) / borderPixels; hasInput = true; } else if (mousePos.y > screenHeight - borderPixels) { panY = (mousePos.y - (screenHeight - borderPixels)) / borderPixels; hasInput = true; } } } if (hasInput) { _timeSinceLastInput = 0f; } else { _timeSinceLastInput += Time.deltaTime; } bool shouldRecenter = _settings.EnableRecentering && _timeSinceLastInput >= _settings.RecenterDelay; if (hasInput) { _targetYaw = _originalCameraRotation.x + panX * _settings.MaxPanAngleX; _targetPitch = _originalCameraRotation.y - panY * _settings.MaxPanAngleY; _targetYaw = Mathf.Clamp(_targetYaw, _originalCameraRotation.x - _settings.MaxPanAngleX, _originalCameraRotation.x + _settings.MaxPanAngleX); _targetPitch = Mathf.Clamp(_targetPitch, _originalCameraRotation.y - _settings.MaxPanAngleY, _originalCameraRotation.y + _settings.MaxPanAngleY); _lerpSpeed = _settings.PanSpeed; _needsUpdate = true; } else if (shouldRecenter) { _targetYaw = _originalCameraRotation.x; _targetPitch = _originalCameraRotation.y; _lerpSpeed = _settings.RecenterSpeed; _needsUpdate = true; } else if (_needsUpdate) { float yawDiff = Mathf.Abs(Mathf.DeltaAngle(_currentYaw, _targetYaw)); float pitchDiff = Mathf.Abs(Mathf.DeltaAngle(_currentPitch, _targetPitch)); if (yawDiff < 0.01f && pitchDiff < 0.01f) { _needsUpdate = false; } } } /// /// Aligns the player and camera to the target object. /// - Player moves to a fixed distance in front of the target /// - Player rotates to face the target /// - Camera pitch adjusts to look at target center /// public void AlignToTarget(Transform target, System.Action onComplete = null) { if (_alignmentCoroutine != null) StopCoroutine(_alignmentCoroutine); _alignmentCoroutine = StartCoroutine(AlignCameraToTargetCoroutine(target, onComplete)); } /// /// Returns player and camera to original position and rotation. /// public void ReturnToOriginalPosition(System.Action onComplete = null) { if (_returnCoroutine != null) StopCoroutine(_returnCoroutine); _returnCoroutine = StartCoroutine(ReturnToOriginalPositionCoroutine(onComplete)); } /// /// Saves current player and camera state as the original position. /// Call this before starting alignment. /// public void SaveOriginalState() { if (_camera != null) { _originalCameraLocalPosition = _camera.transform.localPosition; _originalCameraLocalRotation = _camera.transform.localEulerAngles; } _originalPlayerPosition = _playerTransform.position; _originalPlayerRotation = _playerTransform.eulerAngles; } private IEnumerator AlignCameraToTargetCoroutine(Transform target, System.Action onComplete) { if (_camera == null || target == null || _settings == null) { onComplete?.Invoke(); yield break; } SaveOriginalState(); Vector3 targetCenter = target.position; Renderer targetRenderer = target.GetComponent(); if (targetRenderer != null) { targetCenter = targetRenderer.bounds.center; } else { Collider targetCollider = target.GetComponent(); if (targetCollider != null) targetCenter = targetCollider.bounds.center; } Vector3 startPlayerPos = _playerTransform.position; Vector3 startPlayerRotation = _playerTransform.eulerAngles; Vector3 startCameraLocalRotation = _camera.transform.localEulerAngles; float startPitch = startCameraLocalRotation.x; if (startPitch > 180f) startPitch -= 360f; Vector3 targetForward = target.forward; Vector3 targetPlayerPos = targetCenter - targetForward * _settings.FixedDistanceFromTarget; targetPlayerPos.y = startPlayerPos.y; float elapsed = 0f; float duration = 1f / _settings.AlignmentSpeed; while (elapsed < duration) { elapsed += Time.deltaTime; float t = GetInterpolationValue(elapsed / duration); Vector3 currentCameraPos = _camera.transform.position; Vector3 toTarget = targetCenter - currentCameraPos; Vector3 directionToTarget = toTarget.normalized; Quaternion targetRotation = Quaternion.LookRotation(directionToTarget); float targetYaw = targetRotation.eulerAngles.y; float targetPitch = -Mathf.Asin(directionToTarget.y) * Mathf.Rad2Deg; Vector3 currentPlayerPos = Vector3.Lerp(startPlayerPos, targetPlayerPos, t); _playerTransform.position = currentPlayerPos; float currentYaw = Mathf.LerpAngle(startPlayerRotation.y, targetYaw, t); _playerTransform.eulerAngles = new Vector3(0f, currentYaw, 0f); float currentPitch = Mathf.LerpAngle(startPitch, targetPitch, t); _camera.transform.localEulerAngles = new Vector3(currentPitch, 0f, 0f); yield return null; } _alignmentCoroutine = null; onComplete?.Invoke(); } private IEnumerator ReturnToOriginalPositionCoroutine(System.Action onComplete) { if (_camera == null || _settings == null) { onComplete?.Invoke(); yield break; } Vector3 startPlayerPos = _playerTransform.position; Vector3 startPlayerRotation = _playerTransform.eulerAngles; Vector3 startCameraLocalPos = _camera.transform.localPosition; Vector3 startCameraLocalRotation = _camera.transform.localEulerAngles; float startPitch = startCameraLocalRotation.x; if (startPitch > 180f) startPitch -= 360f; float targetPitch = _originalCameraLocalRotation.x; if (targetPitch > 180f) targetPitch -= 360f; float elapsed = 0f; float duration = 1f / _settings.AlignmentSpeed; while (elapsed < duration) { elapsed += Time.deltaTime; float t = GetInterpolationValue(elapsed / duration); _playerTransform.position = Vector3.Lerp(startPlayerPos, _originalPlayerPosition, t); float currentYaw = Mathf.LerpAngle(startPlayerRotation.y, _originalPlayerRotation.y, t); _playerTransform.eulerAngles = new Vector3(0f, currentYaw, 0f); float currentPitch = Mathf.LerpAngle(startPitch, targetPitch, t); _camera.transform.localEulerAngles = new Vector3(currentPitch, 0f, 0f); _camera.transform.localPosition = Vector3.Lerp(startCameraLocalPos, _originalCameraLocalPosition, t); yield return null; } _playerTransform.position = _originalPlayerPosition; _playerTransform.eulerAngles = _originalPlayerRotation; _camera.transform.localPosition = _originalCameraLocalPosition; _camera.transform.localEulerAngles = _originalCameraLocalRotation; _returnCoroutine = null; onComplete?.Invoke(); } private float GetInterpolationValue(float t) { if (_settings.AlignmentCurve != null && _settings.AlignmentCurve.length > 0) return _settings.AlignmentCurve.Evaluate(t); if (_settings.UseSmoothStep) return Mathf.SmoothStep(0f, 1f, t); return t; } } }