using System; using UnityEngine; using UnityEngine.EventSystems; namespace XGame.VirtualInput { public sealed class VirtualJoystick : MonoBehaviour { [SerializeField] private RectTransform joystickRoot; [SerializeField] private RectTransform knob; [SerializeField] private float deadZone = 0.18f; [SerializeField] private float acquireRadiusMultiplier = 1.6f; private Vector2 value; private bool isActive; private bool isCaptured; private bool sentStarted; private int touchFingerId = -1; private Vector2? screenCenterOverride; private float? screenRadiusOverride; public Vector2 Value => value; public bool IsActive => isActive; public event Action Started; public event Action Changed; public event Action Ended; private void Awake() { BindNodes(); } private void OnDisable() { ReleasePointer(); } private void Update() { if (!isActiveAndEnabled) { return; } if (UnityEngine.Input.touchCount > 0) { UpdateTouchInput(); return; } touchFingerId = -1; UpdateMouseInput(); } public bool IsScreenPositionInAcquireArea(Vector2 screenPosition) { return Vector2.Distance(screenPosition, GetScreenCenter()) <= GetScreenRadius() * acquireRadiusMultiplier; } private void BindNodes() { if (joystickRoot == null) { Transform widget = transform.Find("Joystick"); joystickRoot = widget as RectTransform; } if (joystickRoot == null) { joystickRoot = transform as RectTransform; } if (knob == null) { Transform knobTransform = transform.FindTransform("Knob"); knob = knobTransform as RectTransform; } if (knob == null) { Debug.LogError("[VirtualJoystick] missing Knob node: " + name, this); enabled = false; } } private void UpdateMouseInput() { if (UnityEngine.Input.GetMouseButtonDown(0)) { isCaptured = IsScreenPositionInAcquireArea(UnityEngine.Input.mousePosition); } if (UnityEngine.Input.GetMouseButtonUp(0)) { isCaptured = false; ReleasePointer(); return; } if (isCaptured && UnityEngine.Input.GetMouseButton(0)) { ApplyPointer(UnityEngine.Input.mousePosition); } else { ReleasePointer(); } } private void UpdateTouchInput() { if (touchFingerId >= 0) { for (int i = 0; i < UnityEngine.Input.touchCount; i++) { Touch touch = UnityEngine.Input.GetTouch(i); if (touch.fingerId != touchFingerId) { continue; } if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled) { touchFingerId = -1; ReleasePointer(); return; } ApplyPointer(touch.position); return; } touchFingerId = -1; ReleasePointer(); return; } for (int i = 0; i < UnityEngine.Input.touchCount; i++) { Touch touch = UnityEngine.Input.GetTouch(i); if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled) { continue; } if (IsScreenPositionInAcquireArea(touch.position)) { touchFingerId = touch.fingerId; ApplyPointer(touch.position); return; } } ReleasePointer(); } private void ApplyPointer(Vector2 screenPosition) { if (!sentStarted) { sentStarted = true; Started?.Invoke(); } Vector2 raw = (screenPosition - GetScreenCenter()) / GetScreenRadius(); Vector2 nextValue = Vector2.ClampMagnitude(raw, 1f); bool nextActive = nextValue.magnitude >= deadZone; if (!nextActive) { nextValue = Vector2.zero; } bool changed = (nextValue - value).sqrMagnitude > 0.000001f || nextActive != isActive; value = nextValue; isActive = nextActive; UpdateKnob(); if (changed) { Changed?.Invoke(value, isActive); } } private void ReleasePointer() { bool hadState = sentStarted || isActive || value.sqrMagnitude > 0.000001f; sentStarted = false; isCaptured = false; touchFingerId = -1; value = Vector2.zero; isActive = false; UpdateKnob(); if (hadState) { Changed?.Invoke(value, isActive); Ended?.Invoke(); } } private void UpdateKnob() { if (knob != null) { knob.anchoredPosition = value * GetKnobTravelRadius(); } } private float GetKnobTravelRadius() { RectTransform root = joystickRoot != null ? joystickRoot : transform as RectTransform; if (root == null) { return GetScreenRadius(); } float rootHalf = Mathf.Min(root.rect.width, root.rect.height) * 0.5f; float knobHalf = knob != null ? Mathf.Min(knob.rect.width, knob.rect.height) * 0.5f : 0f; return Mathf.Max(1f, rootHalf - knobHalf); } private Vector2 GetScreenCenter() { if (screenCenterOverride.HasValue) { return screenCenterOverride.Value; } RectTransform root = joystickRoot != null ? joystickRoot : transform as RectTransform; if (root == null) { return Vector2.zero; } return RectTransformUtility.WorldToScreenPoint(GetUICamera(), root.position); } private float GetScreenRadius() { if (screenRadiusOverride.HasValue) { return Mathf.Max(1f, screenRadiusOverride.Value); } RectTransform root = joystickRoot != null ? joystickRoot : transform as RectTransform; if (root == null) { return 1f; } Camera cam = GetUICamera(); Vector3[] corners = new Vector3[4]; root.GetWorldCorners(corners); Vector2 s0 = RectTransformUtility.WorldToScreenPoint(cam, corners[0]); Vector2 s2 = RectTransformUtility.WorldToScreenPoint(cam, corners[2]); return Mathf.Max(1f, Mathf.Min(Mathf.Abs(s2.x - s0.x), Mathf.Abs(s2.y - s0.y)) * 0.5f); } private Camera GetUICamera() { RectTransform root = joystickRoot != null ? joystickRoot : transform as RectTransform; Canvas canvas = root != null ? root.GetComponentInParent() : null; if (canvas == null) { return null; } canvas = canvas.rootCanvas; return canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera; } } }