Unity中实现鼠标拖拽

前言

起因是想写一个类似王权的Demo,然后发现自己对应拖拽这个操作没有系统地归纳与理解一下,于是去看了一下文章、API和视频,然后有了这一篇文
在这里,只讨论鼠标的相关拖拽方式,不讨论移动端也就是屏幕点击拖拽的实现
(以后有必要的话把PC->移动专门弄一下好了)

鼠标拖拽这件事在游戏中并不少见,而我们想要通过Unity实现它也并不困难

本文从基本的坐标系对齐,再到API应用,最后到接口调用。从三个角度小结的如何在Unity中实现”鼠标拖拽”以及相关优劣性

参考的视频:
Unity常用的三种拖拽方法 来自Joe

坐标系判定

个人认为是最最基本的做法了,甚至有了很愚蠢的造轮子的感觉

核心做法很简单:
对于鼠标: 判定鼠标在不在物品上面->判定鼠标是否点击->根据是否点击来修改bool->改变物品

对于物品: 实时更新上述的bool值->如果在“被拖动”的状态,则更新坐标

这里要注意的是鼠标所在的屏幕坐标系和物品所在的世界坐标系的转换,使用内置的ScreenToWorldPoint()就可以轻松解决这件事

贴一下代码,挂在物品上的,其实可以略过

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
public class Drag2DSpirte : MonoBehaviour
{
[SerializeField] private bool isSelected ; //是否已经选中

private void Update()
{
Debug.Log(Input.mousePosition.x + " , " + Input.mousePosition.y);

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 ;
}

}

两个坐标系

鼠标所处的坐标系叫作屏幕坐标系,是以像素点来支撑的一个屏幕坐标系
左下角是(0,0),右上角为(Screen.width,Screen.height)
而我们的Transform组件中的position所在的坐标系,叫世界坐标系
(当一个物体存在父物体的时候,它会以相对坐标系来描述其位置)

可以利用一些方法来转换二者

OnMouseDrag

刚才说过上一种方式就是造轮子了,现在把轮子呈现一遍

1
2
3
4
5
private void OnMouseDrag()
{
Vector2 cursorPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
transform.position = new Vector2 (cursorPos.x,cursorPos.y);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

如此一来我们就不用什么又是点击松开判定,又是根据bool更新的,把**拖拽**这件事交给它全权负责就可以了

> 想要详细了解OnMouseDrag()就去看API,下面会简单说说它的几个兄弟

##### 又是一个小细节
可以通过改变物品的尺寸来给玩家一个“我点到了”的反馈,利用```OnMouseEnter```和```OnMouseExit```可以轻松搞定

```C#
private void OnMouseEnter()
{
transform.localScale += Vector3.one * 0.07f;
}

private void OnMouseExit()
{
transform.localScale -= Vector3.one * 0.07f ;
}

利用接口实现

事实上我个人觉得利用接口实现,接口写到太多了,在不用快速修复的情况下,一个个方法打的还累人

常用的接口包括:
IDragHandler
IBeginDragHandler
IEndDragHandler

接口中主要的方法是:
OnBeginDrag
OnDrag
OnEndDrag
OnDrop

这里需要注意的是,需要有OnDrag,才能正常使用的OnBeginDragOnEndDrag

拖拽物品代码如下:

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
public class DragByInterface : MonoBehaviour,IDragHandler,IBeginDragHandler,IEndDragHandler
{
//利用接口

private RectTransform rectTransform ;

private CanvasGroup canvasGroup ;

[SerializeField]private Canvas canvas ;

private void Start()
{
rectTransform = GetComponent<RectTransform>();
canvasGroup = GetComponent<CanvasGroup>();

}
public void OnBeginDrag(PointerEventData eventData){
canvasGroup.blocksRaycasts = false ;

canvasGroup.alpha = 0.75f ; //顺便修改一下透明度,表示在拖拽
//同理也可以修改canvasGroup中的其它属性
}

public void OnDrag(PointerEventData eventData){
rectTransform.anchoredPosition += eventData.delta /canvas.scaleFactor ;
//% 这里使用下面这个经典代码也是可以的(而且不用考虑屏幕大小)
//transform.position = Input.mousePosition ;
}

public void OnEndDrag(PointerEventData eventData){
//这个地方的检测应该放在槽对应的脚本上来检测放下才合理
canvasGroup.blocksRaycasts = true ;
canvasGroup.alpha = 1f ;
}
}

放置物品的槽的代码如下:

1
2
3
4
5
6
7
8
9
10
11
public class EndDragInSlot : MonoBehaviour , IDropHandler
{
public void OnDrop(PointerEventData eventData){
Debug.Log("槽内松开");
//% 利用 eventData.pointerDrag 可以得到当前拖拽的对象
eventData.pointerDrag.GetComponent<RectTransform>().anchoredPosition = GetComponent<RectTransform>().anchoredPosition ;
//通过让二者的锚点相等,实现放入的操作
}

//! 这里要注意一个在UI中经常遇到的问题:响应对象可能被上层对象遮挡/覆盖(解决手段见DragByInterface)
}

说实话并不觉得接口比OnMouseDrag以及后续的EventTrigger高级或者简单多少,起码现在我是没有看到它的优势。
除此之外,还有以下几个地方要处理:

1.检测不到放置位置:
实质上是因为鼠标拿起我们拖拽的物品的时候,该物品挡住了检测的激光射线,解决这个问题只需要在编辑器中为它加入一个canvasGroup,然后通过canvasGroup.blocksRaycasts控制激光是否能穿过它即可

在Begin的时候改为false,End的时候改回来

文档中的解释:blocksRaycasts为true的时候,会把这个物品当作一个Collider来看待

2.”移动”的第二种实现
利用每帧移动+= 对位移量进行计算,这一部分在代码中体现为:
rectTransform.anchoredPosition += eventData.delta /canvas.scaleFactor

rectTransform.anchoredPosition是获得图片“相对于”锚点的位置信息

eventData.delta 是 对“上一次更新、上一次Update”而言的,用户拖拽这个对象所移动的2D位置坐标信息

因此利用+=就可以产生“移动”的效果

3.canvas.scaleFactor
在2中,我们看到代码中有canvas.scaleFactor,它的存在是为了修正位置的

因为我们没有采用“直接更新坐标”的方法来移动物品,因此UI画布的尺寸大小会影响到二者位置的判定,很明显的一个表现就是鼠标位置和物品位置“逐渐不同步”,在UI画布的Scale不为1的时候,就会出现在这种情况

因此,我们大大方方地为更新量除以相应的UI画布尺寸大小的系数即可

反正就写上吧,你尺寸是1的时候这个除以也不会影响什么对吧

EvenTrigger

我的评价是:舒服

EvenTrigger和按钮的OnClick()一样,当我们在Unity编辑器里面加入它的时候,只需要利用编辑器界面就能完成各个情况、各个方法的调用

代码层面需要做的,就是写一个都是Public的脚本给它调用即可

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
public class DrayByEventTrigger : MonoBehaviour
{
//利用EventTrigger来进行调用,在这里留下一堆public给人家调用就好了
private Vector3 startPos ;

private void Start()
{
startPos = transform.position ;
}

public void DragMethod(){
//用于拖动
transform.position = Input.mousePosition ;
}

public void EndDragMethod(){
GameObject slotGO = GameObject.Find("Slot") ; //通过名字来找到“槽”这个对象,实际上这并不好,因为有各种隐患

float dist = Vector3.Distance(transform.position , slotGO.transform.position);

if(dist <= 100)
transform.position = slotGO.transform.position ;
else
transform.position = startPos ;
}
}

在这里,我们在判定鼠标拖拽的时候调用DragMenthod,而在结束拖拽的时候调用EndDragMethod就好了,简单方便,在编辑器里面就可以选择

(这里就是在我们想要拖到的对象(Image1)中添加了EventTrigger,然后挂上了对应的方法)

甚至可以说你写好了让策划测试去拖都可以

小结

在Unity中实现拖拽放开的检测的方法有很多,事实上除了这里写的三种。什么利用激光射线检测、利用位置检测等等,各种方式层出不穷。

但是万变不离其宗的是,对于鼠标位置、鼠标响应者两个逻辑的编写,以及对于拿起和放置这两个重要状态的判定。

个人喜欢在只需要简单的拖拽判定的时候,就利用OnMouse系列进行相关功能的编写
而在调用/判定会复杂一些的情况下,则使用EventTrigger进行编写(而且EventTrigger还能干除了拖拽之外的其他事情,格局大了)
至于调用接口的方式,个人不是很喜欢,故不评价