在游戲開發過程中,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);}}
}