feat: add reusable virtual hud input

This commit is contained in:
ud18010
2026-07-30 11:14:56 +08:00
parent 3546d16632
commit 5853c5bd83
26 changed files with 6403 additions and 31 deletions
@@ -0,0 +1,273 @@
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<Vector2, bool> 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<Canvas>() : null;
if (canvas == null)
{
return null;
}
canvas = canvas.rootCanvas;
return canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera;
}
}
}