217 lines
8.4 KiB
C#
217 lines
8.4 KiB
C#
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
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<Camera>();
|
|
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);
|
|
}
|
|
}
|