362 lines
14 KiB
C#
362 lines
14 KiB
C#
using System.Collections;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using Unheimlich.Data.CameraSettings;
|
|
using PlayerInput = Unheimlich.Managers.PlayerInputManager;
|
|
|
|
namespace Unheimlich.Gameplay.CameraControl
|
|
{
|
|
/// <summary>
|
|
/// Handles camera panning and player/camera alignment to targets.
|
|
/// Uses CameraPanSettings ScriptableObject for configuration.
|
|
/// </summary>
|
|
public class CameraPanController : MonoBehaviour
|
|
{
|
|
[Header("References")]
|
|
[SerializeField] private UnityEngine.Camera _camera;
|
|
[SerializeField] private Transform _playerTransform;
|
|
|
|
[Header("Settings")]
|
|
[SerializeField] 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;
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts camera pan mode.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops camera pan mode and returns to original state.
|
|
/// </summary>
|
|
public void StopPan()
|
|
{
|
|
IsPanning = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates camera pan based on input (gamepad or mouse).
|
|
/// Call this from Update when pan is active.
|
|
/// </summary>
|
|
public void UpdatePan()
|
|
{
|
|
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<Vector2>();
|
|
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 currentYaw = _playerTransform.eulerAngles.y;
|
|
float currentPitch = _camera.transform.localEulerAngles.x;
|
|
if (currentPitch > 180f)
|
|
currentPitch -= 360f;
|
|
|
|
float targetYaw;
|
|
float targetPitch;
|
|
float lerpSpeed;
|
|
|
|
bool shouldRecenter = _settings.EnableRecentering && _timeSinceLastInput >= _settings.RecenterDelay;
|
|
|
|
if (hasInput)
|
|
{
|
|
targetYaw = _originalCameraRotation.x + panX * _settings.MaxPanAngle;
|
|
targetPitch = _originalCameraRotation.y - panY * _settings.MaxPanAngle;
|
|
targetYaw = Mathf.Clamp(targetYaw, _originalCameraRotation.x - _settings.MaxPanAngle, _originalCameraRotation.x + _settings.MaxPanAngle);
|
|
targetPitch = Mathf.Clamp(targetPitch, _originalCameraRotation.y - _settings.MaxPanAngle, _originalCameraRotation.y + _settings.MaxPanAngle);
|
|
lerpSpeed = _settings.PanSpeed;
|
|
}
|
|
else if (shouldRecenter)
|
|
{
|
|
targetYaw = _originalCameraRotation.x;
|
|
targetPitch = _originalCameraRotation.y;
|
|
lerpSpeed = _settings.RecenterSpeed;
|
|
}
|
|
else
|
|
{
|
|
targetYaw = currentYaw;
|
|
targetPitch = currentPitch;
|
|
lerpSpeed = 0f;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public void AlignToTarget(Transform target, System.Action onComplete = null)
|
|
{
|
|
if (_alignmentCoroutine != null)
|
|
StopCoroutine(_alignmentCoroutine);
|
|
|
|
_alignmentCoroutine = StartCoroutine(AlignCameraToTargetCoroutine(target, onComplete));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns player and camera to original position and rotation.
|
|
/// </summary>
|
|
public void ReturnToOriginalPosition(System.Action onComplete = null)
|
|
{
|
|
if (_returnCoroutine != null)
|
|
StopCoroutine(_returnCoroutine);
|
|
|
|
_returnCoroutine = StartCoroutine(ReturnToOriginalPositionCoroutine(onComplete));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves current player and camera state as the original position.
|
|
/// Call this before starting alignment.
|
|
/// </summary>
|
|
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<Renderer>();
|
|
if (targetRenderer != null)
|
|
{
|
|
targetCenter = targetRenderer.bounds.center;
|
|
}
|
|
else
|
|
{
|
|
Collider targetCollider = target.GetComponent<Collider>();
|
|
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;
|
|
}
|
|
}
|
|
}
|