using UnityEngine;
namespace MazeGen
{
///
/// Top-down camera rig: smoothly glides to focus on newly entered rooms, and can be
/// freely panned with WASD or by holding the middle mouse button and dragging. Any
/// manual pan input cancels whatever auto-focus is currently in progress.
///
public class MazeCameraController : MonoBehaviour
{
[SerializeField] private float panSpeed = 8f;
[SerializeField] private float focusSharpness = 6f;
[SerializeField] private float dragSensitivity = 1f;
[SerializeField] private float zoomSpeed = 1.5f;
[SerializeField] private float minOrthographicSize = 2f;
[SerializeField] private float maxOrthographicSize = 60f;
private Camera _camera;
private Vector2? _focusTarget;
private Vector2 _lastPointerScreenPos;
private bool _isDragging;
private void Awake()
{
_camera = GetComponent();
if (_camera == null) _camera = Camera.main;
}
/// Smoothly glides the camera to look at worldPosition.
public void FocusOn(Vector2 worldPosition)
{
_focusTarget = worldPosition;
}
/// Instantly moves the camera to worldPosition, no smoothing.
public void SnapTo(Vector2 worldPosition)
{
_focusTarget = null;
transform.position = new Vector3(worldPosition.x, worldPosition.y, transform.position.z);
}
///
/// Instantly repositions and (if orthographic) zooms out/in so the whole of `bounds`
/// is visible, e.g. for a "zoom to fit maze" button. Cancels any in-progress
/// auto-focus and does not itself start following anything.
///
public void FrameBounds(Bounds bounds, float padding = 1.2f)
{
_focusTarget = null;
Vector2 center = bounds.center;
transform.position = new Vector3(center.x, center.y, transform.position.z);
if (_camera != null && _camera.orthographic)
{
float aspect = Mathf.Max(_camera.aspect, 0.01f);
float requiredHalfHeight = Mathf.Max(bounds.extents.y, bounds.extents.x / aspect);
_camera.orthographicSize = Mathf.Max(requiredHalfHeight * padding, 1f);
}
}
private void Update()
{
HandleDrag();
HandleKeyboardPan();
HandleScrollZoom();
HandleAutoFocus();
}
private void HandleScrollZoom()
{
if (_camera == null || !_camera.orthographic) return;
float scroll = MazeInput.GetScrollDelta();
if (Mathf.Approximately(scroll, 0f)) return;
_camera.orthographicSize = Mathf.Clamp(
_camera.orthographicSize - scroll * zoomSpeed,
minOrthographicSize, maxOrthographicSize);
}
private void HandleKeyboardPan()
{
Vector2 input = MazeInput.ReadWasdAxis();
if (input.sqrMagnitude <= 0f) return;
_focusTarget = null; // manual input takes over from auto-focus
transform.position += new Vector3(input.x, input.y, 0f) * (panSpeed * Time.deltaTime);
}
private void HandleDrag()
{
bool middleHeldNow = MazeInput.IsMiddleMouseHeld();
Vector2 pointerNow = MazeInput.GetPointerScreenPosition();
if (middleHeldNow && !_isDragging)
{
_isDragging = true;
_lastPointerScreenPos = pointerNow;
return;
}
if (middleHeldNow)
{
Vector2 screenDelta = pointerNow - _lastPointerScreenPos;
if (screenDelta.sqrMagnitude > 0f)
{
_focusTarget = null; // manual input takes over from auto-focus
Vector2 worldDelta = ScreenDeltaToWorldDelta(screenDelta);
transform.position -= new Vector3(worldDelta.x, worldDelta.y, 0f);
}
_lastPointerScreenPos = pointerNow;
}
else
{
_isDragging = false;
}
}
private void HandleAutoFocus()
{
if (!_focusTarget.HasValue) return;
Vector2 current = transform.position;
Vector2 target = _focusTarget.Value;
// Framerate-independent exponential smoothing rather than a constant-speed move,
// so the glide eases in and settles instead of arriving abruptly.
float t = 1f - Mathf.Exp(-focusSharpness * Time.deltaTime);
Vector2 next = Vector2.Lerp(current, target, t);
transform.position = new Vector3(next.x, next.y, transform.position.z);
if (Vector2.Distance(next, target) < 0.01f)
_focusTarget = null;
}
private Vector2 ScreenDeltaToWorldDelta(Vector2 screenDelta)
{
if (_camera == null || Screen.height <= 0) return Vector2.zero;
float worldUnitsPerPixel;
if (_camera.orthographic)
{
worldUnitsPerPixel = (_camera.orthographicSize * 2f) / Screen.height;
}
else
{
// Rooms sit on the z = 0 plane; use the camera's distance to it as the
// reference plane for converting a screen-space drag into world units.
float distance = Mathf.Abs(transform.position.z);
worldUnitsPerPixel = (2f * distance * Mathf.Tan(_camera.fieldOfView * 0.5f * Mathf.Deg2Rad)) / Screen.height;
}
return screenDelta * worldUnitsPerPixel * dragSensitivity;
}
}
}