using UnityEngine; using UnityEngine.InputSystem; /// /// Simple camera movement controller for map navigation. /// Supports WASD/arrow keys, right-click drag, and Q/E or mouse wheel zoom. /// [RequireComponent(typeof(Camera))] public class MazeCameraController : MonoBehaviour { [Header("Movement")] [SerializeField] private float moveSpeed = 20f; [SerializeField] private float dragSpeed = 1f; [Header("Zoom")] [SerializeField] private float zoomSpeed = 25f; [SerializeField] private float zoomMin = 5f; [SerializeField] private float zoomMax = 120f; [Header("Optional Bounds")] [SerializeField] private bool useBounds = false; [SerializeField] private Vector2 minBounds = new Vector2(-100f, -100f); [SerializeField] private Vector2 maxBounds = new Vector2(100f, 100f); private Camera cameraComponent; private Vector3 targetPosition; private Vector3 lastMousePosition; private bool isDragging; // Smooth zoom and pan to location private bool isSmoothlyCentering = false; private Vector3 smoothTargetPosition; private float smoothTargetZoom; private float smoothCenteringSpeed = 5f; private float smoothZoomSpeed = 3f; private void Awake() { cameraComponent = GetComponent(); targetPosition = transform.position; } private void Update() { if (isSmoothlyCentering) { UpdateSmoothCentering(); } HandleKeyboardMovement(); HandleMouseDrag(); HandleZoom(); ApplyPosition(); } private void HandleKeyboardMovement() { var keyboard = Keyboard.current; if (keyboard == null) { return; } float horizontal = 0f; if (keyboard.aKey.isPressed || keyboard.leftArrowKey.isPressed) { horizontal -= 1f; } if (keyboard.dKey.isPressed || keyboard.rightArrowKey.isPressed) { horizontal += 1f; } float vertical = 0f; if (keyboard.sKey.isPressed || keyboard.downArrowKey.isPressed) { vertical -= 1f; } if (keyboard.wKey.isPressed || keyboard.upArrowKey.isPressed) { vertical += 1f; } if (horizontal != 0f || vertical != 0f) { isSmoothlyCentering = false; // Interrupt smooth zoom on manual movement Vector3 right = transform.right; Vector3 forward = Vector3.Cross(transform.right, Vector3.up).normalized; Vector3 worldMove = (right * horizontal + forward * vertical).normalized; // Use unscaledDeltaTime so camera movement is unaffected by simulation speed/pause targetPosition += worldMove * moveSpeed * Time.unscaledDeltaTime; } if (keyboard.qKey.isPressed) { isSmoothlyCentering = false; // Interrupt smooth zoom on manual zoom Zoom(-zoomSpeed * Time.unscaledDeltaTime); } else if (keyboard.eKey.isPressed) { isSmoothlyCentering = false; // Interrupt smooth zoom on manual zoom Zoom(zoomSpeed * Time.unscaledDeltaTime); } } private void HandleMouseDrag() { var mouse = Mouse.current; if (mouse == null) { return; } if (mouse.rightButton.wasPressedThisFrame) { isSmoothlyCentering = false; // Interrupt smooth zoom on manual drag isDragging = true; Vector2 position = mouse.position.ReadValue(); lastMousePosition = new Vector3(position.x, position.y, 0f); } if (mouse.rightButton.wasReleasedThisFrame) { isDragging = false; } if (isDragging && mouse.rightButton.isPressed) { Vector2 position = mouse.position.ReadValue(); Vector3 currentMousePosition = new Vector3(position.x, position.y, 0f); if (currentMousePosition != lastMousePosition) { Vector3 dragDelta = GetDragWorldDelta(lastMousePosition, currentMousePosition); targetPosition += dragDelta * dragSpeed; lastMousePosition = currentMousePosition; } } } private void HandleZoom() { var mouse = Mouse.current; if (mouse != null) { float scroll = mouse.scroll.ReadValue().y; if (scroll != 0f) { isSmoothlyCentering = false; // Interrupt smooth zoom on manual zoom Zoom(-scroll * zoomSpeed * Time.unscaledDeltaTime); } } } private void Zoom(float amount) { if (cameraComponent.orthographic) { cameraComponent.orthographicSize = Mathf.Clamp(cameraComponent.orthographicSize + amount, zoomMin, zoomMax); } else { cameraComponent.fieldOfView = Mathf.Clamp(cameraComponent.fieldOfView + amount, zoomMin, zoomMax); } } private Vector3 GetDragWorldDelta(Vector3 previousScreenPosition, Vector3 currentScreenPosition) { Plane dragPlane = new Plane(-transform.forward, transform.position); Ray previousRay = cameraComponent.ScreenPointToRay(previousScreenPosition); Ray currentRay = cameraComponent.ScreenPointToRay(currentScreenPosition); if (dragPlane.Raycast(previousRay, out float previousDistance) && dragPlane.Raycast(currentRay, out float currentDistance)) { Vector3 previousWorld = previousRay.GetPoint(previousDistance); Vector3 currentWorld = currentRay.GetPoint(currentDistance); return previousWorld - currentWorld; } return Vector3.zero; } private void ApplyPosition() { Vector3 newPosition = targetPosition; if (useBounds) { newPosition.x = Mathf.Clamp(newPosition.x, minBounds.x, maxBounds.x); newPosition.z = Mathf.Clamp(newPosition.z, minBounds.y, maxBounds.y); } transform.position = newPosition; } /// /// Smoothly zooms and pans the camera to a specific room /// /// The room to zoom to /// Multiplier for room size (1.0 = exactly room size, 1.3 = 30% larger) /// Time to complete the zoom/pan in seconds public void ZoomToRoom(MazeRoom room, float zoomPadding = 1.3f, float transitionDuration = 0.5f) { if (room == null) return; // Stop any previous zoom to prevent stacking isSmoothlyCentering = false; // Calculate room center in world space (XZ plane only - don't change camera height Y) Vector2Int roomCenter = room.GetCenter(); Vector3 roomCenterXZ = new Vector3(roomCenter.x + 0.5f, targetPosition.y, roomCenter.y + 0.5f); // Keep Y unchanged // Calculate required zoom level to fit room with padding float roomWidth = room.Width; float roomHeight = room.Height; float maxRoomDim = Mathf.Max(roomWidth, roomHeight) * zoomPadding; // Adjust for camera aspect ratio float requiredZoom = maxRoomDim / (2f * Mathf.Tan(cameraComponent.fieldOfView * Mathf.Deg2Rad / 2f)); if (cameraComponent.orthographic) { requiredZoom = maxRoomDim * 0.5f; } // Set smooth transition targets smoothTargetPosition = roomCenterXZ; // Pan to room center on XZ plane only smoothTargetZoom = Mathf.Clamp(requiredZoom, zoomMin, zoomMax); smoothCenteringSpeed = 1f / transitionDuration; isSmoothlyCentering = true; } /// /// Updates the smooth camera transition toward target position and zoom /// private void UpdateSmoothCentering() { // Smooth pan to target position - use unscaledDeltaTime so it works while paused/slow float posDist = Vector3.Distance(targetPosition, smoothTargetPosition); if (posDist > 0.01f) { targetPosition = Vector3.Lerp(targetPosition, smoothTargetPosition, smoothCenteringSpeed * Time.unscaledDeltaTime); } else { targetPosition = smoothTargetPosition; // Snap to target } // Smooth zoom to target float currentZoom = cameraComponent.orthographic ? cameraComponent.orthographicSize : cameraComponent.fieldOfView; float zoomDist = Mathf.Abs(currentZoom - smoothTargetZoom); if (zoomDist > 0.01f) { if (cameraComponent.orthographic) { cameraComponent.orthographicSize = Mathf.Lerp(cameraComponent.orthographicSize, smoothTargetZoom, smoothZoomSpeed * Time.unscaledDeltaTime); } else { cameraComponent.fieldOfView = Mathf.Lerp(cameraComponent.fieldOfView, smoothTargetZoom, smoothZoomSpeed * Time.unscaledDeltaTime); } } else { // Snap to target zoom if (cameraComponent.orthographic) { cameraComponent.orthographicSize = smoothTargetZoom; } else { cameraComponent.fieldOfView = smoothTargetZoom; } } // Check if we've reached both targets posDist = Vector3.Distance(targetPosition, smoothTargetPosition); currentZoom = cameraComponent.orthographic ? cameraComponent.orthographicSize : cameraComponent.fieldOfView; zoomDist = Mathf.Abs(currentZoom - smoothTargetZoom); if (posDist < 0.01f && zoomDist < 0.01f) { isSmoothlyCentering = false; } } }