(二)UGUI InputModule

1.前言

在上一篇中梳理了一下整个事件系统的流程,包括Workflow,此文则详细讲解一下InputModule本身,并对一些方法做一下解释。

2.综述

InputModule的结构如下所示:
在这里插入图片描述
BaseInputModule为原始基类,只包含最基本的功能。PointerInputModule主要功能是获取当前touch或者鼠标位置坐标、状态以及对应游戏物体信息。最终Standalone/TouchInputModule则进行事件判断与处理。当然页包括一些VR sdk中自定义的InputModule。

2.1 BaseInputModule

此类包含基本的功能包括模块启动管理、获取当前input模块、处理Enter/Exit以及其他辅助功能:
1)启动管理
即在OnEnable和OnDisable方法中处理的事件。即将此模块添加到添加到EventSystem的inputModule列表中。
2)获取当前Input模块
此input模块是指最基本的输入模块,基本上就是unity最基本的Input类的简单封装。用户可以自定义Input类,但是目前基本没有此类需求。
3)处理Enter/Exit
此功能对应代码如下所示,是比较重要的一个方法,但是也是最让人费解的方法。即使有注释也比较费解为什么要这么处理。如果只是处理Enter和Exit方法,则此段代码从 “if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget) return;”开始即可,后来从此方法的使用知道,最开始的一段代码是为了一些特定功能添加的。

  // walk up the tree till a common root between the last entered and the current entered is foung
        // send exit events up to (but not inluding) the common root. Then send enter events up to
        // (but not including the common root).
        protected void HandlePointerExitAndEnter(PointerEventData currentPointerData, GameObject newEnterTarget)
        {
            // if we have no target / pointerEnter has been deleted
            // just send exit events to anything we are tracking
            // then exit
            if (newEnterTarget == null || currentPointerData.pointerEnter == null)
            {
                for (var i = 0; i < currentPointerData.hovered.Count; ++i)
                    ExecuteEvents.Execute(currentPointerData.hovered[i], currentPointerData, ExecuteEvents.pointerExitHandler);

                currentPointerData.hovered.Clear();

                if (newEnterTarget == null)
                {
                    currentPointerData.pointerEnter = null;
                    return;
                }
            }

            // if we have not changed hover target
            if (currentPointerData.pointerEnter == newEnterTarget && newEnterTarget)
                return;

            GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);

            // and we already an entered object from last time
            if (currentPointerData.pointerEnter != null)
            {
                // send exit handler call to all elements in the chain
                // until we reach the new target, or null!
                Transform t = currentPointerData.pointerEnter.transform;

                while (t != null)
                {
                    // if we reach the common root break out!
                    if (commonRoot != null && commonRoot.transform == t)
                        break;

                    ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerExitHandler);
                    currentPointerData.hovered.Remove(t.gameObject);
                    t = t.parent;
                }
            }

            // now issue the enter call up to but not including the common root
            currentPointerData.pointerEnter = newEnterTarget;
            if (newEnterTarget != null)
            {
                Transform t = newEnterTarget.transform;

                while (t != null && t.gameObject != commonRoot)
                {
                    ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);
                    currentPointerData.hovered.Add(t.gameObject);
                    t = t.parent;
                }
            }
        }

4)其他
其他均为辅助方法,但是有个方法可能以后会用到,即寻找两个游戏物体的公共节点,如下

        protected static GameObject FindCommonRoot(GameObject g1, GameObject g2)
        {
            if (g1 == null || g2 == null)
                return null;

            var t1 = g1.transform;
            while (t1 != null)
            {
                var t2 = g2.transform;
                while (t2 != null)
                {
                    if (t1 == t2)
                        return t1.gameObject;
                    t2 = t2.parent;
                }
                t1 = t1.parent;
            }
            return null;
        }

2.2 PointerInputModule

此类继承BaseInputModule模块,作用很简单即获取当前touch或者鼠标的点信息,但是处理确立逻辑却比较繁琐,这是因为涉及到鼠标事件的左中右三个按键问题。此模块通过维护m_PointerData列表来处理各种功能。如下所示。

2.2.1 PointerData字典

通过一个问题来解释这个字典的作用。如何判断鼠标滑动时的delta?解决此问题需要把上一帧的鼠标位置记录下来,然后用当前帧鼠标的坐标去减上一帧的值。PointerData字典就是此作用,将当前帧Pointer信息记录下来,只是此字典记录的不止一个点。包括鼠标左右中三个按键(对应key值为-1,-2,-3)、虚拟按键值(key为-4)以及touch(key为touch的touchID)。

2.2.2 获取Touch事件PointerData

TouchPointerData的获取比较简单。即获取到当前touch数据,针对不同手指不同id去处理,但是大部分只有一个手指,获取当前点击状态。然后射线检测,获取当前检测的游戏物体。
代码如下:

       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;
        }

2.2.3 获取Mouse事件的数据

此部分比较麻烦,因为鼠标事件包含左右中三个,但是他们的位置以及射线检测到的游戏物体是相同的,唯一不同的是点击状态不同。所以针对这些问题定义了三个类:
1)MouseButtonEventData
此类只是在PointerData的基础上进行封装,增加了点击状态处理,即是否进行了点击。鼠标左右中三个按键都是由MouseButtonEventData表现。
2)ButtonState
此类是在MouseButtonEventData基础上的进一步封装。添加了按键位置参数,即表示此点击位置是左右中哪个按键进行了点击。个人认为可以完全跟上一个类合并。
3)MouseState
此类是在ButtonState的基础上继续进行的封装。通过m_TrackedButtons列表来维护三个按键的信息,所以列表中中最多只有三个元素,且除了刚开始运行时,有且只有三个元素。
所以获取mouse事件的数据时,返回值是一个MouseState,流程与Touch相同,只是增加了Copy数据的工程。因为左中右三个按键的位置与对应的游戏物体相同。

2.2.4 其他方法

还定义了其他一些方法,比如processMove、ProcessDrag等方法,其实是比较简单的。但是需要注意的一点是PointerEventData存储的数据包含上一帧的信息,所以处理ProcessMove时,只需要传入PointerEventData就可以。等处理结束后,下一帧未开始的时候一些数据才会统一,比如PointerEnter才会与当前射线检测的物体统一(此时不一定相同,因为接收PointerEnter的游戏物体有可能是当前游戏物体的父物体)。

2.3 StandaloneInputModule

此类才是真正去处理事件的类,是由Process处理的,首先处理Update/Move/Submit事件,然后处理ProcessTouchEvent和ProcessMouseEvent事件,而他们中的核心方法是ProcessTouchPress和ProcessMousePress。

2.3.1 Update/Move/Submit事件

这些事件比较处理比较简单,只是对当前选择是游戏物体,发送update、move以及submit事件。这些事件是pc端的事件,对应一些特殊的按键,比如Enter、esc以及ased字母键。

2.3.2 常规事件处理

常规事件是指常用到的事件比如drag、pointerEnter/exit以及click等事件,移动端和pc端分别是由ProcessTouchEvent和ProcessMouseEvent分别处理。但是不管是Touch还是Mouse,都是按照Press、Move和Drag进行处理。这里的Move与2.3.1中的move事件不同。此处的Move是处理PointerEnter/Exit事件,同理Press处理PointerDown、PointerClick以及PointerUp事件。

ProcessTouchEvent

此方法处理Touch事件,由于移动端不止一个手指触摸,所以所有的手指都要处理。思路是这样的:
(一)首先,如果在当前帧Pressed即手机触碰到触摸板,则处理PointerDown事件,同时记录当前游戏物体可能的Drag对象。
(二)如果当前帧没有Released,则根据上述的结果去处理Move和Drag(如果拖动距离大于EventSystem规定的距离才去执行)。Move处理的是PointerEnter/Exit事件,所以移动端Touch和pc端Mouse处理方式有所不同。
(三)如果当前帧Released(即手离开屏幕)则处理PointerUp事件(如果之前Pressed状态时如果没有处理PointerDown事件,且无click事件,则不会处理PointerUp事件),同时处理EndDrag和Drop事件(如果条件允许)。
此处存在一个问题,即不会单独处理PointerUp事件。不管是Mouse还是Touch,如果一个游戏物体既没有down事件也没有click事件,是不会触发PointerUp事件的。

ProcessMouseEvent

由于mouse事件不存在鼠标离开的问题,所以会一直处理ProcessMove。而且由于鼠标有左右中三个按键,所以针对Press和Drag都会进行判断。Press的处理流程与Touch基本相同(如上文所示)。区别则有两点,一个是touch按下时会判断是否处理Enter/Exit事件;另一个则是当鼠标Release时,mouse会判断是否处理Enter/Exit事件。

3.结语

以上是InputModule模块整个的事件处理流程。

原文地址:https://www.cnblogs.com/llstart-new0201/p/12632104.html