找回密码
 立即注册
查看: 440|回复: 0

[笔记] Unity 触摸事件源码分析

[复制链接]
发表于 2022-11-26 10:22 | 显示全部楼层 |阅读模式
写本文的目的主要是想梳理下触摸事件系统的大体逻辑,看完本文大家伙脑海中有个类的框架这就够了。

首先让我们带着问题来分析
1、引擎是怎么检测到触摸事件的?
2、如果点击区域存在多个物体,哪个会先接收到触摸事件?
3、触摸事件都有哪些?

带着这几个问题我们开始进入我们的正文。

一、事件系统总览

首先我们看下源码的目录结构,让我们对代码结构先有个大致了解。


如上图所示EventSystem目录就是UGUI的事件系统相关代码,
EventData目录是点击或者触摸状态数据结构相关代码,继承关系如下:


InputModules目录是处理触摸事件的相关类,继承关系如下:



TouchInputModule已经被废弃了,本文不讲它

Raycaster目录是射线检测相关代码,继承关系如下:


其他的类都是事件系统相关的类。我们先不讲解。我们先看看我们的第一个问题是怎么回事。

二、引擎是怎么检测到触摸事件的?

首先让我们思考,如果是我们设计我们会怎么写代码呢?如果要检测触摸事件我们需要做2件事,
1、要有获取触摸状态的接口
2、要有轮询检测状态的接口

那我们看看UGUI是不是这么干的:
我们发现在EventSystem.cs中有个Update接口
//EventSystem.cs
protected virtual void Update()
{
    if (current != this)
        return;
    TickModules();

    bool changedModule = false;
    for (var i = 0; i < m_SystemInputModules.Count; i++)
    {
        var module = m_SystemInputModules;
        if (module.IsModuleSupported() && module.ShouldActivateModule())
        {
            if (m_CurrentInputModule != module)
            {
                ChangeEventModule(module);
                changedModule = true;
            }
            break;
        }
    }

    // no event module set... set the first valid one...
    if (m_CurrentInputModule == null)
    {
        for (var i = 0; i < m_SystemInputModules.Count; i++)
        {
            var module = m_SystemInputModules;
            if (module.IsModuleSupported())
            {
                ChangeEventModule(module);
                changedModule = true;
                break;
            }
        }
    }

    if (!changedModule && m_CurrentInputModule != null)
        m_CurrentInputModule.Process();
}
没错,这个接口就是我们说的轮询检测状态的接口。在Update中会不断对m_CurrentInputModule对象处理 m_CurrentInputModule.Process()。

那m_CurrentInputModule 是什么呢?

m_CurrentInputModule 看名字也知道就是BaseInputModule对象。 看代码我们知道 m_CurrentInputModule是 m_SystemInputModules数组的第一个激活对象。

那m_SystemInputModules 是在哪里赋值的呢?

看代码我们发现是在m_EventSystem.UpdateModules();中通过GetComponents(m_SystemInputModules);获取的。
而m_EventSystem.UpdateModules() 是在BaseInputModule中调用的。

用过UGUI的同学都知道我们会在启动界面的某个节点上挂载EventSystem 和 StandaloneInputModule 类,所以游戏一启动就通过EventSystem去轮询检测触摸事件,而m_CurrentInputModule实质也就是StandaloneInputModule 。

所以我们推测检测触摸事件真正的逻辑肯定是在StandaloneInputModule中处理,那我们就去StandaloneInputModule 的Process 接口中去看看怎么处理的。
//StandaloneInputModule.cs
public override void Process()
{
    if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
        return;

    bool usedEvent = SendUpdateEventToSelectedObject();
    if (!ProcessTouchEvents() && input.mousePresent)
        ProcessMouseEvent();

    if (eventSystem.sendNavigationEvents)
    {
        if (!usedEvent)
            usedEvent |= SendMoveEventToSelectedObject();

        if (!usedEvent)
            SendSubmitEventToSelectedObject();
    }
}
StandaloneInputModule覆写了基类的Process接口。

那回到上面的小疑问,获取触摸状态的接口在哪呢?
我们看下StandaloneInputModule类中ProcessTouchEvents 接口
//StandaloneInputModule.cs
private bool ProcessTouchEvents()
{
    for (int i = 0; i < input.touchCount; ++i)
    {
        Touch touch = input.GetTouch(i);

        if (touch.type == TouchType.Indirect)
            continue;

        bool released;
        bool pressed;
        var pointer = GetTouchPointerEventData(touch, out pressed, out released);

        ProcessTouchPress(pointer, pressed, released);

        if (!released)
        {
            ProcessMove(pointer);
            ProcessDrag(pointer);
        }
        else
            RemovePointerData(pointer);
    }
    return input.touchCount > 0;
}

protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
{
    PointerEventData pointerData;
    var created = GetPointerData(input.fingerId, out pointerData, true);

    pointerData.Reset();

    pressed = created || (input.phase == TouchPhase.Began);
    released = (input.phase == TouchPhase.Canceled) || (input.phase == TouchPhase.Ended);

    if (created)
        pointerData.position = input.position;

    if (pressed)
        pointerData.delta = Vector2.zero;
    else
        pointerData.delta = input.position - pointerData.position;

    pointerData.position = input.position;

    pointerData.button = PointerEventData.InputButton.Left;

    if (input.phase == TouchPhase.Canceled)
    {
        pointerData.pointerCurrentRaycast = new RaycastResult();
    }
    else
    {
        eventSystem.RaycastAll(pointerData, m_RaycastResultCache);

        var raycast = FindFirstRaycast(m_RaycastResultCache);
        pointerData.pointerCurrentRaycast = raycast;
        m_RaycastResultCache.Clear();
    }
    return pointerData;
}

我们看到了GetTouchPointerEventData接口 ,这个接口返回了PointerEventData对象,这个对象里就存储了我们触摸状态的所有信息。

然后根据这个触摸信息去处理对应点击,拖动,移动等相关的逻辑,具体接口参照
ProcessTouchPress
ProcessMove
ProcessDrag

这里就不在深入分析了,毕竟细节还是很多的,那不是本章的重点。

三、如果点击区域存在多个物体,哪个会先接收到触摸事件?

我们先看下UGUI是怎样检测到物体的,在GetTouchPointerEventData接口中我们看到有这样一段代码
eventSystem.RaycastAll(pointerData, m_RaycastResultCache);

然后我们看下这个接口
//EventSystem.cs
private static int RaycastComparer(RaycastResult lhs, RaycastResult rhs)
{
    if (lhs.module != rhs.module)
    {
        var lhsEventCamera = lhs.module.eventCamera;
        var rhsEventCamera = rhs.module.eventCamera;
        if (lhsEventCamera != null && rhsEventCamera != null && lhsEventCamera.depth != rhsEventCamera.depth)
        {
            // need to reverse the standard compareTo
            if (lhsEventCamera.depth < rhsEventCamera.depth)
                return 1;
            if (lhsEventCamera.depth == rhsEventCamera.depth)
                return 0;

            return -1;
        }

        if (lhs.module.sortOrderPriority != rhs.module.sortOrderPriority)
            return rhs.module.sortOrderPriority.CompareTo(lhs.module.sortOrderPriority);

        if (lhs.module.renderOrderPriority != rhs.module.renderOrderPriority)
            return rhs.module.renderOrderPriority.CompareTo(lhs.module.renderOrderPriority);
    }

    if (lhs.sortingLayer != rhs.sortingLayer)
    {
        // Uses the layer value to properly compare the relative order of the layers.
        var rid = SortingLayer.GetLayerValueFromID(rhs.sortingLayer);
        var lid = SortingLayer.GetLayerValueFromID(lhs.sortingLayer);
        return rid.CompareTo(lid);
    }

    if (lhs.sortingOrder != rhs.sortingOrder)
        return rhs.sortingOrder.CompareTo(lhs.sortingOrder);

    // comparing depth only makes sense if the two raycast results have the same root canvas (case 912396)
    if (lhs.depth != rhs.depth && lhs.module.rootRaycaster == rhs.module.rootRaycaster)
        return rhs.depth.CompareTo(lhs.depth);

    if (lhs.distance != rhs.distance)
        return lhs.distance.CompareTo(rhs.distance);

    return lhs.index.CompareTo(rhs.index);
}

private static readonly Comparison<RaycastResult> s_RaycastComparer = RaycastComparer;

public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
{
    raycastResults.Clear();
    var modules = RaycasterManager.GetRaycasters();
    for (int i = 0; i < modules.Count; ++i)
    {
        var module = modules;
        if (module == null || !module.IsActive())
            continue;

        module.Raycast(eventData, raycastResults);
    }

    raycastResults.Sort(s_RaycastComparer);
}

通过代码我们看到是通过RaycasterManager.GetRaycasters() 获取所有的BaseRaycaster对象,结果赋值给raycastResults ,然后进行排序。
排序的规则我们通过RaycastComparer接口就能一目了然的理解,不需要详细的介绍。

前面我们提到覆写 BaseRaycaster.Raycast接口 的对象有Physics2DRaycaster、PhysicsRaycaster和 GraphicRaycaster ,射线检测的内容之后我们再说。

那点击区域存在多个物体,哪个会先接收到触摸事件?相信看了上面的代码大家已经很清楚了。
var raycast = FindFirstRaycast(m_RaycastResultCache);
pointerData.pointerCurrentRaycast = raycast;

在射线检测对象经过排序后,通过FindFirstRaycast接口返回一个对象不为空的RaycastResult对象返回赋值给pointerData.pointerCurrentRaycast。

于是我们就得到了我们当前触摸要处理的对象。

四、触摸事件都有哪些?

触摸事件比较多,我们看下对应的接口都有哪些:
//EventSystem.cs
public interface IEventSystemHandler
{
}

public interface IPointerEnterHandler : IEventSystemHandler
{
    void OnPointerEnter(PointerEventData eventData);
}


public interface IPointerExitHandler : IEventSystemHandler
{
    void OnPointerExit(PointerEventData eventData);
}

public interface IPointerDownHandler : IEventSystemHandler
{
    void OnPointerDown(PointerEventData eventData);
}

public interface IPointerUpHandler : IEventSystemHandler
{
    void OnPointerUp(PointerEventData eventData);
}


public interface IPointerClickHandler : IEventSystemHandler
{
    void OnPointerClick(PointerEventData eventData);
}

public interface IBeginDragHandler : IEventSystemHandler
{
    void OnBeginDrag(PointerEventData eventData);
}

public interface IInitializePotentialDragHandler : IEventSystemHandler
{
    void OnInitializePotentialDrag(PointerEventData eventData);
}

public interface IDragHandler : IEventSystemHandler
{
    void OnDrag(PointerEventData eventData);
}


public interface IEndDragHandler : IEventSystemHandler
{
    void OnEndDrag(PointerEventData eventData);
}

public interface IDropHandler : IEventSystemHandler
{
    void OnDrop(PointerEventData eventData);
}


public interface IScrollHandler : IEventSystemHandler
{
    void OnScroll(PointerEventData eventData);
}

public interface IUpdateSelectedHandler : IEventSystemHandler
{
    void OnUpdateSelected(BaseEventData eventData);
}


public interface ISelectHandler : IEventSystemHandler
{
    void OnSelect(BaseEventData eventData);
}


public interface IDeselectHandler : IEventSystemHandler
{
    void OnDeselect(BaseEventData eventData);
}


public interface IMoveHandler : IEventSystemHandler
{
    void OnMove(AxisEventData eventData);
}

public interface ISubmitHandler : IEventSystemHandler
{
    void OnSubmit(BaseEventData eventData);
}

public interface ICancelHandler : IEventSystemHandler
{
    void OnCancel(BaseEventData eventData);
}
我们常用一般有点击,按下,抬起,移动,滚动等,我们游戏脚本只要继承上面我们需要的接口就可以实现我们想要的逻辑了。
Unity 也给我们提供了一个组件 EventTrigger,我们可以直接用。


那我们的游戏对象是怎么触发这些接口的呢?
我们看源码来个简单的分析
protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
{
    var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

    // PointerDown notification
    if (pressed)
    {
        pointerEvent.eligibleForClick = true;
        pointerEvent.delta = Vector2.zero;
        pointerEvent.dragging = false;
        pointerEvent.useDragThreshold = true;
        pointerEvent.pressPosition = pointerEvent.position;
        pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

        DeselectIfSelectionChanged(currentOverGo, pointerEvent);

        if (pointerEvent.pointerEnter != currentOverGo)
        {
            // send a pointer enter to the touched element if it isn't the one to select...
            HandlePointerExitAndEnter(pointerEvent, currentOverGo);
            pointerEvent.pointerEnter = currentOverGo;
        }

        // search for the control that will receive the press
        // if we can't find a press handler set the press
        // handler to be what would receive a click.
        var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);

        // didnt find a press handler... search for a click handler
        if (newPressed == null)
            newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        // Debug.Log("Pressed: " + newPressed);

        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;

        // Save the drag handler as well
        pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

        if (pointerEvent.pointerDrag != null)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);

        m_InputPointerEvent = pointerEvent;
    }

    // PointerUp notification
    if (released)
    {
        // Debug.Log("Executing pressup on: " + pointer.pointerPress);
        ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

        // Debug.Log("KeyCode: " + pointer.eventData.keyCode);

        // see if we mouse up on the same element that we clicked on...
        var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        // PointerClick and Drop events
        if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
        {
            ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
        }
        else if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
        {
            ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
        }

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

        if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

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

        // send exit events as we need to simulate this on touch up on touch device
        ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler);
        pointerEvent.pointerEnter = null;

        m_InputPointerEvent = pointerEvent;
    }
}

通过这个接口我们看到触摸事件都是通过ExecuteEvents类来实现的。主要用到了两个接口ExecuteHierarchy 和  Execute ,通过第三个参数传入EventFunction 类型的委托对象来触发对应的操作。我们来看下Execute接口  
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
    var internalHandlers = s_HandlerListPool.Get();
    GetEventList<T>(target, internalHandlers);

    for (var i = 0; i < internalHandlers.Count; i++)
    {
        T arg;
        try
        {
            arg = (T)internalHandlers;
        }
        catch (Exception e)
        {
            var temp = internalHandlers;
            Debug.LogException(new Exception(string.Format("Type {0} expected {1} received.", typeof(T).Name, temp.GetType().Name), e));
            continue;
        }

        try
        {
            functor(arg, eventData);
        }
        catch (Exception e)
        {
            Debug.LogException(e);
        }
    }

    var handlerCount = internalHandlers.Count;
    s_HandlerListPool.Release(internalHandlers);
    return handlerCount > 0;
}

private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler
{
    if (results == null)
        throw new ArgumentException("Results array is null", "results");

    if (go == null || !go.activeInHierarchy)
        return;

    var components = ListPool<Component>.Get();
    go.GetComponents(components);
    for (var i = 0; i < components.Count; i++)
    {
        if (!ShouldSendToComponent<T>(components))
            continue;

        results.Add(components as IEventSystemHandler);
    }
    ListPool<Component>.Release(components);
}

Execute 接口通过GetEventList获取go对象 身上所有的IEventSystemHandler组件,然后遍历执行相应的接口,这样就触发了我们真正的游戏层逻辑。是不是很简单。

以上就是我们要讲的关于事件系统的一个大致流程,希望对各位同学有所帮助。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-22 11:12 , Processed in 0.092434 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表