From 69fe6cbffdcc83c77d45fa3ab6456c379462fd01 Mon Sep 17 00:00:00 2001 From: KoalaTrapLord Date: Wed, 18 Mar 2026 15:18:09 -0400 Subject: [PATCH] Add MobileInputHandler for touch controls, safe area, responsive UI (closes #58) --- .../Scripts/Services/MobileInputHandler.cs | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 unity-client/Assets/Scripts/Services/MobileInputHandler.cs diff --git a/unity-client/Assets/Scripts/Services/MobileInputHandler.cs b/unity-client/Assets/Scripts/Services/MobileInputHandler.cs new file mode 100644 index 0000000..01c1cf8 --- /dev/null +++ b/unity-client/Assets/Scripts/Services/MobileInputHandler.cs @@ -0,0 +1,167 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace CommanderAILab.Services +{ + /// + /// Mobile-specific input handler and responsive UI utilities. + /// Handles: touch controls, pinch-to-zoom, swipe gestures, + /// safe area insets, orientation changes, and responsive layout. + /// + public class MobileInputHandler : MonoBehaviour + { + public static MobileInputHandler Instance { get; private set; } + + [Header("Touch Settings")] + [SerializeField] private float pinchZoomSpeed = 0.01f; + [SerializeField] private float swipeThreshold = 50f; + [SerializeField] private float longPressTime = 0.5f; + + [Header("Safe Area")] + [SerializeField] private RectTransform safeAreaRect; + + // Gesture state + private float _touchStartTime; + private Vector2 _touchStartPos; + private bool _isTouching; + private float _lastPinchDistance; + + // Events + public event System.Action OnPinchZoom; // delta + public event System.Action OnSwipe; // direction + public event System.Action OnLongPress; // position + public event System.Action OnTap; // position + + private void Awake() + { + if (Instance != null) { Destroy(gameObject); return; } + Instance = this; + DontDestroyOnLoad(gameObject); + } + + private void Start() + { + ApplySafeArea(); + SetTargetFrameRate(); + } + + private void Update() + { + if (!IsMobilePlatform()) return; + + HandleTouchInput(); + HandlePinchZoom(); + + // Handle orientation changes + if (Screen.orientation != _lastOrientation) + { + _lastOrientation = Screen.orientation; + ApplySafeArea(); + } + } + + private ScreenOrientation _lastOrientation; + + // ── Touch Gestures ───────────────────────────────────────────── + private void HandleTouchInput() + { + if (Input.touchCount != 1) return; + Touch touch = Input.GetTouch(0); + + switch (touch.phase) + { + case TouchPhase.Began: + _touchStartTime = Time.time; + _touchStartPos = touch.position; + _isTouching = true; + break; + + case TouchPhase.Ended: + if (!_isTouching) break; + _isTouching = false; + float duration = Time.time - _touchStartTime; + Vector2 delta = touch.position - _touchStartPos; + + if (duration >= longPressTime && delta.magnitude < swipeThreshold) + { + OnLongPress?.Invoke(touch.position); + } + else if (delta.magnitude >= swipeThreshold) + { + OnSwipe?.Invoke(delta.normalized); + } + else + { + OnTap?.Invoke(touch.position); + } + break; + + case TouchPhase.Canceled: + _isTouching = false; + break; + } + } + + private void HandlePinchZoom() + { + if (Input.touchCount != 2) { _lastPinchDistance = 0; return; } + + Touch t0 = Input.GetTouch(0); + Touch t1 = Input.GetTouch(1); + float dist = Vector2.Distance(t0.position, t1.position); + + if (_lastPinchDistance > 0) + { + float delta = (dist - _lastPinchDistance) * pinchZoomSpeed; + OnPinchZoom?.Invoke(delta); + } + _lastPinchDistance = dist; + } + + // ── Safe Area (notch / rounded corners) ───────────────────── + private void ApplySafeArea() + { + if (safeAreaRect == null) return; + Rect safe = Screen.safeArea; + Vector2 anchorMin = safe.position; + Vector2 anchorMax = safe.position + safe.size; + anchorMin.x /= Screen.width; + anchorMin.y /= Screen.height; + anchorMax.x /= Screen.width; + anchorMax.y /= Screen.height; + safeAreaRect.anchorMin = anchorMin; + safeAreaRect.anchorMax = anchorMax; + } + + // ── Platform Config ────────────────────────────────────────── + private void SetTargetFrameRate() + { +#if UNITY_IOS || UNITY_ANDROID + Application.targetFrameRate = 60; + Screen.sleepTimeout = SleepTimeout.NeverSleep; +#endif + } + + public static bool IsMobilePlatform() + { +#if UNITY_IOS || UNITY_ANDROID + return true; +#else + return false; +#endif + } + + /// Returns responsive font size based on screen DPI. + public static int ResponsiveFontSize(int baseSizeAt96DPI) + { + float dpi = Screen.dpi > 0 ? Screen.dpi : 96f; + return Mathf.RoundToInt(baseSizeAt96DPI * (dpi / 96f) * 0.6f); + } + + /// Returns true if device is in portrait orientation. + public static bool IsPortrait() + { + return Screen.height > Screen.width; + } + } +}