EventSystem基于Input,可以对键盘,鼠标,触摸,以及自定义输入进行处理。 EventSystem本身是一个管理控制器,核心功能依赖InputModule和Raycaster模块。
InputModule用来处理Input数据,管理事件状态,和发送事件给GameObject。
Raycaster用来捕获哪些GameObject需要执行事件处理:
Graphic Raycaster 用于UI元素就是继承自Graphic的对象;
Physics2D Raycaster 用于2D物理碰撞元素,依赖于Collider2D;
Physics Raycaster 用于3D物理碰撞元素,依赖于Collider;
原理
EventSystem对象负责管理所有事件相关对象,该对象下挂载了EventSystem组件和StandaloneInputModule组件,前者为管理脚本,后者为输入模块。 Canvas对象下挂载了GraphicRaycaster负责处理射线相关运算,用户的操作都会通过射线检测来映射到UGUI组件上,InputModule将用户的操作转化为射线检测,Raycaster则找到目标对象并通知EventSystem,最后EventSystem发送事件让目标对象进行响应。
事件响应方式1:实现IXXHandler接口,脚本挂在要点击的目标对象上面
public class EventTest : MonoBehaviour, IPointerClickHandler, IDragHandler,
IPointerDownHandler, IPointerUpHandler {
public void OnDrag(PointerEventData eventData) {
}
public void OnPointerClick(PointerEventData eventData) {
}
public void OnPointerDown(PointerEventData eventData) {
}
public void OnPointerUp(PointerEventData eventData) {
}
}
方式2:使用EventTrigger组件 EventTrigger组件是一个通用的事件触发器,它可以用来管理单个组件上的所有可能触发的事件,其使用方法有编辑器设定和动态设置两种。 编辑器设定方法是在指定组件上添加EventTrigger组件,然后为它添加触发事件类型,再为指定类型添加回调方法。这种做法的操作很简单,而且灵活性也相当高,想要跨脚本调用方法只需要鼠标拖一拖点一点就好。
想要更好地管理大量的事件触发和回调处理,尽量动态设置的方案:
protected void setupEventTrigger(GameObject target, UnityAction listener,
EventTriggerType type) {
if(target != null) {
EventTrigger trigger = target.GetComponent() as EventTrigger;
if(trigger == null) {
trigger = target.AddComponent();
}
trigger.triggers = new List();
EventTrigger.Entry entry = new EventTrigger.Entry();
entry.eventID = type;
entry.callback = new EventTrigger.TriggerEvent();
entry.callback.AddListener(listener);
trigger.triggers.Add(entry);
}
}
使用
UGUI使用 1、新建UGUI任意组件时,自动添加EventSystem对象(集成了EventSystem组件和StandaloneInputModule组件); 2、Canvas默认挂载了GraphicRaycaster组件,所以在Canvas对象之下的所有GUI对象都可以通过挂载脚本并且实现一些和事件相关的接口来处理事件; 3、参考“事件响应”章节,实现事件监听与处理;
场景物体使用 1、新建EventSystem对象; 2、Camera添加Physics2D Raycaster或者Physics Raycaster组件; 3、参考“事件响应”章节,实现事件监听与处理;
相关组件和类 EventSystem
1.负责InputModule的切换(因为现在游戏大部分都只有一个StanaloneInputModule,所以切换这部分可以先不考虑)。 2.负责InputModule的激活与反激活。3.负责Tick整个事件系统。 4.更新InputModule,处理失焦和记录鼠标位置。 5.记录一个Selected对象。
1.处理输入的鼠标或触摸事件,进行事件的分发。 2.激活和反激活时负责初始化(选择对象,鼠标位置)和清理无效数据(选择对象、pointerData)。 3.不直接使用Input获取数据,而使用一个MonoBehaviour进行封装,提供切换Input的能力(例如游戏进入了反转模式,点左下角时希望右上角有反应。那么重写一个对应的脚本,在进入这个模式时切换Input脚本就可以)。
1.找到所有被射线检测成功的对象,选排序后第一个对象进行事件分发。
1.负责获取和封装外部的输入信息,如点击、重力感应等。 2.BaseInput提供和Input类一样的能力,是对Input对象的封装,接口名字都一样,方便输入系统的切换。
Touch类1.Touch类是一个Touch行为(在屏幕上按下,抬起的过程算一个Touch行为)某一时刻的数据。 2.Touch类包含的信息。 主要分3部分: 每一个Touch行为从开始到结束,有一个唯一Id 当前这个Touch行为所处在的阶段,一共5个阶段
public enum TouchPhase
{
Began = 0,//按下
Moved = 1,//正在移动
Stationary = 2,//静止,但没有结束
Ended = 3,//离开
Canceled = 4//黑屏等其他因素导致的结束
}
当前位置,移动距离等信息。3.Touch行为在绝大多数情况下都是由Began开始,Ended或Canceled结束。 但是我们并没有监听Touch阶段修改的能力(没找到相关的接口),只能通过Input.GetTouch接口在某一时间点(如update中)来循环获取Touch信息。然后通过FingerId,phase来还原一个完整的Touch行为。但这样会有一个问题,通过GetTouch获取的Touch信息可能是不完整的,如: 1.在一个Touch拖动的过程中开始循环调用GetTouch,那么我们得到的Touch就会不是由Began开始的,而是由Moved开始的。 2.在帧数很低,且在一帧内连续点击多次时,可能出现相同FingerId的Touch没有通过Ended结束,然后又直接Began的情况。
所以在将GetTouch获取的数据作为EventSystem的输入数据时,需要将这些特殊情况考虑进去。
运行流程 总体流程以Touch举例tips: 1.PointerEventData可以理解为对Touch行为的进一步封装,记录了Touch行为信息,如开始位置等,且在此基础上增加了射线检测结果等信息。每一个PointerEventData的生命周期基本上和Touch行为相同。由pressed开始(对应Touch的Began,如缓存中没有对应fingerId的PointerEventData,则新建一个),released结束(对应Touch的Ended或Canceled,从缓存中移除该PointerEventData)。当然对于特殊情况要特殊处理(如上面提到的没有由Began开始的Touch等)。 2.Process主要的工作就是维护PointerEventData的数据,同时根据PointerEventData发出事件。 3.对事件脚本的查找是向上查找的,如C是B的子节点,B是A的子节点。射线检测的结果是C。那么会按C->B->A的顺序去查找可响应该事件的对象。
这里简单说一下GraphicRaycaster作为举例。
GraphicRaycaster的射线检测1.GraphicRaycaster是检测同gameobject下canvas中包含的所有Graphic元素是否被射线击中的脚本。 2.Graphic在Onenable,OnDisable,OnBeforeTransformParentChanged,OnTransformParentChanged,OnCanvasHierarchyChanged这几个时间点把自己加入或移除一个以canvas为键值的graphic集合的字典中。3.具体检测过程: 先从缓存中获取该Canvas下所有的Graphic对象。 处理多显示器问题,先做一波坐标转换。 根据BlockingObjects,对游戏中的3D或2D对象做一次射线检测,保存离相机最近的对象的距离,之后用于对结果的过滤。 先通过RectangleContainsScreenPoint判断射线击中点是否在Graphic的RectTransform中,再通过Graphic自身的Raycast函数进行进一步的检测(检测CanvasGroup,Active状态等)。 最后再做一些测试,如反转剔除,遮挡测试等。
射线检测及排序1.游戏中所有的Raycaster都进行一次射线检测,获取当前射线击中的所有物体,统一进行排序,选排序后的第一个对象作为射线检测的结果。 2.排序规则 不同Racaster下: camera.depth Raycaster.sortOrderPriority 针对ScreenSpaceOverlay Raycaster.renderOrderPriority 针对ScreenSpaceOverlay 相同Racaster下: sortingLayer sortingOrder depth distance index
举例在手机上按这个方式操作。 其中A上的脚本继承了IPointerEnterHandler,IPointerExitHandler,IPointerDownHandler,IPointerUpHandler,IPointerClickHandler,IDragHandler接口。 B上的脚本继承了IPointerEnterHandler,IPointerExitHandler, IDropHandler接口。这一系列操作中事件的触发主要依赖于对这几个变量的设置和判定。 ①pointerPress:按下时射线击中对象或向上查找的某一挂有继承了IPointerDownHandler或IPointerClickHandler脚本的对象。 ②pointerDrag:按下时射线击中对象或向上查找的某一挂有继承了IDragHandler脚本的对象。 ③pointerEnter:当前Touch位置发出的射线击中的第一个物体。 ④pointerCurrentRaycast:当前位置发出射线的计算结果,包括当前击中的物体等信息。1.按下一次完整的touch行为的开始,新生成一个PointerEventData加入缓存中 记录pressPosition,用于开始拖动的判定。 因为当前按下的位置在A上,所以设置pointerEnter为A。且A上的脚本继承了IPointerEnterHandler接口,所以执行A上的PointerEnter函数。 因为当前按下的位置在A上,且A上的脚本继承了IProinterDownHandler、IPointerDragHandler接口,所以设置pointerPress和pointerDrag为A。用于对后续抬起时的Click等事件做判定。同时执行PointerDown函数。2.拖动 在拖动距离超过Threshold之前什么都不做。超过后开始不停的执行pointerDrag(A)对象上的OnDrag函数。3.离开A 在离开A时,pointerEnter对象由A变为了null,所以执行pointerEnter(A)对象上的PointerExit函数。4.拖动 同2。5.进入B 在进入B时,pointerEnter对象由null变为了B,所以执行pointerEnter(B)上的PointerEnter函数。6.抬起touch行为的结束,从缓存中移除这个PointerEventData 执行pointerPress(A)对象上的PointerUp函数。 由于抬起时射线击中的对象是B,而不是pointerPress(A)对象。所以不执行pointerPress(A)对象上的OnClick函数,而执行B上的OnDrop函数。 执行B上的PointExit函数。
1.简单来说EventSysetm的处理过程就是循环获取Touch数据。根据Touch数据来推测完整的Touch行为,来维护对应的PointerEventData,在此基础上进行事件的计算和分发。 2.EventSystem的代码量比较少但特殊处理的地方还挺多的,毕竟一个完善的系统,所有情况都得考虑到位。所以阅读代码时可以先看最核心的Process相关的代码(Touch和Mouse先选一个),像InputModule切换、BaseInput的处理、Touch的特殊情况处理这些可以先略过,把握住核心思路之后再看这些部分。