源码2:StandaloneInputModule
在上文EventSystem中,EventSystem调用了BaseInputModule的process函数,将输入的部分交给了输入模块去处理。根据处理流程的不一样,开发者可以实现不同的BaseInputModule子类,并重写process函数。Unity则是提供了默认的StandaloneInputModule类。这一篇文章先来了解BaseInputModule的继承结构,随后以process函数为入口,继续了解事件系统。
BaseInputModule 的继承结构
BaseInputModule抽象类
PointerInputModule抽象类 继承BaseInputModule
StandaloneInputModule类继承PointerInputModule
TouchInputModule类 继承PointerInputModule(已废弃 功能完全整合到了StandaloneInputModule)
上篇EventSytem 有说到 其主要是更新输入模块BaseInputModule 的process 函数 所以这个为输入响应入口
BaseInputModule
// 用来缓存射线投射后的取得的结果
[NonSerialized]
protected List<RaycastResult> m_RaycastResultCache = new List<RaycastResult>();
//轴向相关的数据
private AxisEventData m_AxisEventData;
private EventSystem m_EventSystem;
private BaseEventData m_BaseEventData;
AxisEventData 的定义也比较简单 一个原始的输入方向 移动朝向的美剧
其他
/// <summary>
/// Event Data associated with Axis Events (Controller / Keyboard).
/// </summary>
public class AxisEventData : BaseEventData
{
/// <summary>
/// Raw input vector associated with this event.
/// </summary>
public Vector2 moveVector { get; set; }
/// <summary>
/// MoveDirection for this event.
/// </summary>
public MoveDirection moveDir { get; set; }
public AxisEventData(EventSystem eventSystem)
: base(eventSystem)
{
moveVector = Vector2.zero;
moveDir = MoveDirection.None;
通过输入参数确定轴向数据 (移动朝向 和方向枚举)
/// <param name="deadZone">Dead zone.</param>
protected virtual AxisEventData GetAxisEventData(float x, float y, float moveDeadZone)
{
if (m_AxisEventData == null)
m_AxisEventData = new AxisEventData(eventSystem);
m_AxisEventData.Reset();
m_AxisEventData.moveVector = new Vector2(x, y);
m_AxisEventData.moveDir = DetermineMoveDirection(x, y, moveDeadZone);
return m_AxisEventData;
}
实际上在输入模块启用的时候 是添加了一个BaseInput 脚本 ,这个脚本实际上是对以前常用的InputSystem的封装 。所有的输入触摸都是通过这里做检测。
/// <summary>
/// The current BaseInput being used by the input module.
/// </summary
public BaseInput input
{
get
{
if (m_InputOverride != null)
return m_InputOverride;
if (m_DefaultInput == null)
{
var inputs = GetComponents<BaseInput>();
foreach (var baseInput in inputs)
{
// We dont want to use any classes that derrive from BaseInput for default.
if (baseInput != null && baseInput.GetType() == typeof(BaseInput))
{
m_DefaultInput = baseInput;
break;
}
}
if (m_DefaultInput == null)
m_DefaultInput = gameObject.AddComponent<BaseInput>();
}
return m_DefaultInput;
}
}
ventSystem调用Process接口处理输入以及事件。由子类去实现这个方法。
/// <summary>
/// Process the current tick for the module.
/// </summary>
public abstract void Process();
BaseInputModule的每帧更新函数,由EventSystem驱动。
/// <summary>
/// Update the internal state of the Module.
/// </summary>
public virtual void UpdateModule()
{}
PointerInputModule
PointerInputModule定义了一些点操作的变量以及方法。
m_PointerData用来记录当前点的数据,每有一个Touch输入,那么会缓存在m_PointerData记录中。
protected Dictionary<int, PointerEventData> m_PointerData = new Dictionary<int, PointerEventData>();
/// <summary>
/// Search the cache for currently active pointers, return true if found.
/// </summary>
/// <param name="id">Touch ID</param>
/// <param name="data">Found data</param>
/// <param name="create">If not found should it be created</param>
/// <returns>True if pointer is found.</returns>
protected bool GetPointerData(int id, out PointerEventData data, bool create)
{
if (!m_PointerData.TryGetValue(id, out data) && create)
{
data = new PointerEventData(eventSystem)
{
pointerId = id,
};
m_PointerData.Add(id, data);
return true;
}
return false;
}
这个是处理了触摸操作
/// Given a touch populate the PointerEventData and return if we are pressed or released.
/// </summary>
/// <param name="input">Touch being processed</param>
/// <param name="pressed">Are we pressed this frame</param>
/// <param name="released">Are we released this frame</param>
/// <returns></returns>
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;
}
GetMousePointerEventData()函数是处理鼠标输入 这里是个虚方法是支持重写
StandaloneInputModule
StandaloneInputModule是标准输入模块。鼠标输入和触摸出入都在这里管理
输入模块最终要的函数Process()
public override void Process()
{
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
return;
bool usedEvent = SendUpdateEventToSelectedObject();
// case 1004066 - touch / mouse events should be processed before navigation events in case
// they change the current selected gameobject and the submit button is a touch / mouse button.
// touch needs to take precedence because of the mouse emulation layer
if (!ProcessTouchEvents() && input.mousePresent)
ProcessMouseEvent();
if (eventSystem.sendNavigationEvents)
{
if (!usedEvent)
usedEvent |= SendMoveEventToSelectedObject();
if (!usedEvent)
SendSubmitEventToSelectedObject();
}
}
-
首先检查当前应用是否是focus状态,如果是非focus状态,且在该状态下是忽略事件处理的,那么跳过处理。
-
SelectedObject代表EventSystem当前记录的选中的UI。SendXXSelectedObject表示,是否要对当前的选中UI发送Update/Move/Submit/Cancel事件。Process会首先尝试执行update事件,同时记录usedEvent。usedEvent代表是否已经执行过事件,如果执行过,后续不执行,也就是同一帧里仅执行一次事件。如果未执行过事件且sendNavigationEvents,那么尝试继续处理Move事件以及Submit/Cancel事件。
-
sendNavigationEvents代表什么呢?主机游戏常用手柄操作,PC游戏常用键盘。它们都有上下左右,以及确认、取消,常用的六个按钮。sendNavigationEvents表示当按下手柄的方向键的时候,会尝试调用被选中UI的Move事件。当前选中UI会deselect,并且在当前选中UI的某一个方向上的UI会被Select选中。在游戏中这是非常常见的,比如在设置面板中有很多玩家可设置的选项,方向键就可以用来在这些选项中进行切换。
Move: 当按下上下左右方向键的时候,会根据当前SelectableObject的位置,查找对应方向的下一个SelectableObject,并Move至对应SelectableObject
Submit、Cancel: 对当前SelectableObject发送对应事件,例如当前SelectableObject是一个Button,那么按下Submit事件,就会触发Button的OnSubmit接口。
-
如果没有触发以上事件的话,那么Process会尝试处理鼠标或者触摸事件。代码是首先去处理Touch事件,如果没有点击事件,且存在鼠标,那么去处理鼠标事件。
处理触摸Touch事件
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;
//获取手指状态存储到pointer里
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;
}
这里是遍历每根手指的Touch示例,每一个都能得到一个PointerEventData ,这个包含了手指触摸后的很多信息 ,包括位置,拖动信息等等。同时根据手指按下抬起能获取到三个状态 从而可以处理 第一帧按下 中间拖动 最后一帧抬起的事件处理。
protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
{
PointerEventData pointerData;
//获取缓存的输入信息,没有就新创建(代表手指刚触摸) 参考上面PointerInputModule的方法
var created = GetPointerData(input.fingerId, out pointerData, true);
pointerData.Reset();
//这里判断是否手指刚按下(true 按下 false 不是刚刚下)
pressed = created || (input.phase == TouchPhase.Began);
//这里判断手指是否抬起释放
released = (input.phase == TouchPhase.Canceled) || (input.phase == TouchPhase.Ended);
//如果是第一次创建 保存输入位置信息
if (created)
pointerData.position = input.position;
//如果是刚按下 位置移动偏移初始化为(0,0)
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;
}
在这里要说明下TouchPhase 相关的内容 ,以前没有UGUI的时候 我们自己做触摸相关的都是通过这个枚举判断
//
// 摘要:
// Describes phase of a finger touch.
public enum TouchPhase
{
//
// 摘要:
// A finger touched the screen.
Began = 0,
//
// 摘要:
// A finger moved on the screen.
Moved = 1,
//
// 摘要:
// A finger is touching the screen but hasn't moved.
Stationary = 2,
//
// 摘要:
// A finger was lifted from the screen. This is the final phase of a touch.
Ended = 3,
//
// 摘要:
// The system cancelled tracking for the touch.
Canceled = 4
}
通过上面函数 就拿到了 三个状态 我们就可以处理4个事件
//press事件 和 release事件 刚好就是第一帧和最后一帧
protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
这个函数中要注意几行代码
// 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);
send exit events as we need to simulate this on touch up on touch device
ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler);
上面代码是通过当前对象沿着Hierarchy一层层往上找实现了对应 事件接口的对象 同时发布事件
相似的还有 ExecuteEvents.Execute 只再当前对象上发布事件 事件相关系统 后面再讲
//手指移动事件
/// <summary>
/// Process movement for the current frame with the given pointer event.
/// </summary>
protected virtual void ProcessMove(PointerEventData pointerEvent)
{
var targetGO = (Cursor.lockState == CursorLockMode.Locked ? null :
pointerEvent.pointerCurrentRaycast.gameObject);
HandlePointerExitAndEnter(pointerEvent, targetGO);
}
在手指按下移动过程中 我们可能会进入某些对象的Enter handler 离开某些Exit haandler
参数列表的第一个参数PointerData会记录之前的enter的GameObject(oldEnterObject),第二个参数则是新enter的GameObject(newEnterTarget)。
//拖拽事件
/// <summary>
/// Process the drag for the current frame with the given pointer event.
/// </summary>
protected virtual void ProcessDrag(PointerEventData pointerEvent)
{
if (!pointerEvent.IsPointerMoving() ||
Cursor.lockState == CursorLockMode.Locked ||
pointerEvent.pointerDrag == null)
return;
if (!pointerEvent.dragging
&& ShouldStartDrag(pointerEvent.pressPosition, pointerEvent.position, eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold))
{
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.beginDragHandler);
pointerEvent.dragging = true;
}
// Drag notification
if (pointerEvent.dragging)
{
// Before doing drag we should cancel any pointer down state
// And clear selection!
if (pointerEvent.pointerPress != pointerEvent.pointerDrag)
{
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
pointerEvent.eligibleForClick = false;
pointerEvent.pointerPress = null;
pointerEvent.rawPointerPress = null;
}
ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.dragHandler);
这段代码也比较清晰 就是主要处理拖拽的状态 处理拖拽的事件。
整体来说 触摸事件 结构写的还是蛮清晰。 主要是理解下 其中的一些手势判定,和对应的数据存储PointerData
鼠标操作类似 也是相关状态 (目前已经合并到标准输入模块中了 ,所以基本是无感)