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 { // Drag-based on-screen stick that writes a Vector2 to the Input System. // Bind your Move action to the same control path (e.g. /leftStick). [AddComponentMenu("Input/Mobile On-Screen Stick")] public class MobileOnScreenStick : OnScreenControl, IPointerDownHandler, IPointerUpHandler, IDragHandler { [Header("Platform / Visibility")] [Tooltip("If true, on-screen stick 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; } [Header("Control Path")] [Tooltip("Input System control path to write the Vector2 to. Commonly '/leftStick'.")] [SerializeField, InputControl(layout = "Vector2")] private string m_ControlPath = "/leftStick"; protected override string controlPathInternal { get => m_ControlPath; set => m_ControlPath = value; } [Header("UI References")] [SerializeField] private RectTransform background; // Joystick background area [SerializeField] private RectTransform handle; // Joystick handle/knob [Header("Behavior")] [Tooltip("Max radius in pixels the handle can travel from center.")] [SerializeField] private float maxRadius = 100f; [Tooltip("Deadzone as a fraction of radius (0..1).")] [Range(0f, 0.5f)] [SerializeField] private float deadZone = 0.1f; [Tooltip("Return handle to center on pointer up.")] [SerializeField] private bool snapBackOnRelease = true; private bool _pressed; private Vector2 _centerLocal; void Awake() { if (background == null) background = GetComponent(); if (handle == null && background != null && background.childCount > 0) handle = background.GetChild(0) as RectTransform; // Disable the UI for non-mobile platforms by default. gameObject.SetActive(ShouldShowOnScreenControls()); } void OnEnable() { // Ensure the Enhanced Touch pipeline is active so pointer/touch events map correctly to OnScreenControl. EnhancedTouchSupport.Enable(); } void OnDisable() { EnhancedTouchSupport.Disable(); } public void OnPointerDown(PointerEventData eventData) { _pressed = true; CacheCenter(); OnDrag(eventData); // update immediately } public void OnDrag(PointerEventData eventData) { if (background == null) return; RectTransformUtility.ScreenPointToLocalPointInRectangle(background, eventData.position, eventData.pressEventCamera, out var localPoint); var delta = localPoint - _centerLocal; // Clamp to radius if (delta.magnitude > maxRadius) delta = delta.normalized * maxRadius; // Normalize to [-1,1] var norm = (maxRadius > 0f) ? (delta / maxRadius) : Vector2.zero; // Apply deadzone var mag = norm.magnitude; if (mag < deadZone) norm = Vector2.zero; else if (mag > 1f) norm = norm.normalized; // Move handle visually if (handle != null) handle.anchoredPosition = delta; // Send to Input System SendValueToControl(norm); } public void OnPointerUp(PointerEventData eventData) { _pressed = false; if (snapBackOnRelease) { ResetHandle(); } SendValueToControl(Vector2.zero); } private void CacheCenter() { if (background != null) { // Center is the pivot point local position (0 if centered pivot). Use rect center for safety. _centerLocal = background.rect.center; } else { _centerLocal = Vector2.zero; } } private void ResetHandle() { if (handle != null) handle.anchoredPosition = Vector2.zero; } #if UNITY_EDITOR void OnValidate() { if (background == null) background = GetComponent(); if (handle == null && background != null && background.childCount > 0) handle = background.GetChild(0) as RectTransform; } #endif } }