using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; /// /// 屏幕虚拟摇杆(移动轮盘)。挂在预制体根节点上,子物体约定按名字查找: /// Background —— 底盘(RectTransform + Image) /// Handle —— 摇杆头(RectTransform + Image) /// /// 鼠标按住拖拽或手指触摸都可以操作,对外输出归一化方向 : /// x = 左右(右为正),y = 前后(上为正),模长 0~1 表示推杆幅度。 /// 松手后方向归零。 /// [DisallowMultipleComponent] public class VirtualJoystick : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler { [Header("引用(留空则按子物体名自动查找)")] [SerializeField] private RectTransform background; [SerializeField] private RectTransform handle; [Header("手感(距离单位是 UI 像素,以底盘轴心为圆心)")] [Tooltip("摇杆头允许离开中心的最大距离。拖到这个距离就是满值输入(Value 模长 = 1),再往外拖摇杆头也不动。" + "底盘 280、摇杆头 120 时,80 ≈ 摇杆头刚好贴到底盘边缘。")] [SerializeField] private float maxDragRadius = 80f; [Tooltip("死区:拖动距离小于「最大半径 × 该比例」时不输出,避免手抖")] [Range(0f, 0.9f)] [SerializeField] private float deadZone = 0.12f; [Tooltip("松手后摇杆头是否弹回中心")] [SerializeField] private bool returnToCenter = true; [Tooltip("松手后是否隐藏整个轮盘(移动端常用)")] [SerializeField] private bool hideOnRelease = false; /// 归一化输入方向,x = 左右,y = 前后,模长 0~1。 public Vector2 Value { get; private set; } /// 当前是否按住了轮盘。 public bool IsPressed { get; private set; } private Canvas _canvas; private Vector2 _handleHomePosition; private void Awake() { if (background == null) background = FindChildRect("Background"); // 没有名为 Background 的子物体时,就把挂脚本的这个节点本身当作底盘 if (background == null) background = transform as RectTransform; if (handle == null) handle = FindChildRect("Handle"); _canvas = GetComponentInParent(); if (handle != null) _handleHomePosition = handle.anchoredPosition; } private void OnEnable() { ResetInput(); } public void OnPointerDown(PointerEventData eventData) { IsPressed = true; gameObject.SetActive(true); UpdateFromPointer(eventData); } public void OnDrag(PointerEventData eventData) { IsPressed = true; UpdateFromPointer(eventData); } public void OnPointerUp(PointerEventData eventData) { IsPressed = false; if (returnToCenter) ResetInput(); if (hideOnRelease) gameObject.SetActive(false); } /// 把摇杆归零,手柄回到初始位置。 public void ResetInput() { Value = Vector2.zero; if (handle != null) handle.anchoredPosition = _handleHomePosition; } private void UpdateFromPointer(PointerEventData eventData) { if (background == null) return; Camera cam = _canvas != null && _canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : (eventData.pressEventCamera != null ? eventData.pressEventCamera : (_canvas != null ? _canvas.worldCamera : null)); if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(background, eventData.position, cam, out Vector2 local)) return; // local 是相对底盘轴心的像素偏移,直接按「最大拖动半径」归一化 float radius = Mathf.Max(1f, maxDragRadius); float distance = local.magnitude; Vector2 direction = distance > 0.0001f ? local / distance : Vector2.zero; float magnitude = Mathf.Clamp01(distance / radius); if (magnitude < deadZone) { Value = Vector2.zero; } else { // 死区外的部分重新映射到 0~1,避免刚出死区就一大跳 float remapped = Mathf.InverseLerp(deadZone, 1f, magnitude); Value = direction * Mathf.Clamp01(remapped); } if (handle != null) handle.anchoredPosition = _handleHomePosition + direction * (magnitude * radius); } private RectTransform FindChildRect(string childName) { Transform child = transform.Find(childName); if (child == null) { // 允许更深层级,比如 Root/Art/Background foreach (RectTransform rt in GetComponentsInChildren(true)) { if (rt != null && rt != transform && rt.name == childName) return rt; } return null; } return child as RectTransform; } private void OnValidate() { if (handle == null) handle = FindChildRect("Handle"); if (background == null) background = FindChildRect("Background") ?? transform as RectTransform; } }