在游戏开发过程中,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)
{
// 创建边框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);
}
}
}