MazeCameraController.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. using UnityEngine;
  2. using UnityEngine.InputSystem;
  3. /// <summary>
  4. /// Simple camera movement controller for map navigation.
  5. /// Supports WASD/arrow keys, right-click drag, and Q/E or mouse wheel zoom.
  6. /// </summary>
  7. [RequireComponent(typeof(Camera))]
  8. public class MazeCameraController : MonoBehaviour
  9. {
  10. [Header("Movement")]
  11. [SerializeField] private float moveSpeed = 20f;
  12. [SerializeField] private float dragSpeed = 1f;
  13. [Header("Zoom")]
  14. [SerializeField] private float zoomSpeed = 25f;
  15. [SerializeField] private float zoomMin = 5f;
  16. [SerializeField] private float zoomMax = 120f;
  17. [Header("Optional Bounds")]
  18. [SerializeField] private bool useBounds = false;
  19. [SerializeField] private Vector2 minBounds = new Vector2(-100f, -100f);
  20. [SerializeField] private Vector2 maxBounds = new Vector2(100f, 100f);
  21. private Camera cameraComponent;
  22. private Vector3 targetPosition;
  23. private Vector3 lastMousePosition;
  24. private bool isDragging;
  25. // Smooth zoom and pan to location
  26. private bool isSmoothlyCentering = false;
  27. private Vector3 smoothTargetPosition;
  28. private float smoothTargetZoom;
  29. private float smoothCenteringSpeed = 5f;
  30. private float smoothZoomSpeed = 3f;
  31. private void Awake()
  32. {
  33. cameraComponent = GetComponent<Camera>();
  34. targetPosition = transform.position;
  35. }
  36. private void Update()
  37. {
  38. if (isSmoothlyCentering)
  39. {
  40. UpdateSmoothCentering();
  41. }
  42. HandleKeyboardMovement();
  43. HandleMouseDrag();
  44. HandleZoom();
  45. ApplyPosition();
  46. }
  47. private void HandleKeyboardMovement()
  48. {
  49. var keyboard = Keyboard.current;
  50. if (keyboard == null)
  51. {
  52. return;
  53. }
  54. float horizontal = 0f;
  55. if (keyboard.aKey.isPressed || keyboard.leftArrowKey.isPressed)
  56. {
  57. horizontal -= 1f;
  58. }
  59. if (keyboard.dKey.isPressed || keyboard.rightArrowKey.isPressed)
  60. {
  61. horizontal += 1f;
  62. }
  63. float vertical = 0f;
  64. if (keyboard.sKey.isPressed || keyboard.downArrowKey.isPressed)
  65. {
  66. vertical -= 1f;
  67. }
  68. if (keyboard.wKey.isPressed || keyboard.upArrowKey.isPressed)
  69. {
  70. vertical += 1f;
  71. }
  72. if (horizontal != 0f || vertical != 0f)
  73. {
  74. isSmoothlyCentering = false; // Interrupt smooth zoom on manual movement
  75. Vector3 right = transform.right;
  76. Vector3 forward = Vector3.Cross(transform.right, Vector3.up).normalized;
  77. Vector3 worldMove = (right * horizontal + forward * vertical).normalized;
  78. // Use unscaledDeltaTime so camera movement is unaffected by simulation speed/pause
  79. targetPosition += worldMove * moveSpeed * Time.unscaledDeltaTime;
  80. }
  81. if (keyboard.qKey.isPressed)
  82. {
  83. isSmoothlyCentering = false; // Interrupt smooth zoom on manual zoom
  84. Zoom(-zoomSpeed * Time.unscaledDeltaTime);
  85. }
  86. else if (keyboard.eKey.isPressed)
  87. {
  88. isSmoothlyCentering = false; // Interrupt smooth zoom on manual zoom
  89. Zoom(zoomSpeed * Time.unscaledDeltaTime);
  90. }
  91. }
  92. private void HandleMouseDrag()
  93. {
  94. var mouse = Mouse.current;
  95. if (mouse == null)
  96. {
  97. return;
  98. }
  99. if (mouse.rightButton.wasPressedThisFrame)
  100. {
  101. isSmoothlyCentering = false; // Interrupt smooth zoom on manual drag
  102. isDragging = true;
  103. Vector2 position = mouse.position.ReadValue();
  104. lastMousePosition = new Vector3(position.x, position.y, 0f);
  105. }
  106. if (mouse.rightButton.wasReleasedThisFrame)
  107. {
  108. isDragging = false;
  109. }
  110. if (isDragging && mouse.rightButton.isPressed)
  111. {
  112. Vector2 position = mouse.position.ReadValue();
  113. Vector3 currentMousePosition = new Vector3(position.x, position.y, 0f);
  114. if (currentMousePosition != lastMousePosition)
  115. {
  116. Vector3 dragDelta = GetDragWorldDelta(lastMousePosition, currentMousePosition);
  117. targetPosition += dragDelta * dragSpeed;
  118. lastMousePosition = currentMousePosition;
  119. }
  120. }
  121. }
  122. private void HandleZoom()
  123. {
  124. var mouse = Mouse.current;
  125. if (mouse != null)
  126. {
  127. float scroll = mouse.scroll.ReadValue().y;
  128. if (scroll != 0f)
  129. {
  130. isSmoothlyCentering = false; // Interrupt smooth zoom on manual zoom
  131. Zoom(-scroll * zoomSpeed * Time.unscaledDeltaTime);
  132. }
  133. }
  134. }
  135. private void Zoom(float amount)
  136. {
  137. if (cameraComponent.orthographic)
  138. {
  139. cameraComponent.orthographicSize = Mathf.Clamp(cameraComponent.orthographicSize + amount, zoomMin, zoomMax);
  140. }
  141. else
  142. {
  143. cameraComponent.fieldOfView = Mathf.Clamp(cameraComponent.fieldOfView + amount, zoomMin, zoomMax);
  144. }
  145. }
  146. private Vector3 GetDragWorldDelta(Vector3 previousScreenPosition, Vector3 currentScreenPosition)
  147. {
  148. Plane dragPlane = new Plane(-transform.forward, transform.position);
  149. Ray previousRay = cameraComponent.ScreenPointToRay(previousScreenPosition);
  150. Ray currentRay = cameraComponent.ScreenPointToRay(currentScreenPosition);
  151. if (dragPlane.Raycast(previousRay, out float previousDistance) && dragPlane.Raycast(currentRay, out float currentDistance))
  152. {
  153. Vector3 previousWorld = previousRay.GetPoint(previousDistance);
  154. Vector3 currentWorld = currentRay.GetPoint(currentDistance);
  155. return previousWorld - currentWorld;
  156. }
  157. return Vector3.zero;
  158. }
  159. private void ApplyPosition()
  160. {
  161. Vector3 newPosition = targetPosition;
  162. if (useBounds)
  163. {
  164. newPosition.x = Mathf.Clamp(newPosition.x, minBounds.x, maxBounds.x);
  165. newPosition.z = Mathf.Clamp(newPosition.z, minBounds.y, maxBounds.y);
  166. }
  167. transform.position = newPosition;
  168. }
  169. /// <summary>
  170. /// Smoothly zooms and pans the camera to a specific room
  171. /// </summary>
  172. /// <param name="room">The room to zoom to</param>
  173. /// <param name="zoomPadding">Multiplier for room size (1.0 = exactly room size, 1.3 = 30% larger)</param>
  174. /// <param name="transitionDuration">Time to complete the zoom/pan in seconds</param>
  175. public void ZoomToRoom(MazeRoom room, float zoomPadding = 1.3f, float transitionDuration = 0.5f)
  176. {
  177. if (room == null) return;
  178. // Stop any previous zoom to prevent stacking
  179. isSmoothlyCentering = false;
  180. // Calculate room center in world space (XZ plane only - don't change camera height Y)
  181. Vector2Int roomCenter = room.GetCenter();
  182. Vector3 roomCenterXZ = new Vector3(roomCenter.x + 0.5f, targetPosition.y, roomCenter.y + 0.5f); // Keep Y unchanged
  183. // Calculate required zoom level to fit room with padding
  184. float roomWidth = room.Width;
  185. float roomHeight = room.Height;
  186. float maxRoomDim = Mathf.Max(roomWidth, roomHeight) * zoomPadding;
  187. // Adjust for camera aspect ratio
  188. float requiredZoom = maxRoomDim / (2f * Mathf.Tan(cameraComponent.fieldOfView * Mathf.Deg2Rad / 2f));
  189. if (cameraComponent.orthographic)
  190. {
  191. requiredZoom = maxRoomDim * 0.5f;
  192. }
  193. // Set smooth transition targets
  194. smoothTargetPosition = roomCenterXZ; // Pan to room center on XZ plane only
  195. smoothTargetZoom = Mathf.Clamp(requiredZoom, zoomMin, zoomMax);
  196. smoothCenteringSpeed = 1f / transitionDuration;
  197. isSmoothlyCentering = true;
  198. }
  199. /// <summary>
  200. /// Updates the smooth camera transition toward target position and zoom
  201. /// </summary>
  202. private void UpdateSmoothCentering()
  203. {
  204. // Smooth pan to target position - use unscaledDeltaTime so it works while paused/slow
  205. float posDist = Vector3.Distance(targetPosition, smoothTargetPosition);
  206. if (posDist > 0.01f)
  207. {
  208. targetPosition = Vector3.Lerp(targetPosition, smoothTargetPosition, smoothCenteringSpeed * Time.unscaledDeltaTime);
  209. }
  210. else
  211. {
  212. targetPosition = smoothTargetPosition; // Snap to target
  213. }
  214. // Smooth zoom to target
  215. float currentZoom = cameraComponent.orthographic ? cameraComponent.orthographicSize : cameraComponent.fieldOfView;
  216. float zoomDist = Mathf.Abs(currentZoom - smoothTargetZoom);
  217. if (zoomDist > 0.01f)
  218. {
  219. if (cameraComponent.orthographic)
  220. {
  221. cameraComponent.orthographicSize = Mathf.Lerp(cameraComponent.orthographicSize, smoothTargetZoom, smoothZoomSpeed * Time.unscaledDeltaTime);
  222. }
  223. else
  224. {
  225. cameraComponent.fieldOfView = Mathf.Lerp(cameraComponent.fieldOfView, smoothTargetZoom, smoothZoomSpeed * Time.unscaledDeltaTime);
  226. }
  227. }
  228. else
  229. {
  230. // Snap to target zoom
  231. if (cameraComponent.orthographic)
  232. {
  233. cameraComponent.orthographicSize = smoothTargetZoom;
  234. }
  235. else
  236. {
  237. cameraComponent.fieldOfView = smoothTargetZoom;
  238. }
  239. }
  240. // Check if we've reached both targets
  241. posDist = Vector3.Distance(targetPosition, smoothTargetPosition);
  242. currentZoom = cameraComponent.orthographic ? cameraComponent.orthographicSize : cameraComponent.fieldOfView;
  243. zoomDist = Mathf.Abs(currentZoom - smoothTargetZoom);
  244. if (posDist < 0.01f && zoomDist < 0.01f)
  245. {
  246. isSmoothlyCentering = false;
  247. }
  248. }
  249. }