Unity高级UI拖动控制器教程

发布于:2025-08-30 ⋅ 阅读:(13) ⋅ 点赞:(0)

在游戏开发过程中,UI组件的拖动功能是一个常见的需求。特别是在需要实现拖动、边界检测、透明度控制以及动画反馈等功能时,编写一个高级UI拖动控制器将非常有用。在本文中,我们将创建一个支持多种Canvas模式和更精确边界检测的高级UI拖动控制器。

1. 脚本概述

AdvancedUIDragController是一个实现了IBeginDragHandlerIDragHandlerIEndDragHandler接口的Unity脚本。它提供了UI元素拖动功能,并且支持透明度调整、边界限制、鼠标指针变化、边框显示等多种特性。

2. 脚本结构

这个脚本包含多个功能模块:

  • 拖动设置:控制拖动是否启用、拖动时的透明度等。
  • 边界限制:控制UI元素是否会被限制在屏幕、Canvas或父物体的边界内。
  • 拖动反馈:改变鼠标指针、显示拖动边框等。
  • 动画设置:拖动结束时是否使用缓动动画。

3. 代码分析

3.1. 变量声明
// 拖动设置
public bool enableDrag = true; // 是否启用拖动功能
public bool showTransparencyOnDrag = true; // 拖动时是否显示半透明效果
public float dragTransparency = 1f; // 拖动时的透明度

// 边界限制
public bool limitToScreenBounds = true; // 是否限制在屏幕边界内
public float boundaryMargin = 10f; // 边界边距
public BoundaryMode boundaryMode = BoundaryMode.ScreenSpace; // 边界限制模式

// 拖动反馈
public bool changeCursorOnDrag = true; // 拖动时是否改变鼠标指针
public Texture2D dragCursor; // 拖动时的鼠标指针
public bool showBorderOnDrag = true; // 拖动时是否显示边框
public Color dragBorderColor = Color.yellow; // 拖动时的边框颜色

// 动画设置
public bool useEasingAnimation = true; // 拖动结束时是否使用缓动动画
public float animationDuration = 0.2f; // 缓动动画持续时间
public AnimationCurve easingCurve = AnimationCurve.EaseInOut(0, 0, 1, 1); // 缓动动画曲线
3.2. 初始化组件

Start()函数中,获取UI组件引用并设置必要的初始化。

void Start()
{
    InitializeComponents();
    SetupBorder();
}

private void InitializeComponents()
{
    rectTransform = GetComponent<RectTransform>();
    canvas = GetComponentInParent<Canvas>();
    canvasScaler = canvas.GetComponent<CanvasScaler>();

    canvasGroup = GetComponent<CanvasGroup>();
    if (canvasGroup == null && showTransparencyOnDrag)
    {
        canvasGroup = gameObject.AddComponent<CanvasGroup>();
    }

    originalAlpha = canvasGroup != null ? canvasGroup.alpha : 1f;
}
3.3. 拖动开始

当拖动开始时,记录鼠标偏移量、设置透明度、显示边框等。

public void OnBeginDrag(PointerEventData eventData)
{
    if (!enableDrag) return;

    isDragging = true;

    Vector2 localPointerPosition;
    RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, eventData.pressEventCamera, out localPointerPosition);
    dragOffset = localPointerPosition;

    originalPosition = rectTransform.anchoredPosition;

    if (showTransparencyOnDrag && canvasGroup != null)
    {
        canvasGroup.alpha = dragTransparency;
    }

    if (showBorderOnDrag && borderImage != null)
    {
        borderImage.gameObject.SetActive(true);
    }

    if (changeCursorOnDrag && dragCursor != null)
    {
        Cursor.SetCursor(dragCursor, hotSpot, cursorMode);
    }
}
3.4. 拖动中

在拖动过程中,计算UI元素的新位置,并应用边界限制。

public void OnDrag(PointerEventData eventData)
{
    if (!enableDrag || !isDragging) return;

    Vector2 localPointerPosition;
    RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform.parent as RectTransform, eventData.position, eventData.pressEventCamera, out localPointerPosition);

    Vector2 newPosition = localPointerPosition - dragOffset;

    if (limitToScreenBounds)
    {
        newPosition = ClampToBounds(newPosition);
    }

    rectTransform.anchoredPosition = newPosition;
    targetPosition = newPosition;
}
3.5. 拖动结束

拖动结束时,恢复透明度、隐藏边框、恢复鼠标指针,并应用缓动动画。

public void OnEndDrag(PointerEventData eventData)
{
    if (!isDragging) return;

    isDragging = false;

    if (showTransparencyOnDrag && canvasGroup != null)
    {
        canvasGroup.alpha = originalAlpha;
    }

    if (showBorderOnDrag && borderImage != null)
    {
        borderImage.gameObject.SetActive(false);
    }

    if (changeCursorOnDrag)
    {
        Cursor.SetCursor(null, Vector2.zero, cursorMode);
    }

    if (useEasingAnimation)
    {
        Vector2 finalPosition = limitToScreenBounds ? ClampToBounds(targetPosition) : targetPosition;
        if (Vector2.Distance(rectTransform.anchoredPosition, finalPosition) > 0.1f)
        {
            easingCoroutine = StartCoroutine(EaseToPosition(finalPosition));
        }
    }
}
3.6. 边界限制与动画

ClampToBounds方法用于根据不同的模式限制拖动元素的位置,EaseToPosition方法则负责缓动动画的执行。

4. 边界限制模式

我们提供了三种边界限制模式:

  • ScreenSpace:限制元素在屏幕空间内。
  • CanvasSpace:限制元素在Canvas空间内。
  • ParentSpace:限制元素在父物体空间内。

5. 实现拖动动画

缓动动画会在拖动结束时平滑地将UI元素移动到目标位置。

private System.Collections.IEnumerator EaseToPosition(Vector2 targetPos)
{
    Vector2 startPos = rectTransform.anchoredPosition;
    float elapsedTime = 0f;

    while (elapsedTime < animationDuration)
    {
        t = easingCurve.Evaluate(t);

        rectTransform.anchoredPosition = Vector2.Lerp(startPos, targetPos, t);
        yield return null;
    }

    rectTransform.anchoredPosition = targetPos;
    easingCoroutine = null;
}

6. 使用方法

  1. 将该脚本附加到任何UI元素(如Panel、Image等)。
  2. 在Inspector面板中设置各项参数,例如是否启用拖动、透明度、边界限制等。
  3. 运行游戏并测试拖动效果。

7. 完整代码

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

/// <summary>
/// 高级UI拖动控制器 - 支持多种Canvas模式和更精确的边界检测
/// </summary>
public class AdvancedUIDragController : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [Header("拖动设置")]
    [Tooltip("是否启用拖动功能")]
    public bool enableDrag = true;

    [Tooltip("拖动时是否显示半透明效果")]
    public bool showTransparencyOnDrag = true;

    [Tooltip("拖动时的透明度")]
    [Range(0f, 1f)]
    public float dragTransparency = 0.7f;

    [Header("边界限制")]
    [Tooltip("是否限制在屏幕边界内")]
    public bool limitToScreenBounds = true;

    [Tooltip("边界边距(像素)")]
    public float boundaryMargin = 10f;

    [Tooltip("边界限制模式")]
    public BoundaryMode boundaryMode = BoundaryMode.ScreenSpace;

    [Header("拖动反馈")]
    [Tooltip("拖动时是否改变鼠标指针")]
    public bool changeCursorOnDrag = true;

    [Tooltip("拖动时的鼠标指针")]
    public Texture2D dragCursor;

    [Tooltip("拖动时是否显示边框")]
    public bool showBorderOnDrag = true;

    [Tooltip("拖动时的边框颜色")]
    public Color dragBorderColor = Color.yellow;

    [Header("动画设置")]
    [Tooltip("拖动结束时是否使用缓动动画")]
    public bool useEasingAnimation = true;

    [Tooltip("缓动动画持续时间")]
    public float animationDuration = 0.2f;

    [Tooltip("缓动动画曲线")]
    public AnimationCurve easingCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);

    // 边界模式枚举
    public enum BoundaryMode
    {
        ScreenSpace,    // 屏幕空间
        CanvasSpace,    // Canvas空间
        ParentSpace     // 父对象空间
    }

    // 私有变量
    private RectTransform rectTransform;
    private Canvas canvas;
    private CanvasScaler canvasScaler;
    private Vector2 originalPosition;
    private Vector2 dragOffset;
    private float originalAlpha;
    private CanvasGroup canvasGroup;
    private bool isDragging = false;
    private CursorMode cursorMode = CursorMode.Auto;
    private Vector2 hotSpot = Vector2.zero;
    private Coroutine easingCoroutine;
    private UnityEngine.UI.Image borderImage;
    private Vector2 targetPosition;

    void Start()
    {
        InitializeComponents();
        SetupBorder();
    }

    private void InitializeComponents()
    {
        // 获取组件引用
        rectTransform = GetComponent<RectTransform>();
        canvas = GetComponentInParent<Canvas>();
        canvasScaler = canvas.GetComponent<CanvasScaler>();

        // 如果没有CanvasGroup,添加一个用于透明度控制
        canvasGroup = GetComponent<CanvasGroup>();
        if (canvasGroup == null && showTransparencyOnDrag)
        {
            canvasGroup = gameObject.AddComponent<CanvasGroup>();
        }

        // 保存原始透明度
        if (canvasGroup != null)
        {
            originalAlpha = canvasGroup.alpha;
        }

        // 设置鼠标指针热点
        if (dragCursor != null)
        {
            hotSpot = new Vector2(dragCursor.width / 2, dragCursor.height / 2);
        }
    }

    private void SetupBorder()
    {
        if (showBorderOnDrag)
        {
            // 创建边框UI
            GameObject borderObj = new GameObject("DragBorder");
            borderObj.transform.SetParent(transform, false);

            borderImage = borderObj.AddComponent<UnityEngine.UI.Image>();
            borderImage.color = dragBorderColor;
            borderImage.raycastTarget = false;

            RectTransform borderRect = borderObj.GetComponent<RectTransform>();
            borderRect.anchorMin = Vector2.zero;
            borderRect.anchorMax = Vector2.one;
            borderRect.offsetMin = Vector2.zero;
            borderRect.offsetMax = Vector2.zero;

            // 添加边框效果
            borderImage.sprite = CreateBorderSprite();
            borderImage.type = UnityEngine.UI.Image.Type.Sliced;

            borderObj.SetActive(false);
        }
    }

    private Sprite CreateBorderSprite()
    {
        // 创建一个简单的边框精灵
        Texture2D borderTexture = new Texture2D(3, 3);
        Color[] pixels = new Color[9];

        // 设置边框像素为白色,内部为透明
        for (int i = 0; i < 9; i++)
        {
            int x = i % 3;
            int y = i / 3;

            if (x == 0 || x == 2 || y == 0 || y == 2)
                pixels[i] = Color.white;
            else
                pixels[i] = Color.clear;
        }

        borderTexture.SetPixels(pixels);
        borderTexture.Apply();

        return Sprite.Create(borderTexture, new Rect(0, 0, 3, 3), new Vector2(0.5f, 0.5f), 1f, 0, SpriteMeshType.FullRect, new Vector4(1, 1, 1, 1));
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        if (!enableDrag) return;

        isDragging = true;

        // 停止任何正在进行的缓动动画
        if (easingCoroutine != null)
        {
            StopCoroutine(easingCoroutine);
            easingCoroutine = null;
        }

        // 计算拖动偏移量
        Vector2 localPointerPosition;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            rectTransform, eventData.position, eventData.pressEventCamera, out localPointerPosition);
        dragOffset = localPointerPosition;

        // 保存原始位置
        originalPosition = rectTransform.anchoredPosition;

        // 设置透明度
        if (showTransparencyOnDrag && canvasGroup != null)
        {
            canvasGroup.alpha = dragTransparency;
        }

        // 显示边框
        if (showBorderOnDrag && borderImage != null)
        {
            borderImage.gameObject.SetActive(true);
        }

        // 改变鼠标指针
        if (changeCursorOnDrag && dragCursor != null)
        {
            Cursor.SetCursor(dragCursor, hotSpot, cursorMode);
        }
    }

    public void OnDrag(PointerEventData eventData)
    {
        if (!enableDrag || !isDragging) return;

        // 计算新位置
        Vector2 localPointerPosition;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            rectTransform.parent as RectTransform, eventData.position, eventData.pressEventCamera, out localPointerPosition);

        Vector2 newPosition = localPointerPosition - dragOffset;

        // 应用屏幕边界限制
        if (limitToScreenBounds)
        {
            newPosition = ClampToBounds(newPosition);
        }

        // 更新位置
        rectTransform.anchoredPosition = newPosition;
        targetPosition = newPosition;
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        if (!isDragging) return;

        isDragging = false;

        // 恢复透明度
        if (showTransparencyOnDrag && canvasGroup != null)
        {
            canvasGroup.alpha = originalAlpha;
        }

        // 隐藏边框
        if (showBorderOnDrag && borderImage != null)
        {
            borderImage.gameObject.SetActive(false);
        }

        // 恢复鼠标指针
        if (changeCursorOnDrag)
        {
            Cursor.SetCursor(null, Vector2.zero, cursorMode);
        }

        // 应用缓动动画
        if (useEasingAnimation)
        {
            Vector2 finalPosition = limitToScreenBounds ? ClampToBounds(targetPosition) : targetPosition;
            if (Vector2.Distance(rectTransform.anchoredPosition, finalPosition) > 0.1f)
            {
                easingCoroutine = StartCoroutine(EaseToPosition(finalPosition));
            }
        }
    }

    /// <summary>
    /// 根据边界模式限制位置
    /// </summary>
    private Vector2 ClampToBounds(Vector2 position)
    {
        switch (boundaryMode)
        {
            case BoundaryMode.ScreenSpace:
                return ClampToScreenBounds(position);
            case BoundaryMode.CanvasSpace:
                return ClampToCanvasBounds(position);
            case BoundaryMode.ParentSpace:
                return ClampToParentBounds(position);
            default:
                return position;
        }
    }

    /// <summary>
    /// 限制在屏幕边界内
    /// </summary>
    private Vector2 ClampToScreenBounds(Vector2 position)
    {
        if (canvas == null) return position;

        // 获取屏幕尺寸
        Vector2 screenSize = new Vector2(Screen.width, Screen.height);

        // 获取UI元素在屏幕空间中的尺寸
        Vector2 uiSize = GetUISizeInScreenSpace();

        // 计算边界
        float minX = uiSize.x / 2 + boundaryMargin;
        float maxX = screenSize.x - uiSize.x / 2 - boundaryMargin;
        float minY = uiSize.y / 2 + boundaryMargin;
        float maxY = screenSize.y - uiSize.y / 2 - boundaryMargin;

        // 将屏幕坐标转换为本地坐标
        Vector2 screenPosition = RectTransformUtility.WorldToScreenPoint(null, rectTransform.TransformPoint(position));
        screenPosition.x = Mathf.Clamp(screenPosition.x, minX, maxX);
        screenPosition.y = Mathf.Clamp(screenPosition.y, minY, maxY);

        // 转换回本地坐标
        Vector2 localPosition;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            rectTransform.parent as RectTransform, screenPosition, null, out localPosition);

        return localPosition;
    }

    /// <summary>
    /// 限制在Canvas边界内
    /// </summary>
    private Vector2 ClampToCanvasBounds(Vector2 position)
    {
        if (canvas == null) return position;

        RectTransform canvasRect = canvas.GetComponent<RectTransform>();
        if (canvasRect == null) return position;

        Vector2 uiSize = rectTransform.sizeDelta;

        float minX = -canvasRect.sizeDelta.x / 2 + uiSize.x / 2 + boundaryMargin;
        float maxX = canvasRect.sizeDelta.x / 2 - uiSize.x / 2 - boundaryMargin;
        float minY = -canvasRect.sizeDelta.y / 2 + uiSize.y / 2 + boundaryMargin;
        float maxY = canvasRect.sizeDelta.y / 2 - uiSize.y / 2 - boundaryMargin;

        position.x = Mathf.Clamp(position.x, minX, maxX);
        position.y = Mathf.Clamp(position.y, minY, maxY);

        return position;
    }

    /// <summary>
    /// 限制在父对象边界内
    /// </summary>
    private Vector2 ClampToParentBounds(Vector2 position)
    {
        RectTransform parentRect = rectTransform.parent as RectTransform;
        if (parentRect == null) return position;

        Vector2 uiSize = rectTransform.sizeDelta;
        Vector2 parentSize = parentRect.sizeDelta;

        float minX = -parentSize.x / 2 + uiSize.x / 2 + boundaryMargin;
        float maxX = parentSize.x / 2 - uiSize.x / 2 - boundaryMargin;
        float minY = -parentSize.y / 2 + uiSize.y / 2 + boundaryMargin;
        float maxY = parentSize.y / 2 - uiSize.y / 2 - boundaryMargin;

        position.x = Mathf.Clamp(position.x, minX, maxX);
        position.y = Mathf.Clamp(position.y, minY, maxY);

        return position;
    }

    /// <summary>
    /// 获取UI元素在屏幕空间中的尺寸
    /// </summary>
    private Vector2 GetUISizeInScreenSpace()
    {
        Vector3[] corners = new Vector3[4];
        rectTransform.GetWorldCorners(corners);

        Vector2 size = new Vector2(
            Vector3.Distance(corners[0], corners[3]),
            Vector3.Distance(corners[0], corners[1])
        );

        return size;
    }

    /// <summary>
    /// 缓动到目标位置
    /// </summary>
    private System.Collections.IEnumerator EaseToPosition(Vector2 targetPos)
    {
        Vector2 startPos = rectTransform.anchoredPosition;
        float elapsedTime = 0f;

        while (elapsedTime < animationDuration)
        {
            elapsedTime += Time.deltaTime;
            float t = elapsedTime / animationDuration;
            t = easingCurve.Evaluate(t);

            rectTransform.anchoredPosition = Vector2.Lerp(startPos, targetPos, t);
            yield return null;
        }

        rectTransform.anchoredPosition = targetPos;
        easingCoroutine = null;
    }

    /// <summary>
    /// 重置到原始位置
    /// </summary>
    public void ResetToOriginalPosition()
    {
        if (rectTransform != null)
        {
            if (useEasingAnimation)
            {
                if (easingCoroutine != null)
                {
                    StopCoroutine(easingCoroutine);
                }
                easingCoroutine = StartCoroutine(EaseToPosition(originalPosition));
            }
            else
            {
                rectTransform.anchoredPosition = originalPosition;
            }
        }
    }

    /// <summary>
    /// 设置拖动是否启用
    /// </summary>
    public void SetDragEnabled(bool enabled)
    {
        enableDrag = enabled;
    }

    /// <summary>
    /// 设置边界限制是否启用
    /// </summary>
    public void SetBoundaryLimitEnabled(bool enabled)
    {
        limitToScreenBounds = enabled;
    }

    /// <summary>
    /// 设置边界模式
    /// </summary>
    public void SetBoundaryMode(BoundaryMode mode)
    {
        boundaryMode = mode;
    }

    void OnDisable()
    {
        // 确保在禁用时恢复鼠标指针和停止动画
        if (isDragging && changeCursorOnDrag)
        {
            Cursor.SetCursor(null, Vector2.zero, cursorMode);
        }

        if (easingCoroutine != null)
        {
            StopCoroutine(easingCoroutine);
            easingCoroutine = null;
        }
    }

    void OnDestroy()
    {
        // 清理边框精灵
        if (borderImage != null && borderImage.sprite != null)
        {
            DestroyImmediate(borderImage.sprite.texture);
            DestroyImmediate(borderImage.sprite);
        }
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到