using System.Collections; using UnityEngine; using UnityEngine.InputSystem; using Unheimlich.Data.CameraSettings; namespace Unheimlich.Gameplay.CameraControl { /// /// Handles camera panning and player/camera alignment to targets. /// Uses CameraPanSettings ScriptableObject for configuration. /// public class CameraPanController : MonoBehaviour { [Header("References")] [SerializeField] private UnityEngine.Camera _camera; [SerializeField] private Transform _playerTransform; [Header("Settings")] [SerializeField] private CameraPanSettings _settings; [Header("Input Actions")] [SerializeField] private InputActionReference _lookAction; 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 const float INPUT_IDLE_THRESHOLD = 0.1f; public bool IsPanning { get; private set; } 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; } /// /// Starts camera pan mode. /// public void StartPan() { if (_settings == null) { Debug.LogError("[CameraPanController] Settings not assigned!"); return; } IsPanning = true; _timeSinceLastInput = 0f; float currentPitch = _camera.transform.localEulerAngles.x; if (currentPitch > 180f) currentPitch -= 360f; _originalCameraRotation = new Vector2(_playerTransform.eulerAngles.y, currentPitch); } /// /// 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; float panX = 0f; float panY = 0f; bool hasInput = false; if (_lookAction != null && _lookAction.action != null) { Vector2 lookInput = _lookAction.action.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(); 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; } float targetYaw; float targetPitch; if (hasInput || _timeSinceLastInput < INPUT_IDLE_THRESHOLD) { targetYaw = _originalCameraRotation.x + panX * _settings.MaxPanAngle; targetPitch = _originalCameraRotation.y - panY * _settings.MaxPanAngle; } else { targetYaw = _originalCameraRotation.x; targetPitch = _originalCameraRotation.y; } targetYaw = Mathf.Clamp(targetYaw, _originalCameraRotation.x - _settings.MaxPanAngle, _originalCameraRotation.x + _settings.MaxPanAngle); targetPitch = Mathf.Clamp(targetPitch, _originalCameraRotation.y - _settings.MaxPanAngle, _originalCameraRotation.y + _settings.MaxPanAngle); float currentYaw = _playerTransform.eulerAngles.y; float currentPitch = _camera.transform.localEulerAngles.x; if (currentPitch > 180f) currentPitch -= 360f; float lerpSpeed = hasInput ? _settings.PanSpeed : _settings.PanSpeed * 2f; float newYaw = Mathf.LerpAngle(currentYaw, targetYaw, Time.deltaTime * lerpSpeed); float newPitch = Mathf.LerpAngle(currentPitch, targetPitch, Time.deltaTime * lerpSpeed); _playerTransform.eulerAngles = new Vector3(0f, newYaw, 0f); _camera.transform.localEulerAngles = new Vector3(newPitch, 0f, 0f); } /// /// Aligns the player and camera to the target object. /// - Player moves along target's local X axis to align with target center /// - 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; 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 targetRightAxis = target.right; Vector3 playerToTargetCenter = targetCenter - _playerTransform.position; float offsetAlongTargetRight = Vector3.Dot(playerToTargetCenter, targetRightAxis); Vector3 targetPlayerPos = _playerTransform.position + targetRightAxis * offsetAlongTargetRight; 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; } } }