拖动在游戏中使用频繁,例如将装备拖动到指定的快捷栏,或者大地图中拖动以查看局部信息等。

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

在上面的基础上参加一个新的扩展方法:

     public static void AddDragListener(this Canvas canvas, Component obj, DragMode mode, UnityAction complete, float speed = 1f)
{
var dragable = obj.gameObject.GetOrAddComponent<Dragable>();
dragable.Init(mode,speed); canvas.AddTriggerListener(obj, EventTriggerType.BeginDrag, dragable.OnBeginDrag);
canvas.AddTriggerListener(obj, EventTriggerType.Drag, dragable.OnDrag);
canvas.AddTriggerListener(obj, EventTriggerType.EndDrag, (x) => dragable.OnEndDrag(x, complete));
} public static void RemoveDragListener(this Canvas canvas, Component obj)
{
canvas.RemoveTriggerListener(obj);
}

调用时如下:

 //添加
Canvas.AddDragListener(View.Map, DragMode.Map, OnDragComplete); //处理
private void OnDragComplete()
{
//Do something else...
} //移除
Canvas.RemoveDragListener(View.Map);

Dragable类就是重新封装过的一个专门用于处理拖动的类,外部使用它时不需要了解任何它的实现细节,而且使用时也简单了许多,什么都不用关心,直接添加侦听即可,不用再像原来一样还要继承三个接口分别写。

当然了,接下来就是要讨论Dragable这个类具体的实现方式,它需要处理通用的拖动操作,首先就是能让拖的物体动起来,其次就是不能乱动,到了拖动范围边缘就不能再朝那个方位动了。

值得注意的是,拖动物件和拖动地图一般是不同的,因为在拖动物件时,整个物件的轮廓范围都应该保持在拖动范围之内,而拖动地图时则完全相反,一般地图大于整个范围才需要拖动来看,所以要保证地图边缘永远大于拖动范围。

见下图:

假设上图中黑色框代表拖动范围,同样贴近范围左边缘的情况下,左图的物件不能再往向左的方向拖动,而右图的地图则不能再往向右的方向拖动。

分别定义两种拖动模式如下,在初始化中可以设置模式与拖动速度:

     public DragMode DragMode = DragMode.Map;
[Range(0.1f, 1.9f)]
public float DragSpeed = 1f; public void Init(DragMode dragMode,float dragSpeed=1f)
{
DragMode = dragMode;
DragSpeed = dragSpeed > 1.9f ? 1.9f : dragSpeed < 0.1f ? 0.1f : dragSpeed;
}
 public enum DragMode
{
Map,
Obj
}

拖动开始时:

     Vector2 lastPos;
RectTransform rt;
Vector2 lastAnchorMin;
Vector2 lastAnchorMax; public void OnBeginDrag(BaseEventData data)
{
//将基类的Data转化为对应子类
var d = data as PointerEventData;
//初始化屏幕位置
lastPos = d.position; rt = GetComponent<RectTransform>();
lastAnchorMin = rt.anchorMin;
lastAnchorMax = rt.anchorMax; //将锚框设置为四周扩展类型的预设,方便后续判断和边缘范围的距离
rt.SetRtAnchorSafe(Vector2.zero, Vector2.one);
}

有一个位置需要注意,动态改变锚框时Unity并不会像是在编辑器中一样友好帮你自动计算RectTransform,而是会各种乱,位置也可能不对了,大小也可能不对了,所以这里写一个扩展方法进行安全改变锚框:

     public static void SetRtAnchorSafe(this RectTransform rt, Vector2 anchorMin, Vector2 anchorMax)
{
if (anchorMin.x < || anchorMin.x > || anchorMin.y < || anchorMin.y > || anchorMax.x < || anchorMax.x > || anchorMax.y < || anchorMax.y > )
return; var lp = rt.localPosition;
//注意不要直接用sizeDelta因为该值会随着anchor改变而改变
var ls = new Vector2(rt.rect.width, rt.rect.height); rt.anchorMin = anchorMin;
rt.anchorMax = anchorMax; //动态改变anchor后size和localPostion可能会发生变化需要重新设置
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, ls.x);
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, ls.y);
rt.localPosition = lp;
}

在拖动的过程中最重要的是要检测是否到达父物体设置的拖动范围,只有该方向上没有到达边缘才能朝这个方向移动:

     public void OnDrag(BaseEventData data)
{
var d = data as PointerEventData;
//一帧内拖动的向量
Vector2 offse = d.position - lastPos; //检测拖动的方向与边缘的关系
if (CheckDragLimit(offse))
{
rt.anchoredPosition += offse * DragSpeed; //极限快速拖动时单帧拖动距离超出范围的归位检测
ResetRtOffset();
}
lastPos = d.position;
}
     bool CheckDragLimit(Vector2 offse)
{
bool result = false;
if (offse.x >= && offse.y >= )
{
//向右上拖动
return DragMode == DragMode.Map ? rt.offsetMin.x < && rt.offsetMin.y < :
rt.offsetMax.x < && rt.offsetMax.y < ;
}
else if (offse.x >= && offse.y < )
{
//向右下拖动
return DragMode == DragMode.Map ? rt.offsetMin.x < && rt.offsetMax.y > :
rt.offsetMax.x < && rt.offsetMin.y > ; }
else if (offse.x < && offse.y >= )
{
//向左上拖动
return DragMode == DragMode.Map ? rt.offsetMax.x > && rt.offsetMin.y < :
rt.offsetMin.x > && rt.offsetMax.y < ;
}
else if (offse.x < && offse.y < )
{
//向左下拖动
return DragMode == DragMode.Map ? rt.offsetMax.x > && rt.offsetMax.y > :
rt.offsetMin.x > && rt.offsetMin.y > ;
}
return result;
}

先判断拖动的方向,再根据拖动的方向结合拖动模式和相对边缘的偏移来判断是否还能朝对应方向拖动。

如果需要在全屏范围内拖动,其上的父物体层都需要四周扩展类型的锚框预设且切合屏幕边缘。

这里的offsetMin和offsetMax并不完全是对应Unity面板上的以下四个值,需要特别注意,网上的很多说法都存在一些未有考虑全面的地方:

比如上面这样的数据,offsetMin实际上的(730,1724),但offsetMax则是(-608,-1138),这里不注意可能会出现很多错误。

那为什么会是这样呢,其实那就要看offsetMin和offsetMax实际代表的是什么,他们分别是以其父物体大小的范围的左下,右上为原点,右,上分别为X轴Y轴正方向得出的偏移值。

注意,无论是offsetMin还是offsetMax都是以右上为X轴和Y轴的正方向作为计算标准的,只不过原点不同。

然而恶意的是,在ugui的编辑面板中却是用的边到边的距离,故而对于左下的点不会产生任何影响,但对于右上的点就会变为其相反数。

有时检测边缘也有丢失的情况,那就是单帧拖动的速度过快了,例如上一帧还远远不到边缘,下一帧已经超出很远,这时就需要对超出的部分进行重新复位到边缘:

     void ResetRtOffset()
{
switch (DragMode)
{
case DragMode.Map:
if (rt.offsetMin.x > )
rt.anchoredPosition -= new Vector2(rt.offsetMin.x, ); if (rt.offsetMin.y > )
rt.anchoredPosition -= new Vector2(, rt.offsetMin.y); if (rt.offsetMax.x < )
rt.anchoredPosition -= new Vector2(rt.offsetMax.x, ); if (rt.offsetMax.y < )
rt.anchoredPosition -= new Vector2(, rt.offsetMax.y);
break;
case DragMode.Obj:
if (rt.offsetMin.x < )
rt.anchoredPosition -= new Vector2(rt.offsetMin.x, ); if (rt.offsetMin.y < )
rt.anchoredPosition -= new Vector2(, rt.offsetMin.y); if (rt.offsetMax.x > )
rt.anchoredPosition -= new Vector2(rt.offsetMax.x, ); if (rt.offsetMax.y > )
rt.anchoredPosition -= new Vector2(, rt.offsetMax.y);
break;
}
}

归为操作实际就是一个平移变换,超过多少就对应方位平移多少,这样即使一帧内拖动的距离远超过了边缘,也只能到达紧紧贴合边缘的程度。

拖动完成后,复位拖动前的锚框预设,执行整个过程完成后的委托:

     public void OnEndDrag(BaseEventData data,UnityAction complete)
{
//还原拖动之前的预设
rt.SetRtAnchorSafe(lastAnchorMin, lastAnchorMax);
complete();
}

当然了,如果真的遇到拖动过程中也需要执行个性化命令,这时也可考虑自行添加其他委托。

完整Dragable脚本:

 using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events; public enum DragMode
{
Map,
Obj
} public class Dragable : MonoBehaviour
{
public DragMode DragMode = DragMode.Map;
[Range(0.1f, 1.9f)]
public float DragSpeed = 1f; public void Init(DragMode dragMode,float dragSpeed=1f)
{
DragMode = dragMode;
DragSpeed = dragSpeed > 1.9f ? 1.9f : dragSpeed < 0.1f ? 0.1f : dragSpeed;
} Vector2 lastPos;
RectTransform rt;
Vector2 lastAnchorMin;
Vector2 lastAnchorMax; public void OnBeginDrag(BaseEventData data)
{
//将基类的Data转化为对应子类
var d = data as PointerEventData;
//初始化屏幕位置
lastPos = d.position; rt = GetComponent<RectTransform>();
lastAnchorMin = rt.anchorMin;
lastAnchorMax = rt.anchorMax; //将锚框设置为四周扩展类型的预设,方便后续判断和屏幕边缘的距离
rt.SetRtAnchorSafe(Vector2.zero, Vector2.one);
} public void OnDrag(BaseEventData data)
{
var d = data as PointerEventData;
//一帧内拖动的向量
Vector2 offse = d.position - lastPos; //检测拖动的方向与边缘的关系
if (CheckDragLimit(offse))
{
rt.anchoredPosition += offse * DragSpeed; //极限快速拖动时单帧拖动距离超出范围的归位检测
ResetRtOffset();
}
lastPos = d.position;
} public void OnEndDrag(BaseEventData data,UnityAction complete)
{
//还原拖动之前的预设
rt.SetRtAnchorSafe(lastAnchorMin, lastAnchorMax);
complete();
} bool CheckDragLimit(Vector2 offse)
{
bool result = false;
if (offse.x >= && offse.y >= )
{
//向右上拖动
return DragMode == DragMode.Map ? rt.offsetMin.x < && rt.offsetMin.y < :
rt.offsetMax.x < && rt.offsetMax.y < ;
}
else if (offse.x >= && offse.y < )
{
//向右下拖动
return DragMode == DragMode.Map ? rt.offsetMin.x < && rt.offsetMax.y > :
rt.offsetMax.x < && rt.offsetMin.y > ; }
else if (offse.x < && offse.y >= )
{
//向左上拖动
return DragMode == DragMode.Map ? rt.offsetMax.x > && rt.offsetMin.y < :
rt.offsetMin.x > && rt.offsetMax.y < ;
}
else if (offse.x < && offse.y < )
{
//向左下拖动
return DragMode == DragMode.Map ? rt.offsetMax.x > && rt.offsetMax.y > :
rt.offsetMin.x > && rt.offsetMin.y > ;
}
return result;
} void ResetRtOffset()
{
switch (DragMode)
{
case DragMode.Map:
if (rt.offsetMin.x > )
rt.anchoredPosition -= new Vector2(rt.offsetMin.x, ); if (rt.offsetMin.y > )
rt.anchoredPosition -= new Vector2(, rt.offsetMin.y); if (rt.offsetMax.x < )
rt.anchoredPosition -= new Vector2(rt.offsetMax.x, ); if (rt.offsetMax.y < )
rt.anchoredPosition -= new Vector2(, rt.offsetMax.y);
break;
case DragMode.Obj:
if (rt.offsetMin.x < )
rt.anchoredPosition -= new Vector2(rt.offsetMin.x, ); if (rt.offsetMin.y < )
rt.anchoredPosition -= new Vector2(, rt.offsetMin.y); if (rt.offsetMax.x > )
rt.anchoredPosition -= new Vector2(rt.offsetMax.x, ); if (rt.offsetMax.y > )
rt.anchoredPosition -= new Vector2(, rt.offsetMax.y);
break;
}
}
}

Unity ugui拖动控件(地图模式与物件模式)的更多相关文章

  1. wxpython 中 用鼠标拖动控件 总结

    #encoding: utf-8 import wx import os import noname class Frame( noname.MyFrame1 ): def __init__(self ...

  2. win10 uwp 拖动控件

    我们会使用控件拖动,可以让我们做出好看的动画,那么我们如何移动控件,我将会告诉大家多个方法.其中第一个是最差的,最后的才是我希望大神你去用. Margin 移动 我们可以使用Margin移动,但这是w ...

  3. Unity中uGUI的控件事件穿透逻辑

    1.正常来说Image和Text是会拦截点击事件的,假设加入EventTrigger的话,就能够响应相应的交互事件. 2.假设Image和Text是一个Button的子控件.那么尽管其会显示在Butt ...

  4. C#程序员整理的Unity 3D笔记(十五):Unity 3D UI控件至尊–NGUI

    目前,UGUI问世不过半年(其随着Unity 4.6发布问世),而市面上商用的产品,UI控件的至尊为NGUI:影响力和广度(可搜索公司招聘Unity 3D,常常能看到对NGUI关键词). NGUI虽然 ...

  5. UGUI Toggle控件

    今天我们来看看Toogle控件, 它由Toogle + 背景 + 打勾图片 + 标签组成的. 它主要用于单选和多选 属性讲解: Is On: 代表是否选中. Toogle Transition: 在状 ...

  6. UGUI Scrollbar控件

    如题就是Scrollbar控件,它简单可以看成 Scrollbar 和 Image组件组成 它基本上不单独使用多数是制作滚动视图.我们来看看他独特的属性,重复的属性就不在介绍了! 属性讲解: Hand ...

  7. UGUI Text控件

    学习UGUI的Text控件,用于显示文本!.  基本属性就不再啰嗦了! Alignment: 文字以 水平和垂直 对齐方式, Horizontal Overflow: 水平 Wrap: 文字大小和数量 ...

  8. C# 在窗体上可拖动控件

    最近做了一个标签打印配置功能,需要根据客户需求自定义标签格式.显示内容,这时就用到了后台生成控件,并且其控件可在窗口中进行拖动,这里仅为记录一下实现过程,方便以后使用. 结果图: 源码: using ...

  9. Unity编辑器 - 输入控件聚焦问题

    Unity编辑器整理 - 输入控件聚焦问题 EditorGUI的输入控件在聚焦后,如果在其他地方改变值,聚焦的框不会更新,而且无法取消聚焦,如下图: 在代码中取消控件的聚焦 取消聚焦的"时机 ...

随机推荐

  1. day7作业

    # day7作业 # 1. 使用while循环输出1 2 3 4 5 6 8 9 10 count = 1 while count < 11: if count == 7: count += 1 ...

  2. BUG 测试计划

       性能追求 目前状况 测试标准 APP平稳运行,无crush现象   快速下拉翻页时,崩溃退出     要求多人使用,均流畅无异常退出方可               页面的放大缩小不会造成页面显 ...

  3. 多线程高并发编程(5) -- CountDownLatch、CyclicBarrier源码分析

    一.CountDownLatch 1.概念 public CountDownLatch(int count) {//初始化 if (count < 0) throw new IllegalArg ...

  4. G - Can you find it? 二分

    Give you three sequences of numbers A, B, C, then we give you a number X. Now you need to calculate ...

  5. codeforces Equalizing by Division (easy version)

    output standard output The only difference between easy and hard versions is the number of elements ...

  6. Mac os Pycharm 中使用Stanza进行实体识别(自然语言处理nlp)

    stanza 是斯坦福开源Python版nlp库,对自然语言处理有好大的提升,具体好在哪里,官网里面都有介绍,这里就不翻译了.下面放上对应的官网和仓库地址. stanza 官网地址:点击我进入 sta ...

  7. Python父类和子类关系/继承

    #!/usr/bin/env python # -*- coding: utf-8 -*- """ @File:继承_子类和父类的关系.py @E-mail:364942 ...

  8. [YII2] 增删改查2

    一.新增 使用model::save()操作进行新增数据 $user= new User; $user->username =$username; $user->password =$pa ...

  9. Windows 上安装msql库安装(基于8.0.19免安装版)

    一.进入官网进行下载mysql程序包: https://dev.mysql.com/downloads/mysql/ 二.解压缩 解压文件夹到指定目录,我放在 D:\mysql-8.0.19-winx ...

  10. JavaScript之预编译

    javascript是一种解释性弱类型语言,在浏览器中执行时,浏览器会先预览某段代码进行语法分析,检查语法的正确与否,然后再进行预编译,到最后才会从上往下一句一句开始执行这段代码,简单得来说可以表示为 ...