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;
}
}
}