using UnityEngine; using UnityEngine.InputSystem; using Unheimlich.Features.Dialogue; using Unheimlich.Core; namespace Unheimlich.World.CameraSystem { /// /// Virtual camera system for detective board inspection (Alan Wake 2 style). /// Handles smooth transition, edge-based panning, and zoom. /// public class DetectiveBoardVirtualCamera : Singleton { [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(); 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(); 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(); 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(); if (renderer != null) boardCenter = renderer.bounds.center; else { Collider collider = _boardTarget.GetComponent(); 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(); if (renderer != null) boardCenter = renderer.bounds.center; else { Collider collider = _boardTarget.GetComponent(); 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(); 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(); if (boardCanvas != null && boardCanvas.renderMode == RenderMode.WorldSpace) { boardCanvas.worldCamera = _playerCamera; } onComplete?.Invoke(); } } }