chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
197
Assets/Scripts/Input/MobileControlManagerAction.cs
Normal file
197
Assets/Scripts/Input/MobileControlManagerAction.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user