using UnityEngine; /// /// Attach to Main Camera in Client.unity. /// Top-down perspective camera that follows the local player capsule. /// /// Features: /// • Auto-follow player when tracking (can be paused by dragging) /// • Single-finger touch drag (or mouse drag) to pan /// • Pinch gesture (or mouse scroll wheel) to zoom (changes camera height) /// • Double-tap anywhere to instantly recenter on player /// • Static Recenter() method called by the HUD recenter button /// public class MapCameraController : MonoBehaviour { // ── Singleton (weak — no DontDestroyOnLoad needed, camera lives in Client.unity) ── public static MapCameraController Instance { get; private set; } // ── Public API ──────────────────────────────────────────────────────────── public void SetTarget(GameObject target) { _target = target; } public void Recenter() { _isTracking = true; _resumeTimer = 0f; } // ── Tuning ──────────────────────────────────────────────────────────────── private const float FollowSmoothing = 8f; // lerp speed when tracking private const float DefaultHeight = 150f; // camera Y (metres above ground) private const float MinHeight = 30f; // closest zoom private const float MaxHeight = 350f; // furthest zoom private const float PinchZoomSens = 1.2f; // multiplier for pinch speed private const float ScrollZoomSens = 30f; // world-units per scroll tick private const float ResumeDelay = 3.5f; // s after drag ends before auto-tracking resumes private const float DoubleTapWindow = 0.32f; // s between taps to count as double private const float DragThreshold = 8f; // pixels moved before drag starts // ── Runtime state ───────────────────────────────────────────────────────── private Camera _cam; private GameObject _target; private float _currentHeight; private bool _isTracking = true; private float _resumeTimer; // Drag private bool _dragActive; private Vector2 _lastDragScreen; // Pinch private float _pinchStartDist = -1f; private float _pinchStartHeight; // Double-tap private int _tapCount; private float _tapTimer; // ── MonoBehaviour ───────────────────────────────────────────────────────── void Awake() { Instance = this; _cam = GetComponent(); if (_cam == null) { Debug.LogError("[MapCamera] No Camera component!"); return; } // Keep existing perspective mode — just ensure straight-down orientation transform.rotation = Quaternion.Euler(90f, 0f, 0f); _currentHeight = transform.position.y > 1f ? transform.position.y : DefaultHeight; transform.position = new Vector3(transform.position.x, _currentHeight, transform.position.z); } void OnEnable() { Instance = this; } void LateUpdate() { HandleInput(); FollowTarget(); } // ── Target following ────────────────────────────────────────────────────── void FollowTarget() { if (!_isTracking || _target == null) return; Vector3 tp = _target.transform.position; Vector3 dest = new Vector3(tp.x, _currentHeight, tp.z); transform.position = Vector3.Lerp(transform.position, dest, Time.deltaTime * FollowSmoothing); } // ── Input ───────────────────────────────────────────────────────────────── void HandleInput() { // Auto-resume tracking after a period of no dragging if (!_isTracking) { _resumeTimer += Time.deltaTime; if (_resumeTimer >= ResumeDelay) _isTracking = true; } // Double-tap timer _tapTimer += Time.deltaTime; if (_tapTimer > DoubleTapWindow) _tapCount = 0; int tc = Input.touchCount; if (tc == 2) { HandlePinch(); return; } _pinchStartDist = -1f; // reset pinch when not 2 fingers if (tc == 1) { Touch t = Input.GetTouch(0); switch (t.phase) { case TouchPhase.Began: OnPointerDown(t.position); break; case TouchPhase.Moved: case TouchPhase.Stationary: OnPointerDrag(t.position); break; case TouchPhase.Ended: case TouchPhase.Canceled: OnPointerUp(); break; } return; } // Mouse fallback (editor / desktop) if (Input.GetMouseButtonDown(0)) OnPointerDown(Input.mousePosition); else if (Input.GetMouseButton(0)) OnPointerDrag(Input.mousePosition); else if (Input.GetMouseButtonUp(0)) OnPointerUp(); float scroll = Input.GetAxis("Mouse ScrollWheel"); if (Mathf.Abs(scroll) > 0.001f) { _currentHeight = Mathf.Clamp(_currentHeight - scroll * ScrollZoomSens, MinHeight, MaxHeight); transform.position = new Vector3(transform.position.x, _currentHeight, transform.position.z); } } void OnPointerDown(Vector2 screenPos) { _lastDragScreen = screenPos; _dragActive = false; // Double-tap detection _tapCount++; _tapTimer = 0f; if (_tapCount >= 2) { _tapCount = 0; Recenter(); } } void OnPointerDrag(Vector2 screenPos) { Vector2 screenDelta = screenPos - _lastDragScreen; if (!_dragActive && screenDelta.magnitude > DragThreshold) { _dragActive = true; _isTracking = false; _resumeTimer = 0f; } if (_dragActive) { // Pan: move camera so that the world point under the finger stays fixed. // Because the camera faces straight down, we can use a simpler formula: // pixels → world = (camera height / focal length in pixels) ratio. // For perspective: visible half-height at ground = height * tan(fov/2) // world_per_pixel = 2 * height * tan(fov/2) / screenHeight float halfFovRad = _cam.fieldOfView * 0.5f * Mathf.Deg2Rad; float worldPerPixelY = 2f * _currentHeight * Mathf.Tan(halfFovRad) / Screen.height; float worldPerPixelX = worldPerPixelY * ((float)Screen.width / Screen.height); // Flip: dragging right moves world right (camera moves left) transform.position += new Vector3( -screenDelta.x * worldPerPixelX, 0f, -screenDelta.y * worldPerPixelY ); } _lastDragScreen = screenPos; } void OnPointerUp() { _dragActive = false; } // ── Pinch zoom ──────────────────────────────────────────────────────────── void HandlePinch() { Touch t0 = Input.GetTouch(0); Touch t1 = Input.GetTouch(1); if (t0.phase == TouchPhase.Began || t1.phase == TouchPhase.Began) { _pinchStartDist = Vector2.Distance(t0.position, t1.position); _pinchStartHeight = _currentHeight; return; } if (_pinchStartDist <= 0f) return; float currentDist = Vector2.Distance(t0.position, t1.position); if (currentDist < 1f) return; // Closer fingers = zoom in (lower height) float ratio = _pinchStartDist / currentDist; _currentHeight = Mathf.Clamp(_pinchStartHeight * ratio * PinchZoomSens, MinHeight, MaxHeight); transform.position = new Vector3(transform.position.x, _currentHeight, transform.position.z); } }