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模块整个的事件处理流程。