编程语言
首页 > 编程语言> > UGUI源码解析--EventSystem系统

UGUI源码解析--EventSystem系统

作者:互联网

1、EventSystem系统

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

//系统输入模块

private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();

//当前输入模块

private BaseInputModule m_CurrentInputModule;

//当前选择GameObject

private GameObject m_CurrentSelected;

//处理的所有输入的EventSystem

private static List<EventSystem> m_EventSystems = new List<EventSystem>();

继承关系:

BaseInputModule抽象类

PointerInputModule抽象类

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

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

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

 

void Update()

  1. EventSystem会在Update里每帧执行TickModules方法,调用每一个InputModule。
  2. 遍历m_SystemInputModules,判断这些module是否支持当前平台IsModuleSupported(),并且是否可激活ShouldActivateModule()。
  3. 如果m_CurrentInputModule为空,激活InputModule,设置m_CurrentSelected,实际上就是调用eventSystem.SetSelectedGameObject,然后有把符合条件的module便赋值给m_CurrentInputModule(当前输入模块)并break。
  4. 如果m_CurrentInputModule不为空,调用每一个InputModule的Process方法,先发送事件给被选择的GameObject(m_CurrentSelected),SendUpdateEventToSelectedObject,然后先处理触摸的一些事件ProcessTouchEvents(),再处理鼠标的一些事件ProcessMouseEvent()。
  5. 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()

  1. 遍历所有的inputTouch输入
  2. GetTouchPointerEventData,获得第一个被射线照射到的对象
  3. ProcessTouchPress
  4. 如果一直长按,触发ProcessMove和ProcessDrag方法。否则RemovePointerData
  5. 之后在ProcessXXX()方法中,传入相应的接口类型,调用ExecuteEvents.Execute()方法,执行事件。

ProcessMouseEvent()

  1. 获取鼠标的input输入
  2. GetMousePointerEventData,获得第一个被射线照射到的对象
  3. ProcessMousePress,依次触发左键,右键,中键的点击事件
  4. 之后在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>(T1 handler, BaseEventData eventData);

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

例如:

private static readonly EventFunction<IPointerEnterHandler> s_PointerEnterHandler = Execute;

private static void Execute(IPointerEnterHandler handler, BaseEventData eventData)
{
    handler.OnPointerEnter(ValidateEventData<PointerEventData>(eventData));
}

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

public static EventFunction<IPointerEnterHandler> pointerEnterHandler
{
    get { return s_PointerEnterHandler; }
}

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

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

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

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

handler.OnPointerEnter(ValidateEventData<PointerEventData>(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>();
        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<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();

//图形重建序列索引集

private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();

 

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方法中

 

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

 

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

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

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

标签:调用,EventSystem,对象,源码,事件,UGUI,执行,方法
来源: https://blog.csdn.net/qq826364410/article/details/88137056