chore: Project Restructuring & Theatre
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user