UGUI拖拽效果实现
环境
Windows10操作系统
Unity2022.3.8f1c1
Unity UI 1.0.0
正文
UGUI本身是没有提供拖拽的组件,但是它仍然提供了接口让我们知道何时为拖拽操作。我们只要通过实现IBeginDragHandler 、IDragHandler 和IEndDragHandler 接口就可以知道何时开始拖拽、何时正在拖拽和何时拖拽结束。下面是简单的拖拽实现,挂载此脚本的UI会跟随鼠标移动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using UnityEngine;using UnityEngine.EventSystems;using UnityEngine.UI;namespace UITest { [RequireComponent(typeof(Image)) ] public class TowableArea : MonoBehaviour , IBeginDragHandler , IDragHandler , IEndDragHandler { private Vector2 dragStartPos; public void OnBeginDrag (PointerEventData eventData ) { dragStartPos = eventData.currentInputModule.input.mousePosition; } public void OnDrag (PointerEventData eventData ) { Vector2 curDragStartPos = eventData.currentInputModule.input.mousePosition; Vector2 diff = curDragStartPos - dragStartPos; this .transform.position += new Vector3(diff.x, diff.y); dragStartPos = curDragStartPos; } public void OnEndDrag (PointerEventData eventData ) { } } }
你会发现我这里强制要求需要Image
组件。这是因为如果我不加,那么TowableArea
就检测不到拖拽事件。当然你也可以直接让TowableArea
继承Image
,然后继续后面的实现。
如果你有反编译过UGUI中Button
的实现,或是看过Button
的源码。你会发现Button
并没有继承或是强制要求Image
。虽然我觉得应该要继承Image
,但是我看到UGUI中Button
的做法后认为也可以按照这个思路。这里有一点要注意的是,如果你想把拖拽单独作做一个UI的功能,那么你最好是继承UIBehaviour
。首先这个本来这个拖拽就是在UGUI上,所以我们让其继承UIBehaviour
也算得上合理。其次如果你有同事做了一个关于UGUI的工具,那么继承于UIBehaviour
的拖拽脚本,大概率可以使用这UGUI的工具。
拖拽UI限制于屏幕内
2024.4.29补充,下面这段代码在我实际项目中出现了问题。原因是UI中的位置并不和窗口比例对应。比如你是1920
x 1080的比例去做UI适配,然后你运行在一个2560 x
1440的电脑上运行。下面的代码仍然要使用1920 x
1080。现在我项目忙,没有时间去做细致化的修改,等之后我有时间了并且记起来。我就修改下面的代码。
2024.5.3补充。我仔细想了一下,实现起来考虑的情况太多了。我觉得我只要给出我的想法就好了,实现方面我也只给出我的情况。后面大家可以按照我的思路结合一下自己的情况进行修改底代码。
大多数情况下,我们并不希望玩家那么自由的拖动UI。我们大都希望无论玩家怎么拖动UI,UI所在的位置都在屏幕内。我的办法是计算一个UI组件的最左上的点和最右下的点,然后我们就可以判断这个UI组件是否在屏幕内。主要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var rectT = this .gameObject.GetComponent<RectTransform>();var pos = rectT.anchoredPosition + diff;var rect = rectT.rect;var centerPos = new Vector2(Screen.width * (rectT.anchorMin.x + rectT.anchorMax.x), Screen.height * (rectT.anchorMin.y + rectT.anchorMax.y)) / 2 ;float maxXVal = pos.x + centerPos.x + rect.width * (1 - rectT.pivot.x) * rectT.localScale.x;float maxYVal = pos.y + centerPos.y + rect.height * (1 - rectT.pivot.y) * rectT.localScale.y;float minXVal = pos.x + centerPos.x - rect.width * rectT.pivot.x * rectT.localScale.x;float minYVal = pos.y + centerPos.y - rect.height * rectT.pivot.y * rectT.localScale.y;if (maxXVal > Screen.width){ pos.x -= maxXVal - Screen.width; } if (maxYVal > Screen.height){ pos.y -= maxYVal - Screen.height; } if (minXVal < 0 ) pos.x -= minXVal; if (minYVal < 0 ) pos.y -= minYVal; rectT.anchoredPosition = pos;
这里我改变的位置从position
改为了anchoredPosition
,我认为使用这个会更加方便去判断组件是否在屏幕内。有关Anchor
、Pivot
和anchoredPosition
的对应关系,大家可以网络上搜索相关的文章。在我个人的测试下,这段逻辑可以支持Unity提供的所有Anchor
选择。但是其他奇怪的Anchor
设定,我并没有进行测试。如果你发现上面代码的问题,麻烦您指出来或私信告知。
上面我粗暴的将UI的画布大小认为就是窗口大小,这是一个错误的观点。在UGUI中,我们所有的UI都要在一个主画布下。我们可以在主画布中找到一个组件:CanvasScaler
。当其UI Scale Mode
为Constant Pixel Size
,我们才可以大致的认为画布大小就是窗口大小。(关于CanvasScaler
中UI Scale Mode
的具体数值介绍,大家可以查看一下官方文档——画布缩放器
(Canvas Scaler) - Unity
手册 )所以当其不是这个设置的时候,上面的代码从逻辑上就是错误的。那只要我们可以获得主画布真正的大小,那么上面的代码就正确了。
其实主画布本身仍然算是画布,它是画布,那么它必定就有RectTransform
。那么其真正的大小不就是RectTransform.sizeDelta
。一般而言,我们只有主画布需要CanvasScaler
,所以我这里用了一些取巧的方法获取主画布。大家可以根据自己的UI框架进行修改。
完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 using UnityEngine;using UnityEngine.EventSystems;using UnityEngine.UI;namespace UITest { [RequireComponent(typeof(Image)) ] public class TowableArea : UIBehaviour , IBeginDragHandler , IDragHandler , IEndDragHandler { private Vector2 dragStartPos; private RectTransform canvasSize; protected override void Awake () { base .Awake(); var scaler = GetComponentInParent<CanvasScaler>(); canvasSize = scaler.GetComponent<RectTransform>(); } public void OnBeginDrag (PointerEventData eventData ) { dragStartPos = eventData.currentInputModule.input.mousePosition; } public void OnDrag (PointerEventData eventData ) { Vector2 curCanvasSize = canvasSize.sizeDelta; Vector2 curDragStartPos = eventData.currentInputModule.input.mousePosition; Vector2 diff = curDragStartPos - dragStartPos; var rectT = this .gameObject.GetComponent<RectTransform>(); var pos = rectT.anchoredPosition + diff; var rect = rectT.rect; var centerPos = new Vector2(curCanvasSize.x * (rectT.anchorMin.x + rectT.anchorMax.x), curCanvasSize.y * (rectT.anchorMin.y + rectT.anchorMax.y)) / 2 ; float maxXVal = pos.x + centerPos.x + rect.width * (1 - rectT.pivot.x) * rectT.localScale.x; float maxYVal = pos.y + centerPos.y + rect.height * (1 - rectT.pivot.y) * rectT.localScale.y; float minXVal = pos.x + centerPos.x - rect.width * rectT.pivot.x * rectT.localScale.x; float minYVal = pos.y + centerPos.y - rect.height * rectT.pivot.y * rectT.localScale.y; if (maxXVal > curCanvasSize.x) { pos.x -= maxXVal - curCanvasSize.x; } if (maxYVal > curCanvasSize.y) { pos.y -= maxYVal - curCanvasSize.y; } if (minXVal < 0 ) pos.x -= minXVal; if (minYVal < 0 ) pos.y -= minYVal; rectT.anchoredPosition = pos; dragStartPos = curDragStartPos; } public void OnEndDrag (PointerEventData eventData ) { } } }
补充说明
即使是上面的这段代码,我也没有在很多特殊的场景下测试过。我也只是在自己的电脑上进行了尝试。所以我真的不能保证是没有问题的,如果你有问题请帮忙指出。我会努力修改的。当然如果是因为版本变换而出现的问题,那我就无能为力了。我本身较懒,我也不想去维护两个版本的代码。