using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.InputSystem; using UnityEngine.InputSystem.EnhancedTouch; using UnityEngine.InputSystem.OnScreen; using UnityEngine.InputSystem.Layouts; using EnhancedTouchSupport = UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport; namespace DeviantMobile.Input { // Variant that targets actions directly instead of raw control paths. [RequireComponent(typeof(RectTransform))] public class MobileControlManagerAction : MonoBehaviour, IPointerDownHandler, IPointerUpHandler { [Header("Platform / Visibility")] [Tooltip("If true, on-screen controls will be visible when running in the Editor (useful for testing). If false, controls are hidden in editor/desktop builds and only shown on mobile platforms.")] [SerializeField] private bool allowShowInEditor = false; // Helper to determine whether on-screen controls should be active for the current platform. private bool ShouldShowOnScreenControls() { if (Application.platform == RuntimePlatform.Android || Application.platform == RuntimePlatform.IPhonePlayer) return true; if (Application.isEditor && allowShowInEditor) return true; return false; } [System.Serializable] public class ActionBinding { [Tooltip("InputActionReference to trigger for this tap count. Leave empty to ignore.")] public InputActionReference action; [Tooltip("Optional label for clarity only (not used at runtime)")] public string label; } [Header("Tap to Action Bindings (index 0=single, 1=double, 2=triple, 3=quad)")] public ActionBinding[] actions = new ActionBinding[4]; [Header("Tap Settings")] [Tooltip("Max delay between taps to register as a multi-tap (seconds)")] [Range(0.1f, 0.6f)] public float multiTapWindow = 0.3f; [Tooltip("How long to hold the virtual press (seconds)")] [Range(0.01f, 0.2f)] public float pressDuration = 0.05f; private int tapCount = 0; private float lastTapTime = -1f; private Coroutine finalizeCoroutine; // Internally we build OnScreenPulseControl(s) that point at the bound actions' control paths. private readonly List _pulsers = new List(4); void Awake() { BuildOrRefreshPulsers(); // Ensure visibility matches platform (do this early so UI is correct in hierarchy) gameObject.SetActive(ShouldShowOnScreenControls()); } void OnEnable() { // Ensure EnhancedTouch is available so Unity's UI and On-Screen controls receive multi-touch data. EnhancedTouchSupport.Enable(); BuildOrRefreshPulsers(); gameObject.SetActive(ShouldShowOnScreenControls()); } void OnDisable() { // Balance the Enable call; safe to invoke even if already disabled elsewhere. EnhancedTouchSupport.Disable(); } public void OnPointerDown(PointerEventData eventData) { } public void OnPointerUp(PointerEventData eventData) { var now = Time.unscaledTime; if (lastTapTime > 0 && (now - lastTapTime) <= multiTapWindow) tapCount++; else tapCount = 1; lastTapTime = now; if (finalizeCoroutine != null) StopCoroutine(finalizeCoroutine); finalizeCoroutine = StartCoroutine(FinalizeTapAfterWindow()); } private IEnumerator FinalizeTapAfterWindow() { yield return new WaitForSecondsRealtime(multiTapWindow); TriggerForTapCount(tapCount); tapCount = 0; lastTapTime = -1f; finalizeCoroutine = null; } private void TriggerForTapCount(int taps) { int index = Mathf.Clamp(taps, 1, 4) - 1; if (index >= _pulsers.Count) return; var pulser = _pulsers[index]; if (pulser == null) return; pulser.Pulse(1f, pressDuration); } private void BuildOrRefreshPulsers() { _pulsers.Clear(); foreach (var existing in GetComponents()) { if (Application.isPlaying) Destroy(existing); else DestroyImmediate(existing); } // If actions not set, nothing to do. if (actions == null) return; // We resolve each InputActionReference to a suitable control path. for (int i = 0; i < Mathf.Min(4, actions.Length); i++) { var binding = actions[i]; if (binding == null || binding.action == null || binding.action.action == null) { _pulsers.Add(null); continue; } // Determine a control path. Prefer the first non-empty binding path. string path = ResolvePreferredControlPath(binding.action.action); if (string.IsNullOrWhiteSpace(path)) { _pulsers.Add(null); continue; } var pulser = gameObject.AddComponent(); // Set the control path resolved from the InputAction. // In the InputAction asset, ensure the action is bound to a control that matches this path // (for example a Button control on a touch/gamepad). The OnScreenPulseControl will send // press/release values to that control path, which the InputSystem action can receive. pulser.controlPath = path; _pulsers.Add(pulser); } } private static string ResolvePreferredControlPath(InputAction action) { // Try active controls if the action is enabled if (action.enabled && action.controls.Count > 0) { // Use the first control's path return action.controls[0].path; } // Fallback to the first binding that has a path foreach (var b in action.bindings) { if (!string.IsNullOrWhiteSpace(b.effectivePath)) return b.effectivePath; if (!string.IsNullOrWhiteSpace(b.path)) return b.path; } return null; } #if UNITY_EDITOR void OnValidate() { if (actions == null || actions.Length != 4) { var old = actions; actions = new ActionBinding[4]; if (old != null) { for (int i = 0; i < Mathf.Min(4, old.Length); i++) actions[i] = old[i]; } } if (Application.isPlaying == false) UnityEditor.EditorApplication.delayCall += () => { if (this != null) BuildOrRefreshPulsers(); }; } #endif } }