Files
Ipseity-Demo/Assets/Unheimlich_POC/Scripts/World/Camera/Runtime/DetectiveBoardVirtualCamera.cs
T
2026-03-16 17:06:47 +01:00

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();
}
}
}