Files
Vision/Assets/Scripts/UI/VirtualJoystick.cs
T

153 lines
5.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// 屏幕虚拟摇杆(移动轮盘)。挂在预制体根节点上,子物体约定按名字查找:
/// Background —— 底盘(RectTransform + Image)
/// Handle —— 摇杆头(RectTransform + Image)
///
/// 鼠标按住拖拽或手指触摸都可以操作,对外输出归一化方向 <see cref="Value"/>:
/// x = 左右(右为正),y = 前后(上为正),模长 0~1 表示推杆幅度。
/// 松手后方向归零。
/// </summary>
[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;
/// <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)
background = FindChildRect("Background");
// 没有名为 Background 的子物体时,就把挂脚本的这个节点本身当作底盘
if (background == null)
background = transform as RectTransform;
if (handle == null)
handle = FindChildRect("Handle");
_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);
}
private RectTransform FindChildRect(string childName)
{
Transform child = transform.Find(childName);
if (child == null)
{
// 允许更深层级,比如 Root/Art/Background
foreach (RectTransform rt in GetComponentsInChildren<RectTransform>(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;
}
}