拖動在游戲中使用頻繁,例如將裝備拖動到指定的快捷欄,或者大地圖中拖動以查看局部信息等。
Unity的EventSystems中可以直接繼承幾個接口來實現拖動功能,如下:
namespace UnityEngine.EventSystems { public interface IBeginDragHandler : IEventSystemHandler { void OnBeginDrag(PointerEventData eventData); } } namespace UnityEngine.EventSystems { public interface IDragHandler : IEventSystemHandler { void OnDrag(PointerEventData eventData); } } namespace UnityEngine.EventSystems { public interface IEndDragHandler : IEventSystemHandler { void OnEndDrag(PointerEventData eventData); } }
他們分別代表拖動開始,持續和結束時的處理方法。然而遺憾的是,每有一個要拖動的物件對象,都需要重新寫一遍如何去處理它們,而大部分時候拖動的功能都相對通用,一般就是根據你鼠標或者手指滑動的方向對應的移動物體的方向,只有在拖動結束的時候可能需要額外判斷一下物體的狀態,例如是不是在指定的范圍內不是的話可能需要復位,是的話可能增加了某項屬性或者完成了一些其他功能,這時才是因情況而異。基於這樣的思考,考慮將一些通用的拖動實現過程再封裝一下,只留一個拖動結束后的委托用於外部調用即可,這樣省去了每次都寫一遍地圖拖動時如何移動,拖動到邊界了如何判斷等。
幸運的是,Unity在EventTrigger中已經包含了拖動的事件,具體如何動態添加EventTrigger的偵聽可以詳細見上一篇隨筆的末尾處:
https://www.cnblogs.com/koshio0219/p/12808063.html
在上面的基礎上參加一個新的擴展方法:
1 public static void AddDragListener(this Canvas canvas, Component obj, DragMode mode, UnityAction complete, float speed = 1f) 2 { 3 var dragable = obj.gameObject.GetOrAddComponent<Dragable>(); 4 dragable.Init(mode,speed); 5 6 canvas.AddTriggerListener(obj, EventTriggerType.BeginDrag, dragable.OnBeginDrag); 7 canvas.AddTriggerListener(obj, EventTriggerType.Drag, dragable.OnDrag); 8 canvas.AddTriggerListener(obj, EventTriggerType.EndDrag, (x) => dragable.OnEndDrag(x, complete)); 9 } 10 11 public static void RemoveDragListener(this Canvas canvas, Component obj) 12 { 13 canvas.RemoveTriggerListener(obj); 14 }
調用時如下:
1 //添加 2 Canvas.AddDragListener(View.Map, DragMode.Map, OnDragComplete); 3 4 //處理 5 private void OnDragComplete() 6 { 7 //Do something else... 8 } 9 10 //移除 11 Canvas.RemoveDragListener(View.Map);
Dragable類就是重新封裝過的一個專門用於處理拖動的類,外部使用它時不需要了解任何它的實現細節,而且使用時也簡單了許多,什么都不用關心,直接添加偵聽即可,不用再像原來一樣還要繼承三個接口分別寫。
當然了,接下來就是要討論Dragable這個類具體的實現方式,它需要處理通用的拖動操作,首先就是能讓拖的物體動起來,其次就是不能亂動,到了拖動范圍邊緣就不能再朝那個方位動了。
值得注意的是,拖動物件和拖動地圖一般是不同的,因為在拖動物件時,整個物件的輪廓范圍都應該保持在拖動范圍之內,而拖動地圖時則完全相反,一般地圖大於整個范圍才需要拖動來看,所以要保證地圖邊緣永遠大於拖動范圍。
見下圖:
假設上圖中黑色框代表拖動范圍,同樣貼近范圍左邊緣的情況下,左圖的物件不能再往向左的方向拖動,而右圖的地圖則不能再往向右的方向拖動。
分別定義兩種拖動模式如下,在初始化中可以設置模式與拖動速度:
1 public DragMode DragMode = DragMode.Map; 2 [Range(0.1f, 1.9f)] 3 public float DragSpeed = 1f; 4 5 public void Init(DragMode dragMode,float dragSpeed=1f) 6 { 7 DragMode = dragMode; 8 DragSpeed = dragSpeed > 1.9f ? 1.9f : dragSpeed < 0.1f ? 0.1f : dragSpeed; 9 }
1 public enum DragMode 2 { 3 Map, 4 Obj 5 }
拖動開始時:
1 Vector2 lastPos; 2 RectTransform rt; 3 Vector2 lastAnchorMin; 4 Vector2 lastAnchorMax; 5 6 public void OnBeginDrag(BaseEventData data) 7 { 8 //將基類的Data轉化為對應子類 9 var d = data as PointerEventData; 10 //初始化屏幕位置 11 lastPos = d.position; 12 13 rt = GetComponent<RectTransform>(); 14 lastAnchorMin = rt.anchorMin; 15 lastAnchorMax = rt.anchorMax; 16 17 //將錨框設置為四周擴展類型的預設,方便后續判斷和邊緣范圍的距離 18 rt.SetRtAnchorSafe(Vector2.zero, Vector2.one); 19 }
有一個位置需要注意,動態改變錨框時Unity並不會像是在編輯器中一樣友好幫你自動計算RectTransform,而是會各種亂,位置也可能不對了,大小也可能不對了,所以這里寫一個擴展方法進行安全改變錨框:
1 public static void SetRtAnchorSafe(this RectTransform rt, Vector2 anchorMin, Vector2 anchorMax) 2 { 3 if (anchorMin.x < 0 || anchorMin.x > 1 || anchorMin.y < 0 || anchorMin.y > 1 || anchorMax.x < 0 || anchorMax.x > 1 || anchorMax.y < 0 || anchorMax.y > 1) 4 return; 5 6 var lp = rt.localPosition; 7 //注意不要直接用sizeDelta因為該值會隨着anchor改變而改變 8 var ls = new Vector2(rt.rect.width, rt.rect.height); 9 10 rt.anchorMin = anchorMin; 11 rt.anchorMax = anchorMax; 12 13 //動態改變anchor后size和localPostion可能會發生變化需要重新設置 14 rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, ls.x); 15 rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, ls.y); 16 rt.localPosition = lp; 17 }
在拖動的過程中最重要的是要檢測是否到達父物體設置的拖動范圍,只有該方向上沒有到達邊緣才能朝這個方向移動:
1 public void OnDrag(BaseEventData data) 2 { 3 var d = data as PointerEventData; 4 //一幀內拖動的向量 5 Vector2 offse = d.position - lastPos; 6 7 //檢測拖動的方向與邊緣的關系 8 if (CheckDragLimit(offse)) 9 { 10 rt.anchoredPosition += offse * DragSpeed; 11 12 //極限快速拖動時單幀拖動距離超出范圍的歸位檢測 13 ResetRtOffset(); 14 } 15 lastPos = d.position; 16 }
1 bool CheckDragLimit(Vector2 offse) 2 { 3 bool result = false; 4 if (offse.x >= 0 && offse.y >= 0) 5 { 6 //向右上拖動 7 return DragMode == DragMode.Map ? rt.offsetMin.x < 0 && rt.offsetMin.y < 0 : 8 rt.offsetMax.x < 0 && rt.offsetMax.y < 0; 9 } 10 else if (offse.x >= 0 && offse.y < 0) 11 { 12 //向右下拖動 13 return DragMode == DragMode.Map ? rt.offsetMin.x < 0 && rt.offsetMax.y > 0 : 14 rt.offsetMax.x < 0 && rt.offsetMin.y > 0; 15 16 } 17 else if (offse.x < 0 && offse.y >= 0) 18 { 19 //向左上拖動 20 return DragMode == DragMode.Map ? rt.offsetMax.x > 0 && rt.offsetMin.y < 0 : 21 rt.offsetMin.x > 0 && rt.offsetMax.y < 0; 22 } 23 else if (offse.x < 0 && offse.y < 0) 24 { 25 //向左下拖動 26 return DragMode == DragMode.Map ? rt.offsetMax.x > 0 && rt.offsetMax.y > 0 : 27 rt.offsetMin.x > 0 && rt.offsetMin.y > 0; 28 } 29 return result; 30 }
先判斷拖動的方向,再根據拖動的方向結合拖動模式和相對邊緣的偏移來判斷是否還能朝對應方向拖動。
如果需要在全屏范圍內拖動,其上的父物體層都需要四周擴展類型的錨框預設且切合屏幕邊緣。
這里的offsetMin和offsetMax並不完全是對應Unity面板上的以下四個值,需要特別注意,網上的很多說法都存在一些未有考慮全面的地方:
比如上面這樣的數據,offsetMin實際上的(730,1724),但offsetMax則是(-608,-1138),這里不注意可能會出現很多錯誤。
那為什么會是這樣呢,其實那就要看offsetMin和offsetMax實際代表的是什么,他們分別是以其父物體大小的范圍的左下,右上為原點,右,上分別為X軸Y軸正方向得出的偏移值。
注意,無論是offsetMin還是offsetMax都是以右上為X軸和Y軸的正方向作為計算標准的,只不過原點不同。
然而惡意的是,在ugui的編輯面板中卻是用的邊到邊的距離,故而對於左下的點不會產生任何影響,但對於右上的點就會變為其相反數。
有時檢測邊緣也有丟失的情況,那就是單幀拖動的速度過快了,例如上一幀還遠遠不到邊緣,下一幀已經超出很遠,這時就需要對超出的部分進行重新復位到邊緣:
1 void ResetRtOffset() 2 { 3 switch (DragMode) 4 { 5 case DragMode.Map: 6 if (rt.offsetMin.x > 0) 7 rt.anchoredPosition -= new Vector2(rt.offsetMin.x, 0); 8 9 if (rt.offsetMin.y > 0) 10 rt.anchoredPosition -= new Vector2(0, rt.offsetMin.y); 11 12 if (rt.offsetMax.x < 0) 13 rt.anchoredPosition -= new Vector2(rt.offsetMax.x, 0); 14 15 if (rt.offsetMax.y < 0) 16 rt.anchoredPosition -= new Vector2(0, rt.offsetMax.y); 17 break; 18 case DragMode.Obj: 19 if (rt.offsetMin.x < 0) 20 rt.anchoredPosition -= new Vector2(rt.offsetMin.x, 0); 21 22 if (rt.offsetMin.y < 0) 23 rt.anchoredPosition -= new Vector2(0, rt.offsetMin.y); 24 25 if (rt.offsetMax.x > 0) 26 rt.anchoredPosition -= new Vector2(rt.offsetMax.x, 0); 27 28 if (rt.offsetMax.y > 0) 29 rt.anchoredPosition -= new Vector2(0, rt.offsetMax.y); 30 break; 31 } 32 }
歸為操作實際就是一個平移變換,超過多少就對應方位平移多少,這樣即使一幀內拖動的距離遠超過了邊緣,也只能到達緊緊貼合邊緣的程度。
拖動完成后,復位拖動前的錨框預設,執行整個過程完成后的委托:
1 public void OnEndDrag(BaseEventData data,UnityAction complete) 2 { 3 //還原拖動之前的預設 4 rt.SetRtAnchorSafe(lastAnchorMin, lastAnchorMax); 5 complete(); 6 }
當然了,如果真的遇到拖動過程中也需要執行個性化命令,這時也可考慮自行添加其他委托。
完整Dragable腳本:

1 using UnityEngine; 2 using UnityEngine.EventSystems; 3 using UnityEngine.Events; 4 5 public enum DragMode 6 { 7 Map, 8 Obj 9 } 10 11 public class Dragable : MonoBehaviour 12 { 13 public DragMode DragMode = DragMode.Map; 14 [Range(0.1f, 1.9f)] 15 public float DragSpeed = 1f; 16 17 public void Init(DragMode dragMode,float dragSpeed=1f) 18 { 19 DragMode = dragMode; 20 DragSpeed = dragSpeed > 1.9f ? 1.9f : dragSpeed < 0.1f ? 0.1f : dragSpeed; 21 } 22 23 Vector2 lastPos; 24 RectTransform rt; 25 Vector2 lastAnchorMin; 26 Vector2 lastAnchorMax; 27 28 public void OnBeginDrag(BaseEventData data) 29 { 30 //將基類的Data轉化為對應子類 31 var d = data as PointerEventData; 32 //初始化屏幕位置 33 lastPos = d.position; 34 35 rt = GetComponent<RectTransform>(); 36 lastAnchorMin = rt.anchorMin; 37 lastAnchorMax = rt.anchorMax; 38 39 //將錨框設置為四周擴展類型的預設,方便后續判斷和屏幕邊緣的距離 40 rt.SetRtAnchorSafe(Vector2.zero, Vector2.one); 41 } 42 43 public void OnDrag(BaseEventData data) 44 { 45 var d = data as PointerEventData; 46 //一幀內拖動的向量 47 Vector2 offse = d.position - lastPos; 48 49 //檢測拖動的方向與邊緣的關系 50 if (CheckDragLimit(offse)) 51 { 52 rt.anchoredPosition += offse * DragSpeed; 53 54 //極限快速拖動時單幀拖動距離超出范圍的歸位檢測 55 ResetRtOffset(); 56 } 57 lastPos = d.position; 58 } 59 60 public void OnEndDrag(BaseEventData data,UnityAction complete) 61 { 62 //還原拖動之前的預設 63 rt.SetRtAnchorSafe(lastAnchorMin, lastAnchorMax); 64 complete(); 65 } 66 67 bool CheckDragLimit(Vector2 offse) 68 { 69 bool result = false; 70 if (offse.x >= 0 && offse.y >= 0) 71 { 72 //向右上拖動 73 return DragMode == DragMode.Map ? rt.offsetMin.x < 0 && rt.offsetMin.y < 0 : 74 rt.offsetMax.x < 0 && rt.offsetMax.y < 0; 75 } 76 else if (offse.x >= 0 && offse.y < 0) 77 { 78 //向右下拖動 79 return DragMode == DragMode.Map ? rt.offsetMin.x < 0 && rt.offsetMax.y > 0 : 80 rt.offsetMax.x < 0 && rt.offsetMin.y > 0; 81 82 } 83 else if (offse.x < 0 && offse.y >= 0) 84 { 85 //向左上拖動 86 return DragMode == DragMode.Map ? rt.offsetMax.x > 0 && rt.offsetMin.y < 0 : 87 rt.offsetMin.x > 0 && rt.offsetMax.y < 0; 88 } 89 else if (offse.x < 0 && offse.y < 0) 90 { 91 //向左下拖動 92 return DragMode == DragMode.Map ? rt.offsetMax.x > 0 && rt.offsetMax.y > 0 : 93 rt.offsetMin.x > 0 && rt.offsetMin.y > 0; 94 } 95 return result; 96 } 97 98 void ResetRtOffset() 99 { 100 switch (DragMode) 101 { 102 case DragMode.Map: 103 if (rt.offsetMin.x > 0) 104 rt.anchoredPosition -= new Vector2(rt.offsetMin.x, 0); 105 106 if (rt.offsetMin.y > 0) 107 rt.anchoredPosition -= new Vector2(0, rt.offsetMin.y); 108 109 if (rt.offsetMax.x < 0) 110 rt.anchoredPosition -= new Vector2(rt.offsetMax.x, 0); 111 112 if (rt.offsetMax.y < 0) 113 rt.anchoredPosition -= new Vector2(0, rt.offsetMax.y); 114 break; 115 case DragMode.Obj: 116 if (rt.offsetMin.x < 0) 117 rt.anchoredPosition -= new Vector2(rt.offsetMin.x, 0); 118 119 if (rt.offsetMin.y < 0) 120 rt.anchoredPosition -= new Vector2(0, rt.offsetMin.y); 121 122 if (rt.offsetMax.x > 0) 123 rt.anchoredPosition -= new Vector2(rt.offsetMax.x, 0); 124 125 if (rt.offsetMax.y > 0) 126 rt.anchoredPosition -= new Vector2(0, rt.offsetMax.y); 127 break; 128 } 129 } 130 }