MazeCameraController.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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 = 1.5f;
  15. [SerializeField] private float minOrthographicSize = 2f;
  16. [SerializeField] private float maxOrthographicSize = 60f;
  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. HandleScrollZoom();
  59. HandleAutoFocus();
  60. }
  61. private void HandleScrollZoom()
  62. {
  63. if (_camera == null || !_camera.orthographic) return;
  64. float scroll = MazeInput.GetScrollDelta();
  65. if (Mathf.Approximately(scroll, 0f)) return;
  66. _camera.orthographicSize = Mathf.Clamp(
  67. _camera.orthographicSize - scroll * zoomSpeed,
  68. minOrthographicSize, maxOrthographicSize);
  69. }
  70. private void HandleKeyboardPan()
  71. {
  72. Vector2 input = MazeInput.ReadWasdAxis();
  73. if (input.sqrMagnitude <= 0f) return;
  74. _focusTarget = null; // manual input takes over from auto-focus
  75. transform.position += new Vector3(input.x, input.y, 0f) * (panSpeed * Time.deltaTime);
  76. }
  77. private void HandleDrag()
  78. {
  79. bool middleHeldNow = MazeInput.IsMiddleMouseHeld();
  80. Vector2 pointerNow = MazeInput.GetPointerScreenPosition();
  81. if (middleHeldNow && !_isDragging)
  82. {
  83. _isDragging = true;
  84. _lastPointerScreenPos = pointerNow;
  85. return;
  86. }
  87. if (middleHeldNow)
  88. {
  89. Vector2 screenDelta = pointerNow - _lastPointerScreenPos;
  90. if (screenDelta.sqrMagnitude > 0f)
  91. {
  92. _focusTarget = null; // manual input takes over from auto-focus
  93. Vector2 worldDelta = ScreenDeltaToWorldDelta(screenDelta);
  94. transform.position -= new Vector3(worldDelta.x, worldDelta.y, 0f);
  95. }
  96. _lastPointerScreenPos = pointerNow;
  97. }
  98. else
  99. {
  100. _isDragging = false;
  101. }
  102. }
  103. private void HandleAutoFocus()
  104. {
  105. if (!_focusTarget.HasValue) return;
  106. Vector2 current = transform.position;
  107. Vector2 target = _focusTarget.Value;
  108. // Framerate-independent exponential smoothing rather than a constant-speed move,
  109. // so the glide eases in and settles instead of arriving abruptly.
  110. float t = 1f - Mathf.Exp(-focusSharpness * Time.deltaTime);
  111. Vector2 next = Vector2.Lerp(current, target, t);
  112. transform.position = new Vector3(next.x, next.y, transform.position.z);
  113. if (Vector2.Distance(next, target) < 0.01f)
  114. _focusTarget = null;
  115. }
  116. private Vector2 ScreenDeltaToWorldDelta(Vector2 screenDelta)
  117. {
  118. if (_camera == null || Screen.height <= 0) return Vector2.zero;
  119. float worldUnitsPerPixel;
  120. if (_camera.orthographic)
  121. {
  122. worldUnitsPerPixel = (_camera.orthographicSize * 2f) / Screen.height;
  123. }
  124. else
  125. {
  126. // Rooms sit on the z = 0 plane; use the camera's distance to it as the
  127. // reference plane for converting a screen-space drag into world units.
  128. float distance = Mathf.Abs(transform.position.z);
  129. worldUnitsPerPixel = (2f * distance * Mathf.Tan(_camera.fieldOfView * 0.5f * Mathf.Deg2Rad)) / Screen.height;
  130. }
  131. return screenDelta * worldUnitsPerPixel * dragSensitivity;
  132. }
  133. }
  134. }