在游戏开发过程中,UI组件的拖动功能是一个常见的需求。特别是在需要实现拖动、边界检测、透明度控制以及动画反馈等功能时,编写一个高级UI拖动控制器将非常有用。在本文中,我们将创建一个支持多种Canvas模式和更精确边界检测的高级UI拖动控制器。
1. 脚本概述
AdvancedUIDragController
是一个实现了IBeginDragHandler
、IDragHandler
和IEndDragHandler
接口的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. 使用方法
- 将该脚本附加到任何UI元素(如Panel、Image等)。
- 在Inspector面板中设置各项参数,例如是否启用拖动、透明度、边界限制等。
- 运行游戏并测试拖动效果。
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){// 创建边框UIGameObject 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;elsepixels[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);}}
}