前言
在Unity中实现拖拽的方法有多种,以下是几种常见的方法和它们的优缺点
1. 鼠标按键的点击事件
Input.GetMouseButtonDown和Input.GetMouseButtonUp 方法可以监测用户鼠标按键的点击事件,通过检测鼠标按钮的状态来实现拖拽效果。用户通过鼠标进行拖拽操作。
1.1 优点:
- 简单易懂,是最通用的实现拖拽方法之一。
- 适用于所有平台,包括PC和移动设备。
1.2 缺点:
- 拖拽细节(如拖拽的加速度、加速度的方向等)难以控制。
- 如果需要控制多个物体的拖拽行为,则需要编写大量的代码逻辑。
2. OnMouseDrag
OnMouseDrag 方法是 Unity 内建的一个组件事件,用于处理鼠标拖拽事件,可以通过在物体上加上事件脚本来实现拖拽。
2.1 优点:
- 简单明了,易于使用。
- 对于简单的拖拽需求,非常适用。
2.2 缺点:
- 只能用于PC平台或者Web平台。
- 不支持多点触摸和移动设备上的触摸操作。
3. Event Trigger 中的 BeginDrag、OnDrag 和 EndDrag
Event Trigger 是 Unity 中常用的 GUI 事件框架,通过监听不同的事件类型实现拖拽功能,包括 BeginDrag、OnDrag 和 EndDrag 事件。
BeginDrag 事件:用户开始拖拽一个物体时触发该事件;
OnDrag 事件:在拖拽物体时持续调用该事件,可以实现拖拽过程中的反馈等功能;
EndDrag 事件:在用户释放物体时触发该事件,可以在此处理放置、执行等操作。
3.1 优点:
- 支持多点触摸和移动设备上的触摸操作。
- 比较容易控制拖拽的操作流程,如速度、拖拽范围等。
- 可以实现更多基于 GUI 的拖拽效果。
3.2 缺点:
- 对于非 GUI 元素的拖拽,需要额外的逻辑实现。
- 开销比较大。
4. 接口实现的 OnBeginDrag、OnDrag 和 OnEndDrag
该方法需要继承 UnityEngine.EventSystems.IDragHandler 接口并实现接口中的方法,从而接收该界面上的物体的拖拽操作。
4.1 优点:
- 支持多点触摸和移动设备上的触摸操作。
- 对于非 GUI 元素的拖拽,也很容易实现。
4.2 缺点:
- 开销较大。
- 需要手动实现接口中的方法。
综上所述,以上几种实现拖拽效果的方法各有优缺点,需要针对实际需求来选择使用。如果需要快速实现拖拽效果,可以使用
Input.GetMouseButtonDown 和 OnMouseDrag。如果要实现更多的拖拽事件,可以使用 Event Trigger
或者接口实现。
实例
还不懂的也没关系,下面我会举一下实例,更深入的了解他们的用法
1. 鼠标按键的点击事件
新建场景
我们要做的就是:当游戏运行后,通过鼠标的点击、拖拽、松开等操作,能够自由地将右边的这些人物的零部件,自定义(拖拽)到我们左边的这个人物的外貌上
扩展问题
:我们如何让我们处于世界坐标系的2D图片,跟随着处于屏幕坐标系的鼠标一起移动呢?
我们只要将鼠标的屏幕坐标系信息转换为世界坐标系就可以了,可以通过Camera.Main.ScreenToWorldPoint
方法将Screen屏幕坐标系To转换为WorldPoint世界坐标系上的每一个像素点(坐标)
using UnityEngine;public class Drag2DSprite : MonoBehaviour
{[SerializeField] private bool isSelected; // 是否被选中private void Update(){if (isSelected){Vector2 cursorPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);transform.position = new Vector2(cursorPos.x, cursorPos.y);}}private void OnMouseOver(){if (Input.GetMouseButtonDown(0))isSelected = true; // 被选中if (Input.GetMouseButtonUp(0))isSelected = false; // 取消选中}
}
2. 拖拽内置方法实现
其中就有一个叫做【OnMouseDrag
】方法,它可以更方便的可以实现这个案例当中的2D贴图的拖拽
注意
:如果我们想使用OnMouseDrag、OnMouseOver、OnMouseEnter、On MouseExit、OnMouseUp等方法,这个对象必须含有Collider
组件,之后才能被这些方法所调用
2.1 OnMouseEnter、OnMouseExit例子
还是上面的案例,我们可以通过【OnMouseDrag
】来实现,当我们的鼠标进入、或者离开2D贴图时,可以增加相应的放大、缩小功能,来增加一些交互的体验感
private void OnMouseDrag() // 当鼠标拖动时
{Vector2 cursorPos = Camera.main.ScreenToWorldPoint(Input.mousePosition); // 将鼠标位置转换为世界坐标transform.position = new Vector2(cursorPos.x,cursorPos.y); // 将物体位置设置为鼠标位置
}private void OnMouseEnter() // 当鼠标进入时
{transform.localScale += Vector3.one * 0.07f; // 增加物体的缩放大小
}private void OnMouseExit() // 当鼠标离开时
{transform.LocalScale -= Vector3.one * 0.07f; // 减小物体的缩放大小
}
2.2 OnMouseUp例子
我们要做的就是,将下方的三张图通过拖拽来进行正确的匹配,我们首先选中可拖拽的这三张图片,因为由于我们之后会用到OnMouseDrag等方法,我们首先去添加BoxCollider2D
组件
代码实现
private Vector2 startPos; // 起始位置
[SerializeField] private Transform correctTrans; // 黑色图像
[SerializeField] private bool isFinished; // 是否完成
private void Start()
{startPos = transform.position; // 记录起始位置
}
private void OnMouseDrag()
{if (!isFinished) // 如果还未完成{transform.position = new Vector2(Camera.main.ScreenToWorldPoint(Input.mousePosition).x,Camera.main.ScreenToWorldPoint(Input.mousePosition).y); // 鼠标拖拽时移动物体}
}
private void OnMouseUp()
{if (Mathf.Abs(transform.position.x - correctTrans.position.x) <= 0.5f &&Mathf.Abs(transform.position.y - correctTrans.position.y) <= 0.5f) // 如果移动到了正确位置{transform.position = new Vector2(correctTrans.position.x, correctTrans.position.y); // 将物体移动到正确位置isFinished = true; // 标记为已完成}else // 如果没有移动到正确位置{transform.position = new Vector2(startPos.x, startPos.y); // 将物体移回起始位置}
}
结果
UI拖拽方法
我们刚才说了OnMouseDrag(等拖拽内置方法),一般适用于2D图片贴图和3D场景当中,如果遇到UI图片呢,是不会去使用OnMouseDrag等方法的
Event Trigger组件
新建ui场景
通过添加Event Trigger组件来实现
按下【Add New Event Type】添加新的事件类型,根据游戏中不同的事件类型,来实现不同的交互效果,不同的事件类型,包括了鼠标指针的进入、离开、按下、松开、点击,还有我们将会去使用到的Drag拖拽
还有我们拖拽结束后的EndDrag事件
书写代码方法
private Vector3 startPos; // 初始位置private void Start()
{startPos = transform.position; // 记录初始位置
}
public void DragMethod()
{transform.position = Input.mousePosition; // 将物体位置设置为鼠标位置
}
public void EndDragMethod()
{Gameobject slotGO = Gameobject.Find("SLot"); // 查找名为 "SLot" 的物体float dist = Vector3.Distance(transform.position, slotGO.transform.position); // 计算物体与 "SLot" 之间的距离if (dist <= 100)transform.position = slotGO.transform.position; // 如果距离小于等于 100,则将物体位置设置为 "SLot" 的位置elsetransform.position = startPos; // 否则将物体位置设置为初始位置
}
在刚才的Event Trigger上挂载对应的方法
运行效果
2. 疑问
这时候有人可能会有疑问了,前面不是说鼠标的位置信息是屏幕坐标系,而我们现在图片是UI图片并不是在同一坐标系,为什么我们可以用等号直接来写呢?
如果我们的UI模式是Screenspace Overlay模式下,我们可以直接的将鼠标位置信息直接赋值给我们的transition.position(如果想使用RectTransformUtility.ScreenPointToLocalPointInRectangle也是可以的)
3. 问题
- 首先:一般情况下我们会很少去使用GameObject.Find方法(如果需要找很多类似的游戏对象)
原因之一就是因为这个方法是在所有游戏对象中,通过名字挨个去查找满足这个名字要求的游戏对象,我们有可能会有很多个【槽】也有很多个物品,那么这将会是一件非常消耗性能的地方 - 第二就是如果你的同事修改了你这个对象的名字,那这个方法对这个项目可能就会造成不必要的隐患
- 还有就是Vector3.Distancel的计算方法也非常消耗性能,这就引出了本文章的最后一种方法,通过接口来实现UI对象的拖拽、点击等操作
接口
首先我们需要去引用UnityEngine.EventSystems命名空间
接着我们开始使用EventSystem下提供的拖拽接口,有关Drag的接口,有IBeginDrag、IEndDrag、IDrag等接口
注意
:OnBeginDrag和OnEndDrag的使用必须依赖OnDragHandler,而反过来,OnDragHandler却可以单独使用
1. IBeginDrag、IEndDrag、IDrag接口实现拖拽
1.1 扩展
我们可以通过rectTrans.AnchoredPosition的方式,获取这个UI图片「相对于」Anchor锚点的(参考)位置坐标信息,等于形参变量「eventData.delta」,这里的eventData.deltal的类型是Vector2结构体类型,表示的是自从上一次更新、上一次Update(Since Last Update可以理解为每一帧)用户拖着这个对象所移动的2D位置坐标信息通过+=的方式的累加(赋值),在拖拽的过程当中,赋值给RectTrans组件当中的anchoredPosition,如果你对这个方法有所迟疑呢,我们还可以去使用之前的transform.position=input.mousePosition来实现
1.2 槽的实现
我们拖拽的这个物品是否在【槽内】还是在【槽外】,所以放下物品这一个操作接口IDropHandler应该在写【槽Slot】这个游戏对象中,而不是写在我们的这个物品脚本上,因为我们的物品随时是可以Drop的,但是只有当我们的物品在【槽内】Drop的时候呢,那才有意义
1.2.1 槽的代码实现
using UnityEngine;
using UnityEngine.EventSystems;public class Slot:MonoBehaviour,IDropHandler
{// 在拖拽结束时调用,将拖拽的物体的位置设置为当前物体的位置public void OnDrop(PointerEventData eventData) {eventData.pointerDrag.GetComponent<RectTransform>().anchoredPosition = GetComponent<RectTransform>().anchoredPosition;}
}
1.2.2 可能出现的问题
当我们想要点击、想要触发的这个对象时,可能被上一层的U对象呢所遮挡、所覆盖了,会导致我们的鼠标无法被检测到,无法实现我们想要实现的功能。
比如说这里,我们想要实现的是槽这个游戏对象中的OnDrop方法,但是槽本身呢被上方鼠标拖拽的这个UI物品所覆盖、所遮挡了,他无法获取到我们鼠标何时松开Drop的操作,因为他被我们的物品所遮挡
这里介绍一个更为方便的组件,适合管理这一物体,包括他的子物体的所有的UI对象(透明度、可交互、是否遮挡等属性),添加Canvas Group组件
在脚本当中呢,我们首先去获取Canvas Group组件
当我们开始拖拽时,在OnBeginDrag方法的内部,将这个组件的blocksRaycasts属性设置为false,表示在我们刚开始拖拽的整个过程当中,鼠标不会再去把这个UI物品当作一个阻挡物来看待
1.3 物品代码
using UnityEngine;
using UnityEngine.EventSystems;public class DragByInterface : MonoBehaviour,IDragHandler,IBeginDragHandler,IEndDragHandler
{private RectTransform rectTrans;private CanvasGroup canvasGroup;private void Start(){rectTrans = GetComponent<RectTransform>();canvasGroup = GetComponent<CanvasGroup>();}public void OnBeginDrag(PointerEventData eventData){// 开始拖拽时,禁用射线检测和设置透明度canvasGroup.blocksRaycasts = false;canvasGroup.alpha = 0.35f;}public void OnDrag(PointerEventData eventData){// 拖拽时,更新位置rectTrans.anchoredPosition += eventData.delta;//transform.position Input.mousePosition;//0PTIONAL}public void OnEndDrag(PointerEventData eventData){// 结束拖拽时,启用射线检测和恢复透明度canvasGroup.blocksRaycasts = true;canvasGroup.alpha = 1f;}
}
1.4 效果
1.5 问题
这里最后提一句,如果鼠标在拖拽的过程当中,并不是和你要拖着这个物品同步,比如说鼠标和你拖拽的这个点的位置偏离过大
我们需要去检查Cnavasi画布当中Scale的数值呢是否为1,如果这个scalel不是1,那么就会出现鼠标拖拽过程当中不同步的问题,我们需要在EventData.deltal的后面,去除以相应的U画布尺寸大小的系数,这样就可以去解决鼠标拖拽U物品跑偏的这个问题了
public void OnDrag(PointerEventData eventData)
{rectTrans.anchoredPosition += eventData.delta / canvas.scaleFactor;//transform.position Input.mousePosition;//0PTIONAL
}
2. 单独使用IDragHandler实现拖拽
我们简单的通过接口来实现针对不同的UI面板窗口,进行拖拽的功能实现
我们希望拖拽的每个面板上方的导航栏会显示在UI的最高层,我们可以去使用panelRectPanel.SetAsLastSibling
表示的是:set设置,as为,last最后一个、最下方的Sibling同级最下方的这个位置将它在Hierarchy窗▣中,这个父物体下的顺序设置为最后一个,这样,我们就可以确保它能渲染在最前方
using UnityEngine;
using UnityEngine.EventSystems;public class Dragwindow : MonoBehaviour, IDragHandler, IPointerDownHandler
{private RectTransform panelRectTrans;private void Awake(){// 如果panelRectTrans为空,则设置为父对象的RectTransform组件if (panelRectTrans == null)panelRectTrans = transform.parent.GetComponent<RectTransform>();}//当用户拖拽物体时,将会触发public void OnDrag(PointerEventData eventData){// 更新panelRectTrans的anchoredPositionpanelRectTrans.anchoredPosition += eventData.delta;}//当用户按下物体时,将会触发public void OnPointerDown(PointerEventData eventData){// 将panelRectTrans设置为最后一个兄弟对象panelRectTrans.SetAsLastSibling();}
}
效果