本文章向大家介绍UGUI源码解析--EventSystem系统,主要包括UGUI源码解析--EventSystem系统使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
1、EventSystem系统
看似名字很大,其实EventSystem处理和管理的是点击、触摸、键盘输入等事件,叫做InputEventSystem更为合适。
//系统输入模块
private List m_SystemInputModules = new List();
//当前输入模块
private BaseInputModule m_CurrentInputModule;
//当前选择GameObject
private GameObject m_CurrentSelected;
//处理的所有输入的EventSystem
private static List m_EventSystems = new List();
继承关系:
BaseInputModule抽象类
PointerInputModule抽象类
StandaloneInputModule类,面向“PC, Mac& Linux Standalone”这个平台的输入模块
TouchInputModule类,面向“IOS Android”等可触摸移动平台的输入模块
(注:在最新的2017.4UGUI源码中,TouchInputModule已经被弃用,触摸输入已经被集成到StandaloneInputModule。)
void Update()
EventSystem会在Update里每帧执行TickModules方法,调用每一个InputModule。
遍历m_SystemInputModules,判断这些module是否支持当前平台IsModuleSupported(),并且是否可激活ShouldActivateModule()。
如果m_CurrentInputModule为空,激活InputModule,设置m_CurrentSelected,实际上就是调用eventSystem.SetSelectedGameObject,然后有把符合条件的module便赋值给m_CurrentInputModule(当前输入模块)并break。
如果m_CurrentInputModule不为空,调用每一个InputModule的Process方法,先发送事件给被选择的GameObject(m_CurrentSelected),SendUpdateEventToSelectedObject,然后先处理触摸的一些事件ProcessTouchEvents(),再处理鼠标的一些事件ProcessMouseEvent()。
m_CurrentSelected大部分情况是Selectable组件(继承它的Button、Dropdown、InputField等组件)设置的。设置m_CurrentSelected,实际调用eventSystem.SetSelectedGameObject,会通过ExecuteEvents这个类对之前的对象执行一个被取消事件,且对新选中的对象执行一个被选中事件。这就是OnSelect和OnDeselect两个方法的由来。
EventSystem的RaycastAll方法
使用射线从相机到某个点(设为点E)投射到UI上,然后对所有投射到的对象进行排序,大致是远近排序。
RaycastAll会在PointerInputModule类的GetTouchPointerEventData和GetMousePointerEventData中调用,如果发生点击(或触摸)事件时,该事件影响的对象也会改变,通过RaycastAll方法(传入的PointerEventData的position作为点E)获得到第一个被射线照射到的对象,如果与之前的对象不同,则变更对象。(选择了新的对象,取消旧的对象)
ProcessTouchEvents()
遍历所有的inputTouch输入
GetTouchPointerEventData,获得第一个被射线照射到的对象
ProcessTouchPress
如果一直长按,触发ProcessMove和ProcessDrag方法。否则RemovePointerData
之后在ProcessXXX()方法中,传入相应的接口类型,调用ExecuteEvents.Execute()方法,执行事件。
ProcessMouseEvent()
获取鼠标的input输入
GetMousePointerEventData,获得第一个被射线照射到的对象
ProcessMousePress,依次触发左键,右键,中键的点击事件
之后在ProcessXXX()方法中,传入相应的接口类型,调用ExecuteEvents.Execute()方法,执行事件。
IsPointerOverGameObject
是EventSystem类里特别常用的一个方法,用于判断是否点击在UI上,具体是在PointerInputModule中实现的,判断最后一次点击的EventData数据是否为空,不为空即在UI上。
EventSystem current
最后我们注意到EventSystem有一个static属性:
public static EventSystem current
get { return m_EventSystems.Count > 0 ? m_EventSystems[0] : null; }
当一个EventSystem组件OnEnable的时候会将这个对象加入到m_EventSystems。
m_EventSystems.Add(this);
OnDisable的时候会将current从m_EventSystems移除
m_EventSystems.Remove(this);
2、执行事件
在上面的事件系统,其中我们讲到EventSystem可以通过ExecuteEvents这个类来执行事件,那么事件是如何执行的呢?这里涉及到了两个文件EventInterface和ExecuteEvents。
EventInterface类
EventInterface定义了一系列的跟输入有关的接口。例如IPointerEnterHandler(指针进入事件接口)。一个组件添加这个接口的继承之后,再实现OnPointerEnter方法,便可以接收到指针进入事件,也就是当鼠标滑入对象所在的区域之后,便会回调OnPointerEnter方法。这些接口全都继承自IEventSystemHandler,而后者也是声明在EventInterface里的接口。
例如:
public interface IPointerEnterHandler : IEventSystemHandler
{
void OnPointerEnter(PointerEventData eventData);
}
ExecuteEvents类
以上这些接口都会在ExecuteEvents里被调用。ExecuteEvents类是个静态类,不能被实例化,所有的公共方法都通过ExecuteEvents.XXXX来调用。ExecuteEvents里声明了一个delegate的类型EventFunction,这是一个泛型委托:
public delegate void EventFunction(T1 handler, BaseEventData eventData);
然后对EventInterface里的除IEventSystemHandler所有的接口声明了一个EventFunction类型的委托变量和方法。
例如:
private static readonly EventFunction
s_PointerEnterHandler = Execute;
private static void Execute(IPointerEnterHandler handler,
BaseEventData eventData)
{
handler.OnPointerEnter(ValidateEventData(eventData));
}
然后又声明了一系列属性,这些属性是获取上述委托变量的只读属性,用于在外部调用。
public static EventFunction
pointerEnterHandler
{
get { return s_PointerEnterHandler; }
}
而外部统一调用执行事件的方法是:
ExecuteEvents.Execute(t.gameObject, currentPointerData, ExecuteEvents.pointerEnterHandler);
在方法内部,通过GetEventList获得targetGameObject上的T类型的组件列表,然后遍历这些组件,并执行EventFunction委托functor(arg, eventData);。
以pointerEnterHandler为例,我们可以了解functor这个方法实际上执行的是我们上面声明的EventFunction类型的委托方法:
handler.OnPointerEnter(ValidateEventData(eventData));
也就是调用了IPointerEnterHandler类型的组件的OnPointerEnter方法。
至此,我们就了解到了UGUI里的事件是如何执行的:指定某个接口类型,由Execute方法调用目标对象的接口方法。
接着,补充一下ExecuteEvents类里面其他方法的介绍。
ExecuteHierarchy方法会通过GetEventChain获取target的所有父对象,并对这些对象(包括target)执行Execute方法。
GetEventHandler会遍历目标对象及其父对象,判断他们是否可以处理某个指定的接口事件,如果可以,把目标对象作为返回值返回。而判断方法是CanHandleEvent,通过GetEventList方法获取target上的T类型的组件列表,判断列表数量不为零。GetEventHandler主要在输入模块里被调用,用于获取某个输入事件的响应对象。
3、截取事件
有两种截取事件的方法:
第一种,你可以扩展EventTrigger,并覆盖你感兴趣截取的事件的函数(简单常用);
第二种,指定单个的委托事件。
第一种方式:
public class EventTriggerExample : EventTrigger
{
public override void OnBeginDrag(PointerEventData data)
{
Debug.Log("OnBeginDrag called.");
}
//省略其他重写的事件方法
}
第二种方式:
public class EventTriggerDelegateExample : MonoBehaviour
{
void Start()
{
EventTrigger trigger = GetComponent();
EventTrigger.Entry entry = new EventTrigger.Entry();
entry.eventID = EventTriggerType.PointerDown;
entry.callback.AddListener((data) => { OnPointerDownDelegate((PointerEventData)data); });
trigger.triggers.Add(entry);
}
public void OnPointerDownDelegate(PointerEventData data)
{
Debug.Log("OnPointerDownDelegate called.");
}
}
4、输入模块
在第一部分EventSystem我们探究了事件系统,在第二部分执行事件中我们介绍了事件是如何执行的。那么事件是如何产生的呢?这就涉及到BaseInputModule、PointerInputModule、StandaloneInputModule、TouchInputModule这些类。我们就探究一下输入模块的原理。
比如处理触摸事件时,EventSystem在OnUpdate()方法,执行ProcessTouchEvents()方法时,遍历所有的input.touchCount(input为BaseInput类),取得Touch对象。Touch对象包括,唯一ID:fingerId,位置信息等。
Touch touch = input.GetTouch(i);
取出的Touch对象,传入GetTouchPointerEventData方法,
GetTouchPointerEventData(touch, out pressed, out released);
在方法中,pointerData.position = input.position;
eventSystem.RaycastAll(pointerData, m_RaycastResultCache);
通过RaycastAll方法(传入的PointerEventData的position作为点E,从相机到点E投射一条射线,)获得到第一个被射线照射到的对象。最后执行ProcessTouchPress()方法。
这些事件是由输入模块产生的,而归根结底大部分是通过Input这个类的各种属性和静态方法获取了数据才生成了事件。
比如:
当鼠标或触摸进入、退出当前对象时执行pointerEnterHandler、pointerExitHandler。
在鼠标或者触摸按下、松开时执行pointerDownHandler、pointerUpHandler。
在鼠标或触摸松开并且与按下时是同一个响应物体时执行pointerClickHandler。
在鼠标或触摸位置发生偏移(偏移值大于一个很小的常量)时执行beginDragHandler。
在鼠标或者触摸按下且当前对象可以响应拖拽事件时执行initializePotentialDrag。
对象正在被拖拽且鼠标或触摸移动时执行dragHandler。
对象正在被拖拽且鼠标或触摸松开时执行endDragHandler。
鼠标或触摸松开且对象未响应pointerClickHandler情况下,如果对象正在被拖拽,执行dropHandler。
当鼠标滚动差值大于零执行scrollHandler。
当输入模块切换到StandaloneInputModule时执行updateSelectedHandler。(不需要Input类)
当鼠标移动导致被选中的对象改变时,执行selectHandler和deselectHandler。
UI组件的继承关系:(缩进表示子类)
5、CanvasUpdateRegistry
CanvasUpdateRegistry(画布更新注册处)是一个单例,它是UGUI与Canvas之间的中介,继承了ICanvasElement接口的组件都可以注册到它,它监听了Canvas即将渲染的事件,并调用已注册组件的Rebuild等方法。
CanvasUpdateRegistry维护了两个索引集(不会存放相同的元素):
//布局重建序列索引集
private readonly IndexedSet m_LayoutRebuildQueue = new IndexedSet();
//图形重建序列索引集
private readonly IndexedSet m_GraphicRebuildQueue = new IndexedSet();
m_LayoutRebuildQueue是通过RegisterCanvasElementForLayoutRebuild
和TryRegisterCanvasElementForLayoutRebuild方法添加元素。
m_GraphicRebuildQueue是通过RegisterCanvasElementForGraphicRebuild
和TryRegisterCanvasElementForGraphicRebuild方法添加元素。
二者通过UnRegisterCanvasElementForRebuild移除注册元素。
CanvasUpdateRegistry的构造函数:
protected CanvasUpdateRegistry()
{
Canvas.willRenderCanvases += PerformUpdate;
}
willRenderCanvases是Canvas的静态事件,事件是一种特殊的委托,在渲染所有的Canvas之前,抛出willRenderCanvases事件,继而调用CanvasUpdateRegistry的PerformUpdate方法。
public enum CanvasUpdate
{
Prelayout = 0,
Layout = 1,
PostLayout = 2,
PreRender = 3,
LatePreRender = 4,
MaxUpdateValue = 5
}
除了最后一个枚举项,其他五个项分别代表了布局的三个阶段和渲染的两个阶段。
在PerformUpdate方法中
从两个序列中删除不可用的元素 CleanInvalidItems();
布局更新开始
对m_LayoutRebuildQueue依据父对象的数量进行排序
分别以PreLayout,Layout,PostLayout的参数顺序调用每一个元素的Rebuild方法
调用所有元素的LayoutComplete方法
清除布局重建序列中的所有元素
布局更新结束
完成布局后,调用组件的修剪方法
图形更新开始
以PreRender,LatePreRender的参数顺序调用每一个元素的Rebulid方法
调用所有元素的GraphicUpdateComplete方法
清除图形重建序列中的所有元素
图形更新结束
至此,一个完整的更新流程就完成了。
6、Raycast
编程小技巧:
Mathf.Approximately(0.0f, projectionDirection); 比较两个float值,如果他们在很小的相差(Epsilon)内,返回true。
浮点不精确使得使用等号运算符比较浮点数不准确。例如,(1.0 == 10.0 / 10.0)每次都可能不会返回true。
BaseRaycaster是其他Raycaster的抽象基类,它在OnEnable()时,把自己注册到RecasterManger中,而在OnDisable()时,从RecasterManager中移除。
RecasterManager是一个静态类,维护了一个BaseRaycaster类型的List。EventSystem里也通过这个类来管理所有的射线照射器,也就是EventSystem.RaycastAll()方法。
PhysicsRaycaster(物理射线照射器)添加了特性
[RequireComponent(typeof(Camera))]
说明它依赖于Camera组件。它通过eventCamera属性来获取对象上的Camera组件。
Raycast方法重写了BaseRaycaster的同名抽象方法:
在2017.4UGUI源码中,采用ReflectionMethodsCache.Singleton.raycast3DAll()来获取所有射线照射到的对象,用反射的方式把Physics.RaycastAll()方法缓存下来,让Unity的Physics模块与UI模块,保持低耦合,没有过分依赖。
获取到被射线照射到的对象,根据距离进行排序,然后包装成RaycastResult,加入到resultAppendList中。EventSystem会将所有的Raycast的照射结果合在一起进行排序,然后输入模块取到第一个距离最近的对象作为目标对象。
Physics2DRaycaster继承自PhysicsRaycaster,其他都一样,只重写了Raycast方法,改为用Physics2D.RaycastAll来照射对象,在2017.4UGUI源码中也采用了反射的方式获取的方法,并且根据SpriteRenderer组件设置结果变量(在EventSystem里会作为排序依据,毕竟是2D对象)。
GraphicRaycaster继承自BaseRaycaster,它添加了特性:
[RequireComponent(typeof(Canvas))]
表示它依赖于Canvas组件(通过canvas属性来获取)。
它重写了三个属性sortOrderPriority、renderOrderPriority(获取Canvas的sortingOrder和renderOrder,这在EventSystem里会作为排序依据)和eventCamera(获取canvas.worldCamera,为null则返回Camera.main),当canvas.renderMode == RenderMode.ScreenSpaceCamera时,canvas.worldCamera不能为空,要把渲染UI的相机拖到为Canvas上的Render Camera。
首先,多屏显示的支持,然后把屏幕上的点转化为相机的视窗坐标,用于判断是否在视窗之中。然后,从相机发射一条射线,根据blockingObjects来判断采用raycast3D还是raycast2D的方式,取得hitDistance,也就是从射线的原点到撞击点的矢量的大小。
再调用静态方法Raycast,获得在射线照射区域的所有Graphic列表m_RaycastResults(调用Graphic.Raycast()方法判断,射线位置是否有效),接着遍历m_RaycastResults,判断Graphic的方向向量与eventCamera的方向向量是否相交,如果相交,然后再判断Graphic是否在eventCamera的前面,并且距离小于hitDistance,满足这些条件,才会把它打包成RaycastResult添加到resultAppendList里。
由此可见GraphicRaycaster与其他射线照射器的区别就在于,它把照射对象限定为了Graphic。
补充知识点,RaycastHit.distance:
在射线的情况下,距离表示从射线的原点到撞击点的矢量的大小
在扫描体积或球体投射的情况下,距离表示从原点到体积接触另一碰撞体的平移点的矢量的大小
领取专属 10元无门槛券
私享最新 技术干货