Files
DeviantMobile-Rohan/Assets/Scripts/Input/MobileControlManagerAction.cs

198 lines
7.3 KiB
C#

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<OnScreenPulseControl> _pulsers = new List<OnScreenPulseControl>(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<OnScreenPulseControl>())
{
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<OnScreenPulseControl>();
// 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
}
}