一. 引言
上一节我们在讲EventSystem类中的某些函数细节的时候,经常会讨论到xxxInputModule这个输入模块,今天就来仔细讲一下xxxInputModule到底做了些什么。
附上UGUI源码
二. BaseInputModule
在具体介绍输入模块之前,我们先要知道输入模块的结构构成是如何的。
输入事件捕获模块由四个类构成:BaseInputModule、 PointerInputModule、 StandaloneInputModule、 TouchInputModule。
其中BaseInputModule是抽象基类,为子类提供必须的空接口和基本变量。我们先来具体看一下吧。
首先开头部分
[RequireComponent(typeof(EventSystem))]
说明BaseInputModule是依赖于EventSystem这个组件的,可以回看上一章讲EventSystem类时,在Unity创建Canvas会自动生成一个EventSystem的物体,其身上有EventSystem脚本和StandaloneInputModule脚本。
接下来举了个例子,用来表示我们可以通过创建自己的InputModule继承自BaseInputModule,用来引发某个事件并将此事件传递给某物体来处理。
public class MyInputModule : BaseInputModule
{
public GameObject m_TargetObject;
public override void Process()
if (m_TargetObject == null)
return;
ExecuteEvents.Execute (m_TargetObject, new BaseEventData (eventSystem), ExecuteEvents.moveHandler);
}
}
其中重写的Process()函数在BaseInputModule中是抽象函数,无具体功能实现,用于表示处理模块中事件的当前瞬间。
ExecuteEvents.Execute()则是将一个move事件传递给了m_TargetObject
ExecuteEvents会在日后再开一章具体讲解(挖坑呗)…
说了那么多其实还没开始介绍BaseInputModule下到底有哪些变量和函数…先给大家一个概念,知道BaseInputModule大概做些啥,我们接下来继续
声明的变量其实都是一些基本用来接收事件或者接收输入用的,这里大家自行看下源码即可。
我们看到OnEnable()和OnDisable()两个函数中,里面每次触发时都会调用EventSystem类中的UpdateModules(),用于更新当前模块列表,剔除不活跃的模块,具体请看EventSystem类
protected override void OnEnable()
{
base.OnEnable();
m_EventSystem = GetComponent<EventSystem>();
m_EventSystem.UpdateModules();
}
protected override void OnDisable()
{
m_EventSystem.UpdateModules();
base.OnDisable();
}
接下来就开始讲有用的函数了
/// <summary>
/// 返回第一个合法的RaycastResult
/// </summary>
protected static RaycastResult FindFirstRaycast(List<RaycastResult> candidates)
{
for (var i = 0; i < candidates.Count; ++i)
{
if (candidates[i].gameObject == null)
continue;
return candidates[i];
}
return new RaycastResult();
}
FindFirstRaycast()用于返回第一个RaycastResult,接收射线检测到的物体各类信息。
主要在PointerInputModule中有被调用进行数据处理。
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;
}
FindCommonRoot()用于给定两个物体,返回他们最近共同的根物体,这不就是二叉树找两节点的最近公共节点的方法么…
接下来的HandlePointerExitAndEnter()稍微有点复杂,主要用于当一个新的进入目标物体被发现,处理发送相应的进入和退出事件,截取了函数中关键内容,建议看下源码自行体会理解,代码中附带了一些自己的理解注释
GameObject commonRoot = FindCommonRoot(currentPointerData.pointerEnter, newEnterTarget);
// and we already an entered object from last time
if (currentPointerData.pointerEnter != null)
{
// 发送退出事件的调用给t.gameobject到commonRoot这条路径上所有的物体(不包括commonRoot)
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;
}
}
// 发送进入事件的调用给newEnterTarget到commonRoot这条路径上所有的物体(不包括commonRoot)
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;
}
}
public virtual bool IsPointerOverGameObject(int pointerId)
{
return false;
}
BaseInputModule中定义了IsPointerOverGameObject()传入指针id判断是否放在GameObject上,具体实现在PointerInputModule中实现,待会会讲。
剩余的就是对模块的一些激活和失效等函数定义了虚方法,由派生类PointerInputModule去实现。
三. PointerInputModule
PointerInputModule继承自BaseInputModule,在其基础上扩展了对与点位的输入逻辑,增加了输入类型和状态。而且PointerInputModule也是抽象类,供StandaloneInputModule和TouchInputModule继承。
首先定义了鼠标指针的id,其次建立了指针id与PointerEventData数据处理的字典,以及一些对字典的添加与删除函数 GetPointerData()和RemovePointerData()
// 建立指针id与数据的字典
protected Dictionary<int, PointerEventData> m_PointerData = new Dictionary<int, PointerEventData>();
// 根据id与bool create决定是否创建PointerEventData对象并建立字典映射
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;
}
// 从字典中移除PointerEventData
protected void RemovePointerData(PointerEventData data)
{
m_PointerData.Remove(data.pointerId);
}
protected PointerEventData GetTouchPointerEventData(Touch input, out bool pressed, out bool released)
GetTouchPointerEventData()通过给定触摸输入信息input来返回PointerEventData信息并且得知当前是指针是按下还是释放状态.
/// <summary>
/// Copy one PointerEventData to another.
/// </summary>
protected void CopyFromTo(PointerEventData @from, PointerEventData @to)
{
@to.position = @from.position;
@to.delta = @from.delta;
@to.scrollDelta = @from.scrollDelta;
@to.pointerCurrentRaycast = @from.pointerCurrentRaycast;
@to.pointerEnter = @from.pointerEnter;
}
CopyFromTo()是对PointerEventData数据对象的拷贝
接下来创建了两个类用于定义输入状态的类型和控制 ButtonState和MouseState,这里就不细讲了,有兴趣的自行查看
来看一个上次在EventSystem没有细讲的IsPointerOverGameObject(),也是在Unity中会经常用到的
public override bool IsPointerOverGameObject(int pointerId)
{
var lastPointer = GetLastPointerEventData(pointerId);
if (lastPointer != null)
return lastPointer.pointerEnter != null;
return false;
}
这个函数就是在EventSystem中,通过变量m_CurrentInputModule,可以得知是哪个模块正在处理事件,其次再调用IsPointerOverGameObject(),传入一个指针id,进而判断该指针id是否落在物体上
其中的函数GetLastPointerEventData()代码如下,其中的GetPointerData()就是开头介绍PointerInputModule中,根据指针id来获取到字典中相应PointerEventData数据的函数。
这个data数据里就包含了是哪个物体等各种位置信息。真的是一环套一环…这种编码思想值得学习…
protected PointerEventData GetLastPointerEventData(int id)
{
PointerEventData data;
GetPointerData(id, out data, false);
return data;
}
四. StandaloneInputModule
StandaloneInputModule模块主要是针对鼠标/键盘/控制器的输入。 继承自PointerInputModule。
直接看代码!
private float m_PrevActionTime; // 记录上一个动作的时间
private Vector2 m_LastMoveVector; // 记录上一个移动的方向向量
private int m_ConsecutiveMoveCount = 0; // 连续移动次数
private Vector2 m_LastMousePosition; // 记录上一次鼠标位置
private Vector2 m_MousePosition; // 记录当前鼠标位置
private GameObject m_CurrentFocusedGameObject; // 记录当前聚焦的物体
private PointerEventData m_InputPointerEvent; // 输入事件
[SerializeField]
private string m_HorizontalAxis = "Horizontal";
/// <summary>
/// Name of the vertical axis for movement (if axis events are used).
/// </summary>
[SerializeField]
private string m_VerticalAxis = "Vertical";
/// <summary>
/// Name of the submit button.
/// </summary>
[SerializeField]
private string m_SubmitButton = "Submit";
/// <summary>
/// Name of the submit button.
/// </summary>
[SerializeField]
private string m_CancelButton = "Cancel";
[SerializeField]
private float m_InputActionsPerSecond = 10;
[SerializeField]
private float m_RepeatDelay = 0.5f;
[SerializeField]
[FormerlySerializedAs("m_AllowActivationOnMobileDevice")]
private bool m_ForceModuleActive;
这部分是不是和上一章Unity面板上显示的一样,主要是可以自定义修改数据,进行一些输入上的微调
函数那块许多都是对于操作上一些逻辑处理…看的有点心烦…就讲个关键的Process()吧,之前在讲BaseInputModule中有介绍到过,这里是重写了基类的方法,记住Process()函数是每个间隔tick都会发送一个事件给目标物体
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();
// 判断EventSystem是否允许导航事件(move/ submit/ cancel)
if (eventSystem.sendNavigationEvents)
{
if (!usedEvent)
// 发送移动事件给物体 返回是否事件触发了
usedEvent |= SendMoveEventToSelectedObject();
if (!usedEvent)
// 发送提交事件给物体
SendSubmitEventToSelectedObject();
}
}
这边省略了许多函数的具体实现讲解,建议大家自行看一下,比较好理解。
五. TouchInputModule
TouchInputModule主要用于对触摸板的输入事件,继承自PointerInputModule
绝大多数函数与StandaloneInputModule中实现思路相似,就不重新赘述了,大家自行查看源码了解一下就行了(你不就是想偷懒么…)
六. 总结
输入事件模块这四个类中,实际主要被运用到的就是StandaloneInputModule和TouchInputModule(在EventSystem中的m_SystemInputModules列表中),它们继承自PointerInputModule,分别处理各自适用类型的输入逻辑处理,并将触发的事件传递给相应的物体进行处理。主要函数依靠Process()进行上述操作。