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; 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); } private void Update() { HandleDrag(); HandleKeyboardPan(); HandleAutoFocus(); } 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; } } }