431 lines
17 KiB
C#
431 lines
17 KiB
C#
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using Unheimlich.Features.Dialogue;
|
|
using Unheimlich.Core;
|
|
|
|
namespace Unheimlich.World.CameraSystem
|
|
{
|
|
/// <summary>
|
|
/// Virtual camera system for detective board inspection (Alan Wake 2 style).
|
|
/// Handles smooth transition, edge-based panning, and zoom.
|
|
/// </summary>
|
|
public class DetectiveBoardVirtualCamera : Singleton<DetectiveBoardVirtualCamera>
|
|
{
|
|
[Header("References")]
|
|
[SerializeField] private Camera _virtualCamera;
|
|
private Transform _boardTarget;
|
|
|
|
[Header("Transition Settings")]
|
|
[SerializeField] private float _transitionSpeed = 3f;
|
|
[SerializeField] private AnimationCurve _transitionCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
|
|
[SerializeField] private float _initialDistance = 1.5f;
|
|
|
|
[Header("Pan Settings")]
|
|
[SerializeField] private float _panSpeed = 2f;
|
|
[SerializeField] private float _panSmoothTime = 0.2f;
|
|
[SerializeField][Range(0f, 0.5f)] private float _edgeBorderSize = 0.1f;
|
|
[SerializeField] private Vector2 _maxPanOffset = new Vector2(1.5f, 0.5f);
|
|
[SerializeField] private bool _useDynamicPanLimits = true;
|
|
[SerializeField] private float _panMargin = 0.0f;
|
|
[SerializeField] private bool _debugLimits = false;
|
|
|
|
[Header("Zoom Settings")]
|
|
[SerializeField] private float _minDistance = 0.5f;
|
|
[SerializeField] private float _maxDistance = 3f;
|
|
[SerializeField] private float _zoomSpeed = 2f;
|
|
[SerializeField] private float _zoomSmoothTime = 0.15f;
|
|
[SerializeField] private float _scrollSensitivity = 0.3f;
|
|
|
|
private Camera _playerCamera;
|
|
private Transform _playerCameraTransform;
|
|
private Vector3 _startPosition;
|
|
private Quaternion _startRotation;
|
|
private Vector3 _targetCenterPosition;
|
|
private Quaternion _targetCenterRotation;
|
|
private bool _isActive;
|
|
private bool _isTransitioning;
|
|
private float _transitionProgress;
|
|
private Renderer _cachedBoardRenderer;
|
|
|
|
private Vector2 _currentPanOffset;
|
|
private Vector2 _targetPanOffset;
|
|
private Vector2 _panVelocity;
|
|
private Vector2 _dynamicMaxPanOffset;
|
|
|
|
private float _currentDistance;
|
|
private float _targetDistance;
|
|
private float _zoomVelocity;
|
|
|
|
private Vector2 _lastMousePosition;
|
|
|
|
public bool IsActive => _isActive;
|
|
public Camera VirtualCamera => _virtualCamera;
|
|
|
|
protected override void OnAwake()
|
|
{
|
|
if (_virtualCamera == null)
|
|
_virtualCamera = GetComponent<Camera>();
|
|
|
|
if (_virtualCamera != null)
|
|
_virtualCamera.enabled = false;
|
|
}
|
|
|
|
public void ActivateFromPlayerPosition(Camera playerCamera, Transform boardTarget, Renderer boardRenderer = null)
|
|
{
|
|
if (_isActive)
|
|
return;
|
|
|
|
Debug.Log($"[VirtualCam] ActivateFromPlayerPosition - DebugLimits: {_debugLimits}");
|
|
|
|
_playerCamera = playerCamera;
|
|
_playerCameraTransform = playerCamera.transform;
|
|
_boardTarget = boardTarget;
|
|
_cachedBoardRenderer = boardRenderer;
|
|
|
|
_startPosition = _playerCameraTransform.position;
|
|
_startRotation = _playerCameraTransform.rotation;
|
|
|
|
_virtualCamera.transform.position = _startPosition;
|
|
_virtualCamera.transform.rotation = _startRotation;
|
|
|
|
CalculateTargetPosition();
|
|
|
|
_currentDistance = _initialDistance;
|
|
_targetDistance = _initialDistance;
|
|
_currentPanOffset = Vector2.zero;
|
|
_targetPanOffset = Vector2.zero;
|
|
|
|
_isActive = true;
|
|
_isTransitioning = true;
|
|
_transitionProgress = 0f;
|
|
|
|
_virtualCamera.enabled = true;
|
|
_playerCamera.enabled = false;
|
|
|
|
Canvas boardCanvas = _boardTarget.GetComponentInChildren<Canvas>();
|
|
if (boardCanvas != null && boardCanvas.renderMode == RenderMode.WorldSpace)
|
|
{
|
|
boardCanvas.worldCamera = _virtualCamera;
|
|
}
|
|
|
|
if (Mouse.current != null)
|
|
_lastMousePosition = Mouse.current.position.ReadValue();
|
|
}
|
|
|
|
public void Activate(Camera playerCamera, Transform boardTarget)
|
|
{
|
|
if (_isActive)
|
|
return;
|
|
|
|
_playerCamera = playerCamera;
|
|
_playerCameraTransform = playerCamera.transform;
|
|
_boardTarget = boardTarget;
|
|
|
|
_startPosition = _playerCameraTransform.position;
|
|
_startRotation = _playerCameraTransform.rotation;
|
|
|
|
CalculateTargetPosition();
|
|
|
|
_currentDistance = _initialDistance;
|
|
_targetDistance = _initialDistance;
|
|
_currentPanOffset = Vector2.zero;
|
|
_targetPanOffset = Vector2.zero;
|
|
|
|
_isActive = true;
|
|
_isTransitioning = true;
|
|
_transitionProgress = 0f;
|
|
|
|
_virtualCamera.enabled = true;
|
|
_playerCamera.enabled = false;
|
|
|
|
Canvas boardCanvas = _boardTarget.GetComponentInChildren<Canvas>();
|
|
if (boardCanvas != null && boardCanvas.renderMode == RenderMode.WorldSpace)
|
|
{
|
|
boardCanvas.worldCamera = _virtualCamera;
|
|
}
|
|
|
|
if (Mouse.current != null)
|
|
_lastMousePosition = Mouse.current.position.ReadValue();
|
|
}
|
|
|
|
public void Deactivate(System.Action onComplete = null)
|
|
{
|
|
if (!_isActive)
|
|
{
|
|
onComplete?.Invoke();
|
|
return;
|
|
}
|
|
|
|
StartCoroutine(ReturnTransitionCoroutine(onComplete));
|
|
}
|
|
|
|
private void CalculateTargetPosition()
|
|
{
|
|
Vector3 boardCenter = _boardTarget.position;
|
|
Renderer renderer = _boardTarget.GetComponent<Renderer>();
|
|
if (renderer != null)
|
|
boardCenter = renderer.bounds.center;
|
|
else
|
|
{
|
|
Collider collider = _boardTarget.GetComponent<Collider>();
|
|
if (collider != null)
|
|
boardCenter = collider.bounds.center;
|
|
}
|
|
|
|
_targetCenterPosition = boardCenter - _boardTarget.forward * _initialDistance;
|
|
_targetCenterRotation = Quaternion.LookRotation(_boardTarget.forward);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (_debugLimits)
|
|
Debug.Log($"[VirtualCam] Update - Active: {_isActive} | Transitioning: {_isTransitioning}");
|
|
|
|
if (!_isActive)
|
|
return;
|
|
|
|
if (_isTransitioning)
|
|
{
|
|
UpdateTransition();
|
|
}
|
|
else
|
|
{
|
|
UpdatePanning();
|
|
UpdateZoom();
|
|
ApplyCameraTransform();
|
|
}
|
|
}
|
|
|
|
private void UpdateTransition()
|
|
{
|
|
_transitionProgress += Time.deltaTime * _transitionSpeed;
|
|
float t = _transitionCurve.Evaluate(Mathf.Clamp01(_transitionProgress));
|
|
|
|
Vector3 position = Vector3.Lerp(_startPosition, _targetCenterPosition, t);
|
|
Quaternion rotation = Quaternion.Slerp(_startRotation, _targetCenterRotation, t);
|
|
|
|
_virtualCamera.transform.position = position;
|
|
_virtualCamera.transform.rotation = rotation;
|
|
|
|
if (_transitionProgress >= 1f)
|
|
{
|
|
_isTransitioning = false;
|
|
}
|
|
}
|
|
|
|
private void UpdatePanning()
|
|
{
|
|
float panX = 0f;
|
|
float panY = 0f;
|
|
bool hasInput = false;
|
|
|
|
if (Mouse.current != null)
|
|
{
|
|
Vector2 mousePos = Mouse.current.position.ReadValue();
|
|
_lastMousePosition = mousePos;
|
|
|
|
float screenWidth = Screen.width;
|
|
float screenHeight = Screen.height;
|
|
float borderPixels = _edgeBorderSize * Mathf.Min(screenWidth, screenHeight);
|
|
|
|
if (mousePos.x >= 0f && mousePos.x < borderPixels)
|
|
{
|
|
panX = -(borderPixels - mousePos.x) / borderPixels;
|
|
hasInput = true;
|
|
}
|
|
else if (mousePos.x > screenWidth - borderPixels && mousePos.x <= screenWidth)
|
|
{
|
|
panX = (mousePos.x - (screenWidth - borderPixels)) / borderPixels;
|
|
hasInput = true;
|
|
}
|
|
|
|
if (mousePos.y >= 0f && mousePos.y < borderPixels)
|
|
{
|
|
panY = -(borderPixels - mousePos.y) / borderPixels;
|
|
hasInput = true;
|
|
}
|
|
else if (mousePos.y > screenHeight - borderPixels && mousePos.y <= screenHeight)
|
|
{
|
|
panY = (mousePos.y - (screenHeight - borderPixels)) / borderPixels;
|
|
hasInput = true;
|
|
}
|
|
}
|
|
|
|
if (hasInput)
|
|
{
|
|
float deltaX = panX * _panSpeed * Time.deltaTime;
|
|
float deltaY = panY * _panSpeed * Time.deltaTime;
|
|
|
|
_targetPanOffset.x += deltaX;
|
|
_targetPanOffset.y += deltaY;
|
|
}
|
|
|
|
Vector2 maxOffset = _useDynamicPanLimits ? _dynamicMaxPanOffset : _maxPanOffset;
|
|
|
|
_targetPanOffset.x = Mathf.Clamp(_targetPanOffset.x, -maxOffset.x, maxOffset.x);
|
|
_targetPanOffset.y = Mathf.Clamp(_targetPanOffset.y, -maxOffset.y, maxOffset.y);
|
|
|
|
if (!hasInput && _useDynamicPanLimits)
|
|
{
|
|
if (Mathf.Abs(_targetPanOffset.x) > maxOffset.x)
|
|
{
|
|
float targetX = Mathf.Sign(_targetPanOffset.x) * maxOffset.x;
|
|
_targetPanOffset.x = Mathf.Lerp(_targetPanOffset.x, targetX, Time.deltaTime * 3f);
|
|
}
|
|
|
|
if (Mathf.Abs(_targetPanOffset.y) > maxOffset.y)
|
|
{
|
|
float targetY = Mathf.Sign(_targetPanOffset.y) * maxOffset.y;
|
|
_targetPanOffset.y = Mathf.Lerp(_targetPanOffset.y, targetY, Time.deltaTime * 3f);
|
|
}
|
|
}
|
|
|
|
_currentPanOffset = Vector2.SmoothDamp(_currentPanOffset, _targetPanOffset, ref _panVelocity, _panSmoothTime);
|
|
}
|
|
|
|
private void UpdateZoom()
|
|
{
|
|
if (Mouse.current != null)
|
|
{
|
|
float scroll = Mouse.current.scroll.ReadValue().y;
|
|
if (Mathf.Abs(scroll) > 0.1f)
|
|
{
|
|
_targetDistance -= scroll * _scrollSensitivity * Time.deltaTime;
|
|
_targetDistance = Mathf.Clamp(_targetDistance, _minDistance, _maxDistance);
|
|
}
|
|
}
|
|
|
|
_currentDistance = Mathf.SmoothDamp(_currentDistance, _targetDistance, ref _zoomVelocity, _zoomSmoothTime);
|
|
}
|
|
|
|
private void ApplyCameraTransform()
|
|
{
|
|
if (_debugLimits)
|
|
Debug.Log($"[VirtualCam] ApplyCameraTransform called - BoardTarget: {(_boardTarget != null ? _boardTarget.name : "NULL")}");
|
|
|
|
Vector3 boardCenter = _boardTarget.position;
|
|
Renderer renderer = _cachedBoardRenderer;
|
|
if (renderer == null)
|
|
renderer = _boardTarget.GetComponent<Renderer>();
|
|
|
|
if (renderer != null)
|
|
boardCenter = renderer.bounds.center;
|
|
else
|
|
{
|
|
Collider collider = _boardTarget.GetComponent<Collider>();
|
|
if (collider != null)
|
|
boardCenter = collider.bounds.center;
|
|
}
|
|
|
|
if (_debugLimits)
|
|
Debug.Log($"[VirtualCam] Renderer: {(renderer != null ? "Found" : "NULL")} | UseDynamicLimits: {_useDynamicPanLimits}");
|
|
|
|
Vector3 right = _boardTarget.right;
|
|
Vector3 up = _boardTarget.up;
|
|
Vector3 forward = _boardTarget.forward;
|
|
|
|
if (_useDynamicPanLimits)
|
|
{
|
|
CalculateDynamicPanLimits(renderer);
|
|
}
|
|
|
|
Vector3 panOffset = right * _currentPanOffset.x + up * _currentPanOffset.y;
|
|
Vector3 targetPos = boardCenter + panOffset - forward * _currentDistance;
|
|
|
|
_virtualCamera.transform.position = targetPos;
|
|
_virtualCamera.transform.rotation = Quaternion.LookRotation(forward);
|
|
}
|
|
|
|
private void CalculateDynamicPanLimits(Renderer boardRenderer)
|
|
{
|
|
if (_virtualCamera == null)
|
|
{
|
|
if (_debugLimits)
|
|
Debug.LogWarning("[VirtualCam] No VirtualCamera! Using fixed maxPanOffset.");
|
|
_dynamicMaxPanOffset = _maxPanOffset;
|
|
return;
|
|
}
|
|
|
|
float distance = _currentDistance;
|
|
float fov = _virtualCamera.fieldOfView;
|
|
float aspect = _virtualCamera.aspect;
|
|
|
|
float halfHeightAtDistance = distance * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
|
|
float halfWidthAtDistance = halfHeightAtDistance * aspect;
|
|
|
|
float boardHalfWidth;
|
|
float boardHalfHeight;
|
|
|
|
if (boardRenderer != null)
|
|
{
|
|
Bounds bounds = boardRenderer.bounds;
|
|
Vector3 localSize = _boardTarget.InverseTransformVector(bounds.size);
|
|
|
|
boardHalfWidth = Mathf.Abs(localSize.x) * 0.5f;
|
|
boardHalfHeight = Mathf.Abs(localSize.y) * 0.5f;
|
|
}
|
|
else
|
|
{
|
|
RectTransform rectTransform = _boardTarget.GetComponent<RectTransform>();
|
|
if (rectTransform != null)
|
|
{
|
|
Vector2 size = rectTransform.rect.size;
|
|
Vector3 scale = rectTransform.lossyScale;
|
|
|
|
boardHalfWidth = (size.x * scale.x) * 0.5f;
|
|
boardHalfHeight = (size.y * scale.y) * 0.5f;
|
|
}
|
|
else
|
|
{
|
|
if (_debugLimits)
|
|
Debug.LogWarning("[VirtualCam] No Renderer or RectTransform found! Using fixed maxPanOffset.");
|
|
_dynamicMaxPanOffset = _maxPanOffset;
|
|
return;
|
|
}
|
|
}
|
|
|
|
_dynamicMaxPanOffset.x = Mathf.Max(0f, boardHalfWidth - halfWidthAtDistance - _panMargin);
|
|
_dynamicMaxPanOffset.y = Mathf.Max(0f, boardHalfHeight - halfHeightAtDistance - _panMargin);
|
|
|
|
if (_debugLimits)
|
|
{
|
|
Debug.Log($"[VirtualCam] Distance: {distance:F2} | FOV: {fov:F1} | Aspect: {aspect:F2}");
|
|
Debug.Log($"[VirtualCam] Viewport: {halfWidthAtDistance*2:F2}x{halfHeightAtDistance*2:F2} | Board: {boardHalfWidth*2:F2}x{boardHalfHeight*2:F2}");
|
|
Debug.Log($"[VirtualCam] MaxPan: X={_dynamicMaxPanOffset.x:F2} Y={_dynamicMaxPanOffset.y:F2} | Current: X={_targetPanOffset.x:F2} Y={_targetPanOffset.y:F2}");
|
|
}
|
|
}
|
|
|
|
private System.Collections.IEnumerator ReturnTransitionCoroutine(System.Action onComplete)
|
|
{
|
|
Vector3 currentPos = _virtualCamera.transform.position;
|
|
Quaternion currentRot = _virtualCamera.transform.rotation;
|
|
|
|
float elapsed = 0f;
|
|
float duration = 1f / _transitionSpeed;
|
|
|
|
while (elapsed < duration)
|
|
{
|
|
elapsed += Time.deltaTime;
|
|
float t = _transitionCurve.Evaluate(Mathf.Clamp01(elapsed / duration));
|
|
|
|
_virtualCamera.transform.position = Vector3.Lerp(currentPos, _startPosition, t);
|
|
_virtualCamera.transform.rotation = Quaternion.Slerp(currentRot, _startRotation, t);
|
|
|
|
yield return null;
|
|
}
|
|
|
|
_virtualCamera.enabled = false;
|
|
_playerCamera.enabled = true;
|
|
_isActive = false;
|
|
|
|
Canvas boardCanvas = _boardTarget.GetComponentInChildren<Canvas>();
|
|
if (boardCanvas != null && boardCanvas.renderMode == RenderMode.WorldSpace)
|
|
{
|
|
boardCanvas.worldCamera = _playerCamera;
|
|
}
|
|
|
|
onComplete?.Invoke();
|
|
}
|
|
}
|
|
}
|