128 lines
4.7 KiB
C#
128 lines
4.7 KiB
C#
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
using UnityEngine.UI;
|
||
|
||
/// <summary>
|
||
/// 屏幕虚拟摇杆(移动轮盘)。挂在预制体根节点上,底盘和摇杆头两个引用
|
||
/// (background / handle)在 Inspector / 预制体里接好,代码里不按名字查找节点。
|
||
///
|
||
/// 鼠标按住拖拽或手指触摸都可以操作,对外输出归一化方向 <see cref="Value"/>:
|
||
/// x = 左右(右为正),y = 前后(上为正),模长 0~1 表示推杆幅度。
|
||
/// 松手后方向归零。
|
||
/// </summary>
|
||
[DisallowMultipleComponent]
|
||
public class VirtualJoystick : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
|
||
{
|
||
[Header("引用(必须在 Inspector / 预制体里指定)")]
|
||
[Tooltip("摇杆底盘(RectTransform)。拖拽位置以它的轴心为圆心换算,必须是捕获点击的那个节点")]
|
||
[SerializeField] private RectTransform background;
|
||
|
||
[Tooltip("摇杆头(RectTransform),跟着拖拽移动")]
|
||
[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;
|
||
|
||
/// <summary>归一化输入方向,x = 左右,y = 前后,模长 0~1。</summary>
|
||
public Vector2 Value { get; private set; }
|
||
|
||
/// <summary>当前是否按住了轮盘。</summary>
|
||
public bool IsPressed { get; private set; }
|
||
|
||
private Canvas _canvas;
|
||
private Vector2 _handleHomePosition;
|
||
|
||
private void Awake()
|
||
{
|
||
if (background == null)
|
||
Debug.LogError("[VirtualJoystick] 没有指定摇杆底盘(background),请在 Inspector / 预制体里拖入。", this);
|
||
if (handle == null)
|
||
Debug.LogError("[VirtualJoystick] 没有指定摇杆头(handle),请在 Inspector / 预制体里拖入。", this);
|
||
|
||
_canvas = GetComponentInParent<Canvas>();
|
||
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);
|
||
}
|
||
|
||
/// <summary>把摇杆归零,手柄回到初始位置。</summary>
|
||
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);
|
||
}
|
||
}
|