MazeCameraController.cs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. using UnityEngine;
  2. namespace MazeGen
  3. {
  4. /// <summary>
  5. /// Top-down camera rig: smoothly glides to focus on newly entered rooms, and can be
  6. /// freely panned with WASD or by holding the middle mouse button and dragging. Any
  7. /// manual pan input cancels whatever auto-focus is currently in progress.
  8. /// </summary>
  9. public class MazeCameraController : MonoBehaviour
  10. {
  11. [SerializeField] private float panSpeed = 8f;
  12. [SerializeField] private float focusSharpness = 6f;
  13. [SerializeField] private float dragSensitivity = 1f;
  14. [SerializeField] private float zoomSpeed = 150f;
  15. [SerializeField] private float minOrthographicSize = 2f;
  16. [SerializeField] private float maxOrthographicSize = 150f;
  17. private Camera _camera;
  18. private Vector2? _focusTarget;
  19. private Vector2 _lastPointerScreenPos;
  20. private bool _isDragging;
  21. private void Awake()
  22. {
  23. _camera = GetComponent<Camera>();
  24. if (_camera == null) _camera = Camera.main;
  25. }
  26. /// <summary>Smoothly glides the camera to look at worldPosition.</summary>
  27. public void FocusOn(Vector2 worldPosition)
  28. {
  29. _focusTarget = worldPosition;
  30. }
  31. /// <summary>Instantly moves the camera to worldPosition, no smoothing.</summary>
  32. public void SnapTo(Vector2 worldPosition)
  33. {
  34. _focusTarget = null;
  35. transform.position = new Vector3(worldPosition.x, worldPosition.y, transform.position.z);
  36. }
  37. /// <summary>
  38. /// Instantly repositions and (if orthographic) zooms out/in so the whole of `bounds`
  39. /// is visible, e.g. for a "zoom to fit maze" button. Cancels any in-progress
  40. /// auto-focus and does not itself start following anything.
  41. /// </summary>
  42. public void FrameBounds(Bounds bounds, float padding = 1.2f)
  43. {
  44. _focusTarget = null;
  45. Vector2 center = bounds.center;
  46. transform.position = new Vector3(center.x, center.y, transform.position.z);
  47. if (_camera != null && _camera.orthographic)
  48. {
  49. float aspect = Mathf.Max(_camera.aspect, 0.01f);
  50. float requiredHalfHeight = Mathf.Max(bounds.extents.y, bounds.extents.x / aspect);
  51. _camera.orthographicSize = Mathf.Max(requiredHalfHeight * padding, 1f);
  52. }
  53. }
  54. private void Update()
  55. {
  56. HandleDrag();
  57. HandleKeyboardPan();
  58. HandleScrollZoomFOV();
  59. HandleAutoFocus();
  60. }
  61. private void HandleScrollZoomFOV()
  62. {
  63. if (_camera == null || _camera.orthographic) return;
  64. float scroll = MazeInput.GetScrollDelta();
  65. if (Mathf.Approximately(scroll, 0f)) return;
  66. // Decreasing FOV zooms in, increasing FOV zooms out
  67. _camera.fieldOfView = Mathf.Clamp(
  68. _camera.fieldOfView - (scroll * zoomSpeed),
  69. minOrthographicSize,
  70. maxOrthographicSize
  71. );
  72. }
  73. private void HandleKeyboardPan()
  74. {
  75. Vector2 input = MazeInput.ReadWasdAxis();
  76. if (input.sqrMagnitude <= 0f) return;
  77. _focusTarget = null; // manual input takes over from auto-focus
  78. transform.position += new Vector3(input.x, input.y, 0f) * (panSpeed * Time.deltaTime);
  79. }
  80. private void HandleDrag()
  81. {
  82. bool middleHeldNow = MazeInput.IsMiddleMouseHeld();
  83. Vector2 pointerNow = MazeInput.GetPointerScreenPosition();
  84. if (middleHeldNow && !_isDragging)
  85. {
  86. _isDragging = true;
  87. _lastPointerScreenPos = pointerNow;
  88. return;
  89. }
  90. if (middleHeldNow)
  91. {
  92. Vector2 screenDelta = pointerNow - _lastPointerScreenPos;
  93. if (screenDelta.sqrMagnitude > 0f)
  94. {
  95. _focusTarget = null; // manual input takes over from auto-focus
  96. Vector2 worldDelta = ScreenDeltaToWorldDelta(screenDelta);
  97. transform.position -= new Vector3(worldDelta.x, worldDelta.y, 0f);
  98. }
  99. _lastPointerScreenPos = pointerNow;
  100. }
  101. else
  102. {
  103. _isDragging = false;
  104. }
  105. }
  106. private void HandleAutoFocus()
  107. {
  108. if (!_focusTarget.HasValue) return;
  109. Vector2 current = transform.position;
  110. Vector2 target = _focusTarget.Value;
  111. // Framerate-independent exponential smoothing rather than a constant-speed move,
  112. // so the glide eases in and settles instead of arriving abruptly.
  113. float t = 1f - Mathf.Exp(-focusSharpness * Time.deltaTime);
  114. Vector2 next = Vector2.Lerp(current, target, t);
  115. transform.position = new Vector3(next.x, next.y, transform.position.z);
  116. if (Vector2.Distance(next, target) < 0.01f)
  117. _focusTarget = null;
  118. }
  119. private Vector2 ScreenDeltaToWorldDelta(Vector2 screenDelta)
  120. {
  121. if (_camera == null || Screen.height <= 0) return Vector2.zero;
  122. float worldUnitsPerPixel;
  123. if (_camera.orthographic)
  124. {
  125. worldUnitsPerPixel = (_camera.orthographicSize * 2f) / Screen.height;
  126. }
  127. else
  128. {
  129. // Rooms sit on the z = 0 plane; use the camera's distance to it as the
  130. // reference plane for converting a screen-space drag into world units.
  131. float distance = Mathf.Abs(transform.position.z);
  132. worldUnitsPerPixel = (2f * distance * Mathf.Tan(_camera.fieldOfView * 0.5f * Mathf.Deg2Rad)) / Screen.height;
  133. }
  134. return screenDelta * worldUnitsPerPixel * dragSensitivity;
  135. }
  136. }
  137. }