Input Modules 在 Unity 中扮演了什么样的角色?
前言
Input Modules 是 Event System 组成的一部分,负责产生抽象事件并分发事件到 GameObject 处理;在代码架构上它也是 Event System 中处理大部分业务逻辑的地方,可以高度配置化。不同的 Input Module 可以映射外部不同的硬件输入至 Unity Event System 中转化成能处理的事件,并管理这些事件的状态。Unity 中内置了 Standalone Input Module 和 Touch Input Module(已经被标记为 obsolete
,现在由 Standalone Input Module 一并处理)。
本篇文章就来剖析 Input Modules 在 Unity 中的实现。
BaseInputModule
BaseInputModule 是所有关于 Input Module 类的最上层抽象类,它包含一些通用的成员变量(如 List<RaycastResult> m_RaycastResultCache 用于存储射线检测时的结果)和方法(如
abstract void Process()` 定义了 Input Module 的处理事件行为)。
BaseInputModule 是一个 MonoBehaviour 组件,所以当挂载在某个 GameObject 上运行时也会拥有 Unity MonoBehaviour 的生命周期。BaseInputModule 覆写了 OnEnable
和 OnDisable
方法,如下:
|
|
|
|
在 enable 和 disable 的时候,都调用了 EventSystem 的 UpdateModules
方法去更新当前 EventSystem 中处于可用状态的 BaseInputModules。
BaseInputModule 是一个抽象类,因为它其中定义了唯一的一个抽象方法 Process
,如下:
|
|
这个方法就是不同 Input Module 子类实现具体事件抽象产生、分发的地方。
在 BaseInputModule 类还有一个比较重要的方法 HandlePointerExitAndEnter
,用来处理当 pointer 的移出或进入某个 GameObject 时候,发送 enter 或 exit 事件,方法定义如下:
|
|
方法具体实现中,首先检测是否需要处理退出。当没有 newEnterTarget 或者当前的 currentPointerData 的 pointerEnter 被清除,则给当前 currentPointerData 中所有的 hovered 对象发送 exit 事件并清除所有 hovered 对象;此时如果 newEnterTarget 不为空,说明是进入了新的 GameObject 对象,否则则将当前的 currentPointerData 的 pointerEnter 也置空并退出。代码如下:
|
|
紧接着判断是否事件是否是在同一个 target 上面,如果是则返回。否则需要处理事件对象切换过程中事件的产生和分发,如下:
|
|
上面的代码中,当当前进入的对象(newEnterTarget)和上次进入的对象(currentPointerData.pointerEnter)不同的时候,首先调用 FindCommonRoot
寻找它俩的共同父节点(commonRoot),对从上次进入的对象到 commonRoot 之间所有的对象节点(不包括 commonRoot)依次发送 exit 事件;对从 newEnterTarget 到 commonRoot 之间所有的对象节点(不包括 commonRoot)依次发送 enter 事件。
除了上面介绍的几个(抽象)方法之外,BaseInputModule 类还有一些静态辅助方法和一些虚方法。静态辅助方法如: FindFirstRaycast
获取射线检测的第一个结果;方法 DetermineMoveDirection
可以根据输入值确定当期那的移动方向;FindCommonRoot
方法返回两个对象第一个共同拥有的节点。虚方法如: GetAxisEventData
根据输入数据产生一个 AxisEventData(可复用);GetBaseEventData
方法用于复用当前实例的 m_BaseEventData。
PointerInputModule
PointerInputModule 继承自 BaseInputModule 类,它里面定义了一些常用的事件相关的成员变量和方法。类 StandaloneInputModule 和 TouchInputModule 继承自该类。
在成员变量中,有一个 m_PointerData 成员变量如下:
|
|
这个成员变量是当前 Input Module 中的事件的生产者的 ID 以及事件数据对象 PointerEventData 之间的一个映射。
在 PointerInputModule 类中,有几个比较重要的方法:
bool GetPointerData(int id, out PointerEventData data, bool create) 方法
用来根据某个事件生产者的 ID 获取 m_PointerData 映射的事件对象,返回 true
表示为该 ID 创建了一个新的事件,否则返回 false
。
PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released) 方法
根据一个 Touch 生成一个点击事件 PointerEventData,同时指定当前帧是否有按下(pressed)、释放(released)操作。如何根据一个 Touch 事件就确定当前是否有按下和释放操作了?下面就来看看该方法具体的实现。
|
|
在该方法中,首先使用上面介绍过的 GetPointerData
方法去获取一个 PointerEventData,如果该 PointerEventData 是新创建的或者当前处理的 Touch 的 TouchPhase 等于 TouchPhase.Began
(触摸开始阶段),则 pressed 置为 true
表示按下;如果 TouchPhase 等于 TouchPhase.Canceled
(触摸被动结束)或 TouchPhase.Ended
(触摸主动结束),则 released 被置为 true
表示释放。
紧接着,设置 PointerEventData 的相关信息。
如果是新创建的事件实例,则 PointerEventData 的 position 就是 Touch 的 position;
如果当前帧被判定为有按下操作,则与上一帧的事件点击位置的变化量 delta 为 0,否则事件点击位置变化量就是
input.position - pointerData.position
;接着更新 position 为 Touch 的 position;鼠标按键 button 被置为
PointerEventData.InputButton.Left
;最后是为当前事件关联的射线检测结果 pointerCurrentRaycast 设置值,如果当前处理的 Touch 的被动中断(
input.phase == TouchPhase.Canceled
),pointerCurrentRaycast 被设置为一个新的 RaycastResult,如没有被中断则调用 EventSystem 的RaycastAll
方法进行射线检测,并将射线检测的第一个结果作为 pointerCurrentRaycast 的值。
设置结束,返回该 PointerEventData。
MouseState GetMousePointerEventData(int id) 方法
用于获取当前的 MouseState。在分析这个方法之前,先来看看再 PointerInputModule 类中定义的三个内部类。
MouseButtonEventData 类
这个类中包含一个
PointerEventData.FramePressState
类型的 buttonState 变量,表示当前帧 button press 的状态。它是一个枚举类型,有Pressed
(按下)、Released
(释放)、PressedAndReleased
(按下且释放)和NotChanged
(状态同上一帧没有变化)四个值;类中还包含另一个PointerEventData
类型的变量 buttonData,代表着当前关联事件数据对象。该类中也定义了bool PressedThisFrame()
和bool ReleasedThisFrame()
两个方法判断当前帧中 button 是否被按下和是否被释放。ButtonState 类
ButtonState 类更加单,仅包含两个成员变量:
PointerEventData.InputButton
类型的成员变量 m_Button 以及MouseButtonEventData
类型的 m_EventData。MouseState 类
MouseState 包含一个
ButtonState
列表,用于同时管理多个 ButtonState。可以使用bool AnyPressesThisFrame()
和bool AnyReleasesThisFrame()
方法来判断当前帧是否有 button 被按下或被释放。
看完这几个简单的内部类,再回到 GetMousePointerEventData
方法。这个方法生成 PointerEventData 的过程同 GetTouchPointerEventData
方法相似,不同的是其 position 赋值是当前类(继承自 BaseInputModule)的 input
所在的 mousePosition 值,而 GetTouchPointerEventData
方法中使用的是 Touch 的 position;另一处不同是在当前方法中,会生成三个 PointerEventData 分别给 InputButton 中的 Left、Right 和 Middle 对应的三个 ButtonState 使用,生成好的数据会赋值给当前类的 m_MouseState
变量。
virtual void ProcessMove(PointerEventData pointerEvent) 方法
虚方法 ProcessMove
用于处理“移动”事件,当在一个 GameObject 上产生按下事件并且没有释放则会产生“移动”事件。该方法代码很简单,如果此时 Cursor.lockState 处于 CursorLockMode.Locked
状态,那么目标移动对象会被置为空,否则被设置为当前射线检测得到的对象;移动过程中需要 BaseInputModule 类的 HandlePointerExitAndEnter
方法检测是否需要改变事件 hover 的对象。
virtual void ProcessDrag(PointerEventData pointerEvent) 方法
“移动”事件处理完之后,紧接着虚方法 ProcessDrag
处理“拖拽”事件。代码如下:
如果当前 PointerEventData 还不是处于 dragging 并且可以开始拖拽,则为 PointerEventData 的 pointerDrag 发送 ExecuteEvents.beginDragHandler
事件。
|
|
在开始拖拽之前,将所有有关 press 的状态删除,同时清除 selection。如下:
|
|
在 PointerInputModule 类中,除了上面的几个比较重要的方法,还有一些简单的辅助方法或者其他方法,如 DeselectIfSelectionChanged
用来在 Selection 改变的时候反注册 Event System 的 Selected GameObject (最终调用的也是 EventSystem 的 SetSelectedGameObject
方法);再比如 ClearSelection
用来清除当前所有缓存的 PointerEventData 中的 Selection(发送 ExecuteEvents.pointerExitHandler
事件,清空 hovered)。
到这里可能会感觉全是干涩的代码分析,没有应用到真正的 Event System 的流程中。那么接下来,就从 StandaloneInputModule 类开始,看看一个具体的 Input Module 是如何工作的以及再来看看上面讲到过的方法具体的应用。
Standalone Input Module
这种模式是为类似鼠标、控制器等硬件(也包括触摸设备)的输入抽象设计的。根据不同的输入,将会转换成为 Unity 内不同的抽象事件被分发出去,然后被拦截处理。
当产生事件时,自定义配置的 Raycaster 将会计算拦截事件的元素对象;同时,对于 Navigation events,也可以设置要检测的名称。StandaloneInputModule 类有以下属性可以自定义配置:
属性 | 描述 |
---|---|
Horizontal Axis | Input Module 中水平轴所在的名字(Input Manager 中预设的值) |
Vertical Axis | 垂直轴所在的名字(Input Manager 中预设的值) |
Submit Button | Submit button 所在的名字(Input Manager 中预设的值) |
Cancel Button | Cancel button 所在的名字(Input Manager 中预设的值) |
Input Actions Per Second | 每秒允许最大输入事件次数 |
Repeat Delay | 重复输入延迟(单位: 秒) |
Force Module Active | 开启将强制 Standalone Input Module 处于激活状态 |
Vertical / Horizontal 轴用于键盘/控制器的导航性事件
Submit / Cancel button 用来发送 submit 和 cancel 事件
事件和事件之间有超时机制,每秒中最多有 Input Actions Per Second 个事件
究竟 Standalone Input Module 是如何应用到 EventSystem 流程中的了?前面在解析 BaseInputModule 类时提到过,在 OnEnable
方法中会将当前绑定的 Input Module 组件和 EventSystem 关联起来(通过 EventSystem 类的 UpdateModules
方法);然后在 EventSystem 每一帧更新中,都会调用 Input Module 的 Process()
方法,这样 Input Module 就开始运作起来。
void Process() 方法
StandaloneInputModule 类覆写了 Process()
方法,并在其中实现了各类事件的处理。所以接下来就从 StandaloneInputModule 类的 Process()
入手开始分析。
首先是处理当前被 Selected 对象的 Update 事件,代码如下:
|
|
SendUpdateEventToSelectedObject()
方法首先判断当前 EventSystem Selected 的对象是否为空,不为空则获取当前事件数据 m_BaseEventData,然后发送 ExecuteEvents.updateSelectedHandler
事件供拦截处理,返回结果表示当前的事件数据 m_BaseEventData 是否被使用。
然后使用 ProcessTouchEvents()
方法处理 Touch 事件,方法返回 true
表示处理了 Touch 事件;
该方法主要是处理 Touch 事件。对于当前 Input 中的每个 Touch 事件,都会执行以下操作: 首先调用 PointerInputModule 中的 GetTouchPointerEventData
方法根据 Touch 事件获取一个 PointerEventData,并判断当前 Touch 的状态是 pressed 还是 released 状态。接着调用 ProcessTouchPress
方法处理 Touch 按下,下面就来看看处理 Touch press 的过程: 具体下来就有 press 和 release 两大块的实现。
- press 处理
如果方法传入参数 pressed
为 true
,表示当前事件是新创建或 Touch phase 处于 TouchPhase.Began
阶段,所以需要处理 press 相关的事件。在处理 press 中,首先初始化了当前 PointerEventData 的相关值,接着调用了 PointerInputModule 中的 DeselectIfSelectionChanged
方法检测是否需要删除当前 EventSystem 的 Selected 对象(比如点击了新的对象,就会删除旧的 Selected 对象,然后由当前的 press 决定新的 Selected 对象)。处理 press 包含如下几个阶段:
如果当前 PointerEventData 的
pointerEnter
对象和射线检测的对象不是同一个,那么就会调用 BaseInputModule 类的HandlePointerExitAndEnter
方法给射线检测到的对象及其祖先发送ExecuteEvents.pointerEnterHandler
事件。处理完 pointer enter,紧接着处理 pointer down 事件;从当前射线检测到的对象开始搜寻能处理
ExecuteEvents.pointerDownHandler
事件的对象。代码如下:
|
|
- 若未能处理 press,接着寻找能处理 click 事件的对象。代码如下:
|
|
- 得到 newPressed 对象之后,接着对 PointerEventData 相关属性赋值。然后开始处理
ExecuteEvents.initializePotentialDrag
事件,代码如下:
|
|
到这里,press 阶段相关的事件就处理完毕。接下来就是 release 相关部分的实现。
- release 处理
如果方法传入参数 released
为 true
,表示当前 Touch 已经被系统中断或已经结束,所以需要处理 release 相关会触发的事件。
- 对 press 的对象进行
ExecuteEvents.pointerUpHandler
事件处理。代码如下:
|
|
- 紧接着寻找是否有处理 pointer click 事件的对象,如果有且该对象是当前 PointerEventData 的 pointerPress,那么就处理
ExecuteEvents.pointerClickHandler
事件;否则如果当前 PointerEventData 的 pointerDrag 不为空且正在拖拽中,就处理ExecuteEvents.dropHandler
事件。
|
|
- 最后根据相应条件处理结束拖拽(
ExecuteEvents.endDragHandler
)和退出(ExecuteEvents.pointerExitHandler
)事件。
|
|
到这里,release 相关部分也完成了。
处理完 Touch press 相关事件后,然后根据 released 状态,如果为 false
(未释放)则继续调用 ProcessMove
和 ProcessDrag
方法分别处理移动和拖拽事件(这两个事件的处理是直接使用 PointerInputModule 类中的方法,上面已经讲解过),如果已经 released 就将得到的 PointerEventData 从当前的 Input Module 中的 m_PointerData 移除。最后返回是否处理了至少一个 Touch 事件。

上图示 Touch 事件处理流程
如果 Touch 事件处理失败并且检测到了鼠标事件,那么就使用 ProcessMouseEvent()
方法处理鼠标事件;
ProcessMouseEvent()
方法用来处理所有的鼠标事件,它内部调用了重载的 ProcessMouseEvent(int id)
方法处理。鼠标事件的处理过程中,首先调用 PointerInputModule 类的 GetMousePointerEventData
方法(上面分析过该方法)获得 MouseState 对象;然后根据得到的 MouseState 对象调用 ProcessMousePress
方法处理鼠标左键的按下,同样下面就来看看处理鼠标 press 的过程: 具体下来也是 press 和 release 两大块的实现。
鼠标 press 处理和 Touch press 处理时使用
ProcessTouchPress
方法内部实现基本一致,但有一点不同就是: 在进行检测是否需要删除当前 EventSystem 的 Selected 对象之后,紧接着不会调用 BaseInputModule 类的HandlePointerExitAndEnter
方法给射线检测到的对象及其祖先发送ExecuteEvents.pointerEnterHandler
事件(这一步被移至 release 阶段处理)。release 处理也和 Touch 处理时
ProcessTouchPress
方法中 release 阶段一致,只不过在该阶段最后一步,鼠标事件 enter 和 exit 会被重新刷新。因此在鼠标事件的 press 处理中,在 release 阶段最后一步,当前射线检测到的对象及其祖先对象才会收到ExecuteEvents.pointerEnterHandler
事件,上一个触发鼠标事件的对象(如不为空)及其相关祖先对象才会接收到ExecuteEvents.pointerExitHandler
事件。
处理完鼠标左键 press,接下来就调用 PointerInputModule 类的 ProcessMove
方法处理移动事件,前面讲到过 ProcessMove
方法调用 BaseInputModule 类的 HandlePointerExitAndEnter
方法,从而当前射线检测的对象会接收到 ExecuteEvents.pointerExitHandler
事件。
处理完移动事件,最后就是处理鼠标左键的拖拽事件,调用 PointerInputModule 类的 ProcessDrag
方法处理。这个方法之前分析过,这里不再讲解。
到这里鼠标左键处理完毕,接着使用同样的方法对鼠标右键和中间键进行 press 和 drag 处理(不需要进行 move 事件处理)。最后在 ProcessMouseEvent
方法中处理的就是滚轮事件,代码如下:
|
|
当 scrollDelta 不近似为 0 时,就发送 ExecuteEvents.scrollHandler
事件。

上图示 Mouse 事件处理流程
Touch / 鼠标事件处理完毕后,紧接着如果当前需要处理 Navigation 事件。
因为在处理 Touch / 鼠标事件时会改变当前 Selected 对象,所以要先处理 Touch / 鼠标事件,然后按需处理Navigation 事件。
首先处理 Navgation 中当前 Selected 对象的 Move 事件,代码如下:
|
|
在上面处理 ExecuteEvents.updateSelectedHandler
事件时,如果收到事件的对象没有使用 BaseInputModule 中的事件 m_BaseEventData
,那么就开始对当前 EventSystem Selected 的对象处理 move 事件,调用 SendMoveEventToSelectedObject()
方法,这个方法代码主要就是处理 move 事件。
- 首先就是判断是否有按下轴对应的按键,如果没有则返回,如下:
|
|
- 接着检测是否再次按下了轴按键。如果没有再次按下(上次按下后没有松开)且当前按键得到的方向与上一次同向,则等待延时 m_RepeatDelay(这个就是上面提到过可以配置的重复按键的延时时间);若方向改变了 90 度以上或者已经超过了延时时间,则根据当前输入速率是否超过每秒允许输入的最多次数(m_InputActionsPerSecond,也是可以配置的)来判定是否需要处理当前 move,需要处理则进入下一步。这一步的最开始判定如果是再次按下轴按键,那么也是自动进入下一步。
|
|
- 到了这一步,就开始处理 move 事件了。首先获取轴按键按下方向得到事件数据 axisEventData,如果当前按下事件的方向 moveDir 不等于
MoveDirection.None
,就对当前被 Selected 的对象发送ExecuteEvents.moveHandler
事件;否则设置m_ConsecutiveMoveCount
(连续移动计数)为 0。代码如下:
|
|
SendMoveEventToSelectedObject()
方法返回结果为 axisEventData 事件数据是否被使用。
如果上一步处理 move 事件,axisEventData 未被使用,那么最后就会调用 SendSubmitEventToSelectedObject()
方法处理当前 Selected 对象的 Submit 事件,代码如下:
|
|
代码很简单,如果当前按下了 m_SubmitButton 对应的按键,就发送 ExecuteEvents.submitHandler
事件;如果按下 m_CancelButton 按键,就发送 ExecuteEvents.cancelHandler
事件。
到这里 Process()
就分析完了,在 Event System 中,每一帧都会调用该方法。因此事件得以在整个系统中不断产生和分发,然后被拦截处理。

上图示 Standalone Input Module 中事件流处理
StandaloneInputModule 类的主要方法在这里就分析完了。将这些方法串联起来,我们知道了 Standalone Input Module 设计的目的以及其在 Event System 中所扮演的角色。整个 Event System 中主要的逻辑大部分是在 Standalone Input Module 中实现的,包括: 外部事件的输入抽象为 Unity 组件可处理的事件、事件流动的过程、射线检测响应事件的对象、分发事件等等。
Touch Input Module
Touch Input Module 主要是为可触摸设备设计的,对于用户的输入它可以转化为 touching 和 dragging 等事件供 Unity 组件处理。TouchInputModule 类已经被标记为废弃(obsolete),在 StandaloneInputModule 类中已经有了所有关于 Touch Input Module 的实现。