UGUI源码剖析(事件系统)

EventSystem事件系统 

  • ​管理 所有的输入检测模块(InputModule)并帧调用Module的执行(Process)。 
  • 调动射线捕捉模块(Raycasters),为InputModule提供结果(具体的触点所穿透的对象信息)。
  • ​InputModule 管理更新EventData 判断当前的操作事件,并通知具体的EventSystemHandler 进行逻辑处理
//系统输入模块

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>();

void UpdateModules()

获取所有的BaseInputModule组件,放入列表中,遍历这个列表,移除那些为空或者隐藏掉的无效组件。

void SetSelectedGameObject(GameObject selected, BaseEventData pointer)

设置某个物体selected为选中状态,并且调用

    ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.deselectHandler);
    m_CurrentSelected = selected;
    ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.selectHandler);
    m_SelectionGuard = false;

唤醒前一个物体的Deselected方法和当前这个物体的Selected方法,也就是说只能有一个选中的物体。

int RaycastComparer(RaycastResult lhs, RaycastResult rhs)

射线照射结果的比较,依次按照

module.eventCamera.depth,module.sortOrderPriority,module.renderOrderPriority,

sortingLayer,sortingOrder,depth(当module.rootRaycaster相等时),distance,index

进行比较。

RaycastAll

使用射线从相机到某个点(设为点E)投射到UI上,调用RaycasterManager.GetRaycasters()获取所有的对象,然后对所有投射到的对象进行排序,大致是远近排序,实际上调用的时上面的RaycastComparer方法

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

IsPointerOverGameObject

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

OnEnable()

OnDisable()

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

m_EventSystems.Add(this);

OnDisable的时候会将current从m_EventSystems移除

m_EventSystems.Remove(this);

void Update()

  • EventSystem会在Update里每帧执行TickModules方法,调用每一个BaseInputModule实例的UpdateModule方法。
  • 遍历m_SystemInputModules,判断这些module是否支持当前平台IsModuleSupported(),并且是否可激活ShouldActivateModule(),调用ChangeEventModule,设置当前的Module,并分别调用上一个Module的DeactivateModule方法和当前Module的ActivateModule方法。
  • 如果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两个方法的由来。


 

EventInterface和ExecuteEvents执行事件

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

EventInterface

EventInterface定义了一系列的跟输入有关的接口。

IPointerEnterHandler,IPointerExitHandler,IPointerDownHandler,IPointerUpHandler,IPointerClickHandler,IBeginDragHandler,IInitializePotentialDragHandler,IDragHandler,

IEndDragHandler,IDropHandler,IScrollHandler,IUpdateSelectedHandler,ISelectHandler,IDeselectHandler,IMoveHandler,ISubmitHandler,ICancelHandler

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

ExecuteEvents

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

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

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

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

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

 public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler

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主要在输入模块里被调用,用于获取某个输入事件的响应对象。

输入模块

事件传递信息类

EventData文件夹下共有这样几个类,AbstractEventData,BaseEventData,AxisEventData,PointerEventData,这几个类主要是在输入模块中做参数使用,传递所需的信息。

  这几个类的关系如上图所示,其中

  • AbstractEventData有一个m_Used的字段表明当前的这个EventData是否正在被使用中
  • BaseEventData中有EventSystem m_EventSystem,BaseInputModule currentInputModule(返回的是m_EventSystem.currentInputModule)和GameObject selectedObject(返回的是m_EventSystem.currentSelectedGameObject)
  • AxisEventData中有Vector2 moveVector,MoveDirection moveDir记录下移动的方向和具体大小
  • PointerEventData中记录了几个跟指针事件相关的字段,当前事件通过射线检测出的的RaycastResult,Touch相关的信息,当前Pointer的pointerId,position,距上次update为止的delta位置信息变更,按下的位置pressPosition,上一次按下事件的时间clickTime,按下的次数clickCount,滑动的区域大小scrollDelta,是否使用滑动阈值useDragThreshold,是否正在拖动中dragging,当前的按键是哪个按键InputButton button。

InputModules

输入模块InputModules主要有4个类组成:BaseInput,BaseInputModule、PointerInputModule、StandaloneInputModule,其中TouchInputModule已被弃用,这里不再讨论。

BaseInputModule

在内部持有一个BaseInput类型的数据,记录下InputModule的一些输入数据。

抽象方法Process,处理输入事件

FindFirstRaycast 查找第一个射线检测的结果

DetermineMoveDirection  返回一个合适的输入方向

FindCommonRoot 给两个游戏物体,返回他们的共同的根物体

HandlePointerExitAndEnter 处理退出和进入的事件

几个待子类重写的虚方法:

GetAxisEventData ,GetBaseEventData,IsPointerOverGameObject

ShouldActivateModule(),DeactivateModule(),ActivateModule(),UpdateModule(),IsModuleSupported()

PointerInputModule

GetPointerData,获取指定id的PointerEventData数据,如果不存在就新建并缓存到m_PointerData中

RemovePointerData,从缓存m_PointerData中移除指定的PointerEventData

GetTouchPointerEventData,给出Touch,计算返回PointerEventData类,并返回它是否处于按下或者放松的状态

CopyFromTo 复制粘贴PointerEventData的数据

StateForMouseButton 给出一个buttonId返回当前帧的鼠标的状态FramePressState:

PressedAndReleased,Pressed,Released,NotChanged

MouseState GetMousePointerEventData(int id),返回指定id的MouseState

PointerEventData GetLastPointerEventData(int id),返回指定id的PointerEventData

private static bool ShouldStartDrag(Vector2 pressPos, Vector2 currentPos, float threshold, bool useDragThreshold),是否应该开始拖拽

protected virtual void ProcessMove(PointerEventData pointerEvent):处理移动事件

protected virtual void ProcessDrag(PointerEventData pointerEvent):处理拖拽事件

protected void ClearSelection() : 清除所有选中的物体的记录数据

protected void DeselectIfSelectionChanged(GameObject currentOverGo, BaseEventData pointerEvent):如果当前的物体不是事件系统记录下的物体,将事件系统的记录置空

PointerInputModule的内部类:

ButtonState 只有两个字段,PointerEventData和MouseButtonEventData类型的实例

MouseState 持有ButtonState的集合,记录鼠标按键状态,提供对鼠标按键状态进行更新的一系列方法

MouseButtonEventData 记录鼠标事件信息

StandaloneInputModule

重写UpdateModule方法,判断是否应该调用ReleaseMouse方法。

更新上一次记录的鼠标位置和当前的鼠标位置信息。

ReleaseMouse 方法,执行鼠标抬起的事件,同时判断是否需要去执行鼠标点击及拖拽相关事件。更新当前的鼠标事件信息记录实例m_InputPointerEvent。

重写基类的IsModuleSupported、ShouldActivateModule、ActivateModule、

DeactivateModule方法。

Process,主要执行当前选中物体的选中事件及上一个选中物体的取消选中事件。判断是否需要处理TouchEvents,然后调用ProcessMouseEvent处理鼠标事件。

ProcessTouchEvents 根据输入循环遍历Touch实例,执行按压事件、判断是否需要执行移动和拖拽事件。

ProcessTouchPress 执行按下、拖拽初始化、抬起、点击、drop、结束拖拽、焦点移除等事件。

SendSubmitEventToSelectedObject 执行当前选中物体的选中事件及上一个选中物体的取消选中事件。

GetRawMoveVector 获取鼠标水平垂直方向上的位移。

SendMoveEventToSelectedObject 通过调用GetRawMoveVector 方法获取鼠标水平垂直方向上的位移,将位移的相关数据信息传递给选中物体上。

ProcessMouseEvent 根据传入id处理鼠标按压、移动、拖拽等事件。

SendUpdateEventToSelectedObject  发送更新事件到选中物体。

ProcessMousePress 根据传入的MouseButtonEventData实例更新当前的PointerEventData实例数据。

射线检测

BaseRaycaster

BaseRaycaster是其他Raycaster的抽象基类,它在OnEnable()时,把自己注册到RaycasterManger中,而在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

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

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

编程小技巧:

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

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

事件触发

触发的类型枚举EventTriggerType

PointerEnter = 0,PointerExit = 1,PointerDown = 2,PointerUp = 3, PointerClick = 4,

Drag = 5,Drop = 6,Scroll = 7,UpdateSelected = 8,Select = 9,  Deselect = 10, 

Move = 11, InitializePotentialDrag = 12,BeginDrag = 13,  EndDrag = 14,  Submit = 15,  Cancel = 16

EventTrigger

分别实现了IEventSystemHandler派生出的事件接口

核心方法

private void Execute(EventTriggerType id, BaseEventData eventData)
{
    for (int i = 0, imax = triggers.Count; i < imax; ++i)
    {
        var ent = triggers[i];
        if (ent.eventID == id && ent.callback != null)
            ent.callback.Invoke(eventData);
    }
}

内部类:

public class Entry
{
    public EventTriggerType eventID = EventTriggerType.PointerClick;

    public TriggerEvent callback = new TriggerEvent();
}

分别保存了事件的id和回调

事件系统触发流程

由EventSystem的Update开始。

//EventSystem
protected virtual void Update()
{
    if (current != this)
        return;
    TickModules();//遍历并刷新所有的InputModules,更新Modules中的m_LastMousePosition、m_MousePosition           
    //省略中间遍历检查是否需要变更当前m_CurrentInputModule的部分
  	.....
  	//执行当前InputModule的Process,由此开始判断事件
    if (!changedModule && m_CurrentInputModule != null)
        m_CurrentInputModule.Process();
}

InputModule先会进行对外设输入的检测,来更新导航或是确定操作。紧接着会开始触摸检测,若不存在触摸,则进行鼠标事件的检测,因为触摸事件的检测是鼠标检测的简化版,所有下面我们针对鼠标检测进行分析。

//StandaloneInputModule
public override void Process()
{
    if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
        return;
    //向当前选中的目标执行UpdateSelectedHandler,并返回是否执行了
    bool usedEvent = SendUpdateEventToSelectedObject();

    //若用导航的情况会执行MoveHandler与SubmitHandler,当执行成功某一项时停止
    if (eventSystem.sendNavigationEvents)
    {
        if (!usedEvent)
            usedEvent |= SendMoveEventToSelectedObject();
        if (!usedEvent)
            SendSubmitEventToSelectedObject();
    }
    //以上部分是用来检测键盘输入的部分,例如使用键盘方向键选择按钮、使用ENTER键执行Submit。
    //接着开始先进行触摸的事件检测,如果不存在触摸,则会进行鼠标的事件检测
    if (!ProcessTouchEvents() && input.mousePresent)
        ProcessMouseEvent();
}

鼠标事件的检测过程,从中我们也能很清楚的了解到各个Handler的执行顺序(方法的复杂度在不断提升)。

//StandaloneInputModule
protected void ProcessMouseEvent(int id)
{
    //实际上这个id也没有用,每次都是获取左中右三个按键的信息
    //这里包含了PointerEventData数据与ButtonState数据,前者主要记录事件相关的信息,后者记录鼠标按键的当前状态
    var mouseData = GetMousePointerEventData(id);
    var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;
    m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;
    
	//执行鼠标按压的过程(根据buttonState来判断并执行 PointerDown PointerUp PointerClick Drop EndDrag 事件)
    ProcessMousePress(leftButtonData);
    
    //执行鼠标移动过程(根据pointerEvent判断并执行 PointerEnter PointerExit 事件)
    ProcessMove(leftButtonData.buttonData);
    
    //执行拖拽过程(根据pointerEvent判断并执行 BeginDrag Drag PointerUp 事件)
    ProcessDrag(leftButtonData.buttonData);
	
    //以下是对鼠标右键与中键的相同执行
	ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
	ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
	ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
	ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);

    //检测是否存在滚动事件,参数来自于input.mouseScrollDelta,这里使用了leftButtonData,实际上scrollDelta三个ButtonData里都是一样的,因为在GetMousePointerEventData方法中其他两个buttonData都是Copy leftData:)
    if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
    {
        var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
        ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
    }
}

在按下的情况下:PointerDown会先被执行,其次会检查物体是否有DragHandler,如果存在则会执行InitializePotentialDrag,这个会在发生拖拽之前执行。
在抬起的情况下(完成的点击操作):先会执行PointerUp,其次执行PointerClick,接着时Drop,最后时EndDrag。

//StandaloneInputModule
protected void ProcessMousePress(MouseButtonEventData data)
{
    var pointerEvent = data.buttonData;
    var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
    //判断当前是否是按下状态(都可以包含按下和抬起同帧情况)
    if (data.PressedThisFrame())
    {
        pointerEvent.eligibleForClick = true;
        pointerEvent.delta = Vector2.zero;
        pointerEvent.dragging = false;
        pointerEvent.useDragThreshold = true;
        pointerEvent.pressPosition = pointerEvent.position;
        pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

        DeselectIfSelectionChanged(currentOverGo, pointerEvent);
        
        //搜索父级路径下是否有IPointerDownHandler组件并执行
        var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);

        //如果自身及父级路径下没有IPointerDownHandler,则检查该路径下的IPointerClickHandler。
        if (newPressed == null)
            newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        float time = Time.unscaledTime;
        //若按压物体为发生变化,更新点击信息
        if (newPressed == pointerEvent.lastPress)
        {
            var diffTime = time - pointerEvent.clickTime;
            if (diffTime < 0.3f)
                ++pointerEvent.clickCount;
            else
                pointerEvent.clickCount = 1;
            pointerEvent.clickTime = time;
        }
        else
        {
            pointerEvent.clickCount = 1;
        }
        pointerEvent.pointerPress = newPressed;
        pointerEvent.rawPointerPress = currentOverGo;
        pointerEvent.clickTime = time;

        pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);
        //当存在IDragHandler时,先触发 initializePotentialDrag 事件 这个事件在BeginDrag之前
        if (pointerEvent.pointerDrag != null)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);

        m_InputPointerEvent = pointerEvent;
    }

    //当抬起时(都可以包含按下和抬起同帧情况)
    if (data.ReleasedThisFrame())
    {
        // 最先执行PointerUp事件
        ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

        var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
        {
            // 其次才执行PointerClick事件
            ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
        }
        else if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
        {
            // Drop 事件会在 EndDrag 之前执行
            ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
        }

        pointerEvent.eligibleForClick = false;
        pointerEvent.pointerPress = null;
        pointerEvent.rawPointerPress = null;

        // 最后执行EndDrag 事件
        if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

        pointerEvent.dragging = false;
        pointerEvent.pointerDrag = null;

        if (currentOverGo != pointerEvent.pointerEnter)
        {
            HandlePointerExitAndEnter(pointerEvent, null);
            HandlePointerExitAndEnter(pointerEvent, currentOverGo);
        }

        m_InputPointerEvent = pointerEvent;
    }
}

附上相关类图如下:

 

Guess you like

Origin blog.csdn.net/dmk17771552304/article/details/111715503