首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

【Unity】-UGUI源码解析-EventSystem系统

本文章向大家介绍UGUI源码解析--EventSystem系统,主要包括UGUI源码解析--EventSystem系统使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1、EventSystem系统

看似名字很大,其实EventSystem处理和管理的是点击、触摸、键盘输入等事件,叫做InputEventSystem更为合适。

//系统输入模块

private List m_SystemInputModules = new List();

//当前输入模块

private BaseInputModule m_CurrentInputModule;

//当前选择GameObject

private GameObject m_CurrentSelected;

//处理的所有输入的EventSystem

private static List m_EventSystems = new List();

继承关系:

BaseInputModule抽象类

PointerInputModule抽象类

StandaloneInputModule类,面向“PC, Mac& Linux Standalone”这个平台的输入模块

TouchInputModule类,面向“IOS Android”等可触摸移动平台的输入模块

(注:在最新的2017.4UGUI源码中,TouchInputModule已经被弃用,触摸输入已经被集成到StandaloneInputModule。)

void Update()

EventSystem会在Update里每帧执行TickModules方法,调用每一个InputModule。

遍历m_SystemInputModules,判断这些module是否支持当前平台IsModuleSupported(),并且是否可激活ShouldActivateModule()。

如果m_CurrentInputModule为空,激活InputModule,设置m_CurrentSelected,实际上就是调用eventSystem.SetSelectedGameObject,然后有把符合条件的module便赋值给m_CurrentInputModule(当前输入模块)并break。

如果m_CurrentInputModule不为空,调用每一个InputModule的Process方法,先发送事件给被选择的GameObject(m_CurrentSelected),SendUpdateEventToSelectedObject,然后先处理触摸的一些事件ProcessTouchEvents(),再处理鼠标的一些事件ProcessMouseEvent()。

m_CurrentSelected大部分情况是Selectable组件(继承它的Button、Dropdown、InputField等组件)设置的。设置m_CurrentSelected,实际调用eventSystem.SetSelectedGameObject,会通过ExecuteEvents这个类对之前的对象执行一个被取消事件,且对新选中的对象执行一个被选中事件。这就是OnSelect和OnDeselect两个方法的由来。

EventSystem的RaycastAll方法

使用射线从相机到某个点(设为点E)投射到UI上,然后对所有投射到的对象进行排序,大致是远近排序。

RaycastAll会在PointerInputModule类的GetTouchPointerEventData和GetMousePointerEventData中调用,如果发生点击(或触摸)事件时,该事件影响的对象也会改变,通过RaycastAll方法(传入的PointerEventData的position作为点E)获得到第一个被射线照射到的对象,如果与之前的对象不同,则变更对象。(选择了新的对象,取消旧的对象)

ProcessTouchEvents()

遍历所有的inputTouch输入

GetTouchPointerEventData,获得第一个被射线照射到的对象

ProcessTouchPress

如果一直长按,触发ProcessMove和ProcessDrag方法。否则RemovePointerData

之后在ProcessXXX()方法中,传入相应的接口类型,调用ExecuteEvents.Execute()方法,执行事件。

ProcessMouseEvent()

获取鼠标的input输入

GetMousePointerEventData,获得第一个被射线照射到的对象

ProcessMousePress,依次触发左键,右键,中键的点击事件

之后在ProcessXXX()方法中,传入相应的接口类型,调用ExecuteEvents.Execute()方法,执行事件。

IsPointerOverGameObject

是EventSystem类里特别常用的一个方法,用于判断是否点击在UI上,具体是在PointerInputModule中实现的,判断最后一次点击的EventData数据是否为空,不为空即在UI上。

EventSystem current

最后我们注意到EventSystem有一个static属性:

public static EventSystem current

get { return m_EventSystems.Count > 0 ? m_EventSystems[0] : null; }

当一个EventSystem组件OnEnable的时候会将这个对象加入到m_EventSystems。

m_EventSystems.Add(this);

OnDisable的时候会将current从m_EventSystems移除

m_EventSystems.Remove(this);

2、执行事件

在上面的事件系统,其中我们讲到EventSystem可以通过ExecuteEvents这个类来执行事件,那么事件是如何执行的呢?这里涉及到了两个文件EventInterface和ExecuteEvents。

EventInterface类

EventInterface定义了一系列的跟输入有关的接口。例如IPointerEnterHandler(指针进入事件接口)。一个组件添加这个接口的继承之后,再实现OnPointerEnter方法,便可以接收到指针进入事件,也就是当鼠标滑入对象所在的区域之后,便会回调OnPointerEnter方法。这些接口全都继承自IEventSystemHandler,而后者也是声明在EventInterface里的接口。

例如:

public interface IPointerEnterHandler : IEventSystemHandler

{

void OnPointerEnter(PointerEventData eventData);

}

ExecuteEvents类

以上这些接口都会在ExecuteEvents里被调用。ExecuteEvents类是个静态类,不能被实例化,所有的公共方法都通过ExecuteEvents.XXXX来调用。ExecuteEvents里声明了一个delegate的类型EventFunction,这是一个泛型委托:

public delegate void EventFunction(T1 handler, BaseEventData eventData);

然后对EventInterface里的除IEventSystemHandler所有的接口声明了一个EventFunction类型的委托变量和方法。

例如:

private static readonly EventFunction

s_PointerEnterHandler = Execute;

private static void Execute(IPointerEnterHandler handler,

BaseEventData eventData)

{

  handler.OnPointerEnter(ValidateEventData(eventData));

}

然后又声明了一系列属性,这些属性是获取上述委托变量的只读属性,用于在外部调用。

public static EventFunction

pointerEnterHandler

{

  get { return s_PointerEnterHandler; }

}

而外部统一调用执行事件的方法是:

ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);

在方法内部,通过GetEventList获得targetGameObject上的T类型的组件列表,然后遍历这些组件,并执行EventFunction委托functor(arg, eventData);。

以pointerEnterHandler为例,我们可以了解functor这个方法实际上执行的是我们上面声明的EventFunction类型的委托方法:

handler.OnPointerEnter(ValidateEventData(eventData));

也就是调用了IPointerEnterHandler类型的组件的OnPointerEnter方法。

至此,我们就了解到了UGUI里的事件是如何执行的:指定某个接口类型,由Execute方法调用目标对象的接口方法。

接着,补充一下ExecuteEvents类里面其他方法的介绍。

ExecuteHierarchy方法会通过GetEventChain获取target的所有父对象,并对这些对象(包括target)执行Execute方法。

GetEventHandler会遍历目标对象及其父对象,判断他们是否可以处理某个指定的接口事件,如果可以,把目标对象作为返回值返回。而判断方法是CanHandleEvent,通过GetEventList方法获取target上的T类型的组件列表,判断列表数量不为零。GetEventHandler主要在输入模块里被调用,用于获取某个输入事件的响应对象。

3、截取事件

有两种截取事件的方法:

第一种,你可以扩展EventTrigger,并覆盖你感兴趣截取的事件的函数(简单常用);

第二种,指定单个的委托事件。

第一种方式:

public class EventTriggerExample : EventTrigger

{

  public override void OnBeginDrag(PointerEventData data)

  {

  Debug.Log("OnBeginDrag called.");

  }

  //省略其他重写的事件方法

}

第二种方式:

public class EventTriggerDelegateExample : MonoBehaviour

{

  void Start()

  {

      EventTrigger trigger = GetComponent();

      EventTrigger.Entry entry = new EventTrigger.Entry();

      entry.eventID = EventTriggerType.PointerDown;

      entry.callback.AddListener((data) => { OnPointerDownDelegate((PointerEventData)data); });

      trigger.triggers.Add(entry);

  }

  public void OnPointerDownDelegate(PointerEventData data)

  {

      Debug.Log("OnPointerDownDelegate called.");

  }

}

4、输入模块

在第一部分EventSystem我们探究了事件系统,在第二部分执行事件中我们介绍了事件是如何执行的。那么事件是如何产生的呢?这就涉及到BaseInputModule、PointerInputModule、StandaloneInputModule、TouchInputModule这些类。我们就探究一下输入模块的原理。

比如处理触摸事件时,EventSystem在OnUpdate()方法,执行ProcessTouchEvents()方法时,遍历所有的input.touchCount(input为BaseInput类),取得Touch对象。Touch对象包括,唯一ID:fingerId,位置信息等。

Touch touch = input.GetTouch(i);

取出的Touch对象,传入GetTouchPointerEventData方法,

GetTouchPointerEventData(touch, out pressed, out released);

在方法中,pointerData.position = input.position;

eventSystem.RaycastAll(pointerData, m_RaycastResultCache);

通过RaycastAll方法(传入的PointerEventData的position作为点E,从相机到点E投射一条射线,)获得到第一个被射线照射到的对象。最后执行ProcessTouchPress()方法。

这些事件是由输入模块产生的,而归根结底大部分是通过Input这个类的各种属性和静态方法获取了数据才生成了事件。

比如:

当鼠标或触摸进入、退出当前对象时执行pointerEnterHandler、pointerExitHandler。

在鼠标或者触摸按下、松开时执行pointerDownHandler、pointerUpHandler。

在鼠标或触摸松开并且与按下时是同一个响应物体时执行pointerClickHandler。

在鼠标或触摸位置发生偏移(偏移值大于一个很小的常量)时执行beginDragHandler。

在鼠标或者触摸按下且当前对象可以响应拖拽事件时执行initializePotentialDrag。

对象正在被拖拽且鼠标或触摸移动时执行dragHandler。

对象正在被拖拽且鼠标或触摸松开时执行endDragHandler。

鼠标或触摸松开且对象未响应pointerClickHandler情况下,如果对象正在被拖拽,执行dropHandler。

当鼠标滚动差值大于零执行scrollHandler。

当输入模块切换到StandaloneInputModule时执行updateSelectedHandler。(不需要Input类)

当鼠标移动导致被选中的对象改变时,执行selectHandler和deselectHandler。

UI组件的继承关系:(缩进表示子类)

5、CanvasUpdateRegistry

CanvasUpdateRegistry(画布更新注册处)是一个单例,它是UGUI与Canvas之间的中介,继承了ICanvasElement接口的组件都可以注册到它,它监听了Canvas即将渲染的事件,并调用已注册组件的Rebuild等方法。

CanvasUpdateRegistry维护了两个索引集(不会存放相同的元素):

//布局重建序列索引集

private readonly IndexedSet m_LayoutRebuildQueue = new IndexedSet();

//图形重建序列索引集

private readonly IndexedSet m_GraphicRebuildQueue = new IndexedSet();

m_LayoutRebuildQueue是通过RegisterCanvasElementForLayoutRebuild

和TryRegisterCanvasElementForLayoutRebuild方法添加元素。

m_GraphicRebuildQueue是通过RegisterCanvasElementForGraphicRebuild

和TryRegisterCanvasElementForGraphicRebuild方法添加元素。

二者通过UnRegisterCanvasElementForRebuild移除注册元素。

CanvasUpdateRegistry的构造函数:

protected CanvasUpdateRegistry()

{

  Canvas.willRenderCanvases += PerformUpdate;

}

willRenderCanvases是Canvas的静态事件,事件是一种特殊的委托,在渲染所有的Canvas之前,抛出willRenderCanvases事件,继而调用CanvasUpdateRegistry的PerformUpdate方法。

public enum CanvasUpdate

{

  Prelayout = 0,

  Layout = 1,

  PostLayout = 2,

  PreRender = 3,

  LatePreRender = 4,

  MaxUpdateValue = 5

}

除了最后一个枚举项,其他五个项分别代表了布局的三个阶段和渲染的两个阶段。

在PerformUpdate方法中

从两个序列中删除不可用的元素 CleanInvalidItems();

布局更新开始

对m_LayoutRebuildQueue依据父对象的数量进行排序

分别以PreLayout,Layout,PostLayout的参数顺序调用每一个元素的Rebuild方法

调用所有元素的LayoutComplete方法

清除布局重建序列中的所有元素

布局更新结束

完成布局后,调用组件的修剪方法

图形更新开始

以PreRender,LatePreRender的参数顺序调用每一个元素的Rebulid方法

调用所有元素的GraphicUpdateComplete方法

清除图形重建序列中的所有元素

图形更新结束

至此,一个完整的更新流程就完成了。

6、Raycast

编程小技巧:

Mathf.Approximately(0.0f, projectionDirection); 比较两个float值,如果他们在很小的相差(Epsilon)内,返回true。

浮点不精确使得使用等号运算符比较浮点数不准确。例如,(1.0 == 10.0 / 10.0)每次都可能不会返回true。

BaseRaycaster是其他Raycaster的抽象基类,它在OnEnable()时,把自己注册到RecasterManger中,而在OnDisable()时,从RecasterManager中移除。

RecasterManager是一个静态类,维护了一个BaseRaycaster类型的List。EventSystem里也通过这个类来管理所有的射线照射器,也就是EventSystem.RaycastAll()方法。

PhysicsRaycaster(物理射线照射器)添加了特性

[RequireComponent(typeof(Camera))]

说明它依赖于Camera组件。它通过eventCamera属性来获取对象上的Camera组件。

Raycast方法重写了BaseRaycaster的同名抽象方法:

在2017.4UGUI源码中,采用ReflectionMethodsCache.Singleton.raycast3DAll()来获取所有射线照射到的对象,用反射的方式把Physics.RaycastAll()方法缓存下来,让Unity的Physics模块与UI模块,保持低耦合,没有过分依赖。

获取到被射线照射到的对象,根据距离进行排序,然后包装成RaycastResult,加入到resultAppendList中。EventSystem会将所有的Raycast的照射结果合在一起进行排序,然后输入模块取到第一个距离最近的对象作为目标对象。

Physics2DRaycaster继承自PhysicsRaycaster,其他都一样,只重写了Raycast方法,改为用Physics2D.RaycastAll来照射对象,在2017.4UGUI源码中也采用了反射的方式获取的方法,并且根据SpriteRenderer组件设置结果变量(在EventSystem里会作为排序依据,毕竟是2D对象)。

GraphicRaycaster继承自BaseRaycaster,它添加了特性:

[RequireComponent(typeof(Canvas))]

表示它依赖于Canvas组件(通过canvas属性来获取)。

它重写了三个属性sortOrderPriority、renderOrderPriority(获取Canvas的sortingOrder和renderOrder,这在EventSystem里会作为排序依据)和eventCamera(获取canvas.worldCamera,为null则返回Camera.main),当canvas.renderMode == RenderMode.ScreenSpaceCamera时,canvas.worldCamera不能为空,要把渲染UI的相机拖到为Canvas上的Render Camera。

首先,多屏显示的支持,然后把屏幕上的点转化为相机的视窗坐标,用于判断是否在视窗之中。然后,从相机发射一条射线,根据blockingObjects来判断采用raycast3D还是raycast2D的方式,取得hitDistance,也就是从射线的原点到撞击点的矢量的大小。

再调用静态方法Raycast,获得在射线照射区域的所有Graphic列表m_RaycastResults(调用Graphic.Raycast()方法判断,射线位置是否有效),接着遍历m_RaycastResults,判断Graphic的方向向量与eventCamera的方向向量是否相交,如果相交,然后再判断Graphic是否在eventCamera的前面,并且距离小于hitDistance,满足这些条件,才会把它打包成RaycastResult添加到resultAppendList里。

由此可见GraphicRaycaster与其他射线照射器的区别就在于,它把照射对象限定为了Graphic。

补充知识点,RaycastHit.distance

在射线的情况下,距离表示从射线的原点到撞击点的矢量的大小

在扫描体积或球体投射的情况下,距离表示从原点到体积接触另一碰撞体的平移点的矢量的大小

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200607A0FXFO00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券