源码解析之事件系统(4)"/>
Unity中的UGUI源码解析之事件系统(4)
Unity中的UGUI源码解析之事件系统(4)-ExecuteEvents
今天介绍消息系统: ExecuteEvents.
Unity实现的消息系统很简单, 一个静态类加一堆接口, 在处理事件时动态获取需要处理事件的对象, 几乎没有状态维护, 虽然每次处理事件都需要进行获取, 会损失一部分性能, 但是由于每个对象上的组件一般不会太多, 这个性能损失几乎可以忽略不计, 而带来的优势就是去除了大部分的状态管理, 极大的降低了维护和理解难度.
EventInterfaces
Unity通过接口(Interface)定义时间, 所有继承了IEventSystemHandler这个接口的接口, 或者实现了这个接口的类可以被Unity看做是一种事件.
在EventInterfaces定义了IEventSystemHandler和各种事件接口, 括号内是所属事件需要的事件数据类型:
public interface IEventSystemHandler(PointerEventData)
: 所有事件的祖先接口public interface IPointerEnterHandler(PointerEventData)
: 进入事件, 也就是鼠标进入首次进入对象区域public interface IPointerExitHandler(PointerEventData)
: 离开事件, 也就是鼠标进入首次离开对象区域public interface IPointerDownHandler(PointerEventData)
: 按下事件public interface IPointerUpHandler(PointerEventData)
: 抬起事件public interface IPointerClickHandler(PointerEventData)
: 点击事件, 即短时间按下又抬起public interface IInitializePotentialDragHandler(PointerEventData)
: 找到可拖动对象后, 真正开始拖动之前的事件, 整个拖动过程中只发送一次public interface IBeginDragHandler(PointerEventData)
: 开始拖动事件, 整个拖动过程中只发送一次public interface IDragHandler(PointerEventData)
: 拖动事件public interface IEndDragHandler(PointerEventData)
: 停止拖动事件, 整个拖动过程中只发送一次public interface IDropHandler(PointerEventData)
: 拖动并放开事件, 要求放开的时候鼠标还在所拖动的物体内部, 如果同时要处理点击事件(IPointerClickHandler
)则无法触发此事件public interface IScrollHandler(PointerEventData)
: 滚动事件public interface IUpdateSelectedHandler(BaseEventData)
: 更新选中事件, 几乎每帧发送public interface ISelectHandler(BaseEventData)
: 切换选中事件, 每次切换选中时向选中的对象发送public interface IDeselectHandler(BaseEventData)
: 取消选中事件, 每次切换选中时向反选中的对象发送public interface IMoveHandler(AxisEventData)
: 导航移动事件public interface ISubmitHandler(BaseEventData)
: 导航提交事件public interface ICancelHandler(BaseEventData)
: 导航取消事件
EventTrigger
在之前的文章中提到过, 如果我们要接收特定事件, 处理继承或者实现事件接口外, 还可以通过EventTrigger来达到目的. 详情请参考事件系统的概述那篇文章.
我们甚至可以结合两种方式一起使用, 比如下面的例子:
using UnityEngine;
using UnityEngine.EventSystems;public class EventTriggerTest : MonoBehaviour, IPointerClickHandler, IBeginDragHandler {private void Start() {var eventTrigger = GetComponent<EventTrigger>();if (!eventTrigger) eventTrigger = gameObject.AddComponent<EventTrigger>();var entry1 = new EventTrigger.Entry();entry1.eventID = EventTriggerType.PointerClick;entry1.callback.AddListener((eventData => {var pointerEventData = eventData as PointerEventData;Debug.LogError("----- PointerClick");}));var entry2 = new EventTrigger.Entry();entry2.eventID = EventTriggerType.BeginDrag;entry2.callback.AddListener((eventData => {var pointerEventData = eventData as PointerEventData;Debug.LogError("----- BeginDrag");}));eventTrigger.triggers.Add(entry1);eventTrigger.triggers.Add(entry2);}public void OnPointerClick(PointerEventData eventData) {Debug.LogError("##### PointerClick");}public void OnBeginDrag(PointerEventData eventData) {Debug.LogError("##### OnBeginDrag");}
}//-----------------------------------
// 输出
// ##### OnBeginDrag
// ----- BeginDrag
// ##### PointerClick
// ----- PointerClick
下面详细介绍EventTrigger的各个部分.
EventTriggerType
在EventTriggerType中对应前面的各个接口, 定义了各种事件类型, 用来在各个模块之间传递和标识事件:
public enum EventTriggerType
{PointerEnter = 0,PointerExit = 1,PointerDown = 2,PointerUp = 3,PointerClick = 4,Drag = 5,Drop = 6,Scroll = 7,UpdateSelected = 8,Select = 9,Deselect = 10,Move = 11,InitializePotentialDrag = 12,BeginDrag = 13,EndDrag = 14,Submit = 15,Cancel = 16
}
EventTrigger.TriggerEvent
使用UnityEvent封装事件数据, 可通过AddListener
添加事件处理回调.
public class TriggerEvent : UnityEvent<BaseEventData> {}
EventTrigger.Entry
组合事件类型和事件处理回调.
public class Entry
{public EventTriggerType eventID = EventTriggerType.PointerClick;public TriggerEvent callback = new TriggerEvent();
}
EventTrigger
EventTrigger本身是一个MonoBehavior, 没有继承于UIBehavior, 所以也可以用于非UI模块的消息处理.
EventTrigger本身很简单, 只是通过实现上面介绍的各种接口, 然后在触发各种事件时, 使用EventTrigger.Entry封装的类型确定回调, 进行事件处理.
public class EventTrigger :MonoBehaviour,IPointerEnterHandler,IPointerExitHandler,IPointerDownHandler,IPointerUpHandler,IPointerClickHandler,IInitializePotentialDragHandler,IBeginDragHandler,IDragHandler,IEndDragHandler,IDropHandler,IScrollHandler,IUpdateSelectedHandler,ISelectHandler,IDeselectHandler,IMoveHandler,ISubmitHandler,ICancelHandler{}
EventTrigger相比业务类直接实现事件接口的方式来说, 极大的提高了代码的灵活性, 对同一个事件可以添加多个处理回调, 也可以动态删减处理回调等.
EventTrigger只有一个字段, 用于存储封装的入口, 每个入口抽象为一个委托(delegate), 也就是说EventTrigger本身不处理事件, 而只是委托处理: private List<Entry> m_Delegates;
这个委托容器在获取时懒加载创建.
public List<Entry> triggers
{get{if (m_Delegates == null)m_Delegates = new List<Entry>();return m_Delegates;}set { m_Delegates = value; }
}private void Execute(EventTriggerType id, BaseEventData eventData)
{for (int i = 0, imax = triggers.Count; i < imax; ++i){var ent = triggers[i];if (ent.eventID == id && ent.callback != null)ent.callback.Invoke(eventData);}
}public virtual void OnPointerEnter(PointerEventData eventData)
{Execute(EventTriggerType.PointerEnter, eventData);
}public virtual void OnPointerExit(PointerEventData eventData)
{Execute(EventTriggerType.PointerExit, eventData);
}//.............
代码都大同小异, 这里只是摘取了部分代表性的代码.
首先OnPointerEnter
触发后, 调用Execute
, 遍历所有的代理, 发现符合事件类型的代理则进行事件回调处理.
当然, 我们也可以继承EventTrigger来对某些事件类型定制化我们自己的逻辑, 这也是Unity给我们的建议.
ExecuteEvents
事件系统通过ExecuteEvents来进行事件分发, 也就是说ExecuteEvents是消息系统. 虽然它的代码量和复杂度看上去有些"配不上"所谓的"系统"两个字.
ExecuteEvents本身是一个静态类, 划分为以下几个主要的部分.
提供委托封装事件处理函数
对上面提到的所有事件类型, 提供委托属性封装事件触发操作, 下面的代码也是摘取部分, 其它代码大同小异.
public static class ExecuteEvents
{// 通过泛型声明委托函数, 封装处理器, 事件数据public delegate void EventFunction<T1>(T1 handler, BaseEventData eventData);// 转换事件数据类型public static T ValidateEventData<T>(BaseEventData data) where T : class{if ((data as T) == null)throw new ArgumentException(String.Format("Invalid type: {0} passed to event expecting {1}", data.GetType(), typeof(T)));return data as T;}// 事件处理函数, 包含事件处理器, 事件处理回调, 事件数据private static void Execute(IPointerEnterHandler handler, BaseEventData eventData){handler.OnPointerEnter(ValidateEventData<PointerEventData>(eventData));}// 将事件处理函数转换为委托字段private static readonly EventFunction<IPointerEnterHandler> s_PointerEnterHandler = Execute;// 委托属性public static EventFunction<IPointerEnterHandler> pointerEnterHandler{get { return s_PointerEnterHandler; }}
}
从对象身上获取事件处理器
第二个大的部分就是消息系统的核心, 从对象身上获取事件处理器.
大体思路就是收集对象身上所有有效的组件, 然后将这些组件作为事件处理器来处理特定事件.
所谓有效组件就是实现了IEventSystemHandler接口, 并且能够处理指定事件(实现指定事件接口, 如IPointerClickHandler).
同时还是可启用或者禁用的组件(Behaviour), Unity使用这个属性(isActiveAndEnabled)来标识该组件是否处理事件.
所以在默认情况下, 只能通过禁用组件来忽略事件处理. 当然我们可以在消息系统分发之后, 通过二次处理来决定在业务层是否真正处理事件. Unity的实现不管这些, 只保证最基础的功能, 其它交给我们自己.
// 判断组件是否有效
// 组件必须事件特定事件接口
// 组件必须处于可用状态
private static bool ShouldSendToComponent<T>(Component component) where T : IEventSystemHandler
{var valid = component is T;if (!valid)return false;var behaviour = component as Behaviour;if (behaviour != null)return behaviour.isActiveAndEnabled;return true;
}// 获取对象身上所有满足条件的事件处理器
private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler
{// Debug.LogWarning("GetEventList<" + typeof(T).Name + ">");if (results == null)throw new ArgumentException("Results array is null", "results");if (go == null || !go.activeInHierarchy)return;var components = ListPool<Component>.Get();go.GetComponents(components);for (var i = 0; i < components.Count; i++){if (!ShouldSendToComponent<T>(components[i]))continue;// Debug.Log(string.Format("{2} found! On {0}.{1}", go, s_GetComponentsScratch[i].GetType(), typeof(T)));results.Add(components[i] as IEventSystemHandler);}ListPool<Component>.Release(components);// Debug.LogWarning("end GetEventList<" + typeof(T).Name + ">");
}
从对象节点层级上获取第一个满足条件的事件处理器(handler)
// 处理器的对象池
private static readonly ObjectPool<List<IEventSystemHandler>> s_HandlerListPool = new ObjectPool<List<IEventSystemHandler>>(null, l => l.Clear());// 判断指定对象是否可以可以处理特定事件
// 通过对象身上是否存在满足条件的事件处理器
public static bool CanHandleEvent<T>(GameObject go) where T : IEventSystemHandler
{var internalHandlers = s_HandlerListPool.Get();GetEventList<T>(go, internalHandlers);var handlerCount = internalHandlers.Count;s_HandlerListPool.Release(internalHandlers);return handlerCount != 0;
}// 从指定节点顺着父节点一直往上找, 一直找到一个可以处理特定事件的对象
public static GameObject GetEventHandler<T>(GameObject root) where T : IEventSystemHandler
{if (root == null)return null;Transform t = root.transform;while (t != null){if (CanHandleEvent<T>(t.gameObject))return t.gameObject;t = t.parent;}return null;
}
因为几乎每帧都要获取handler, Unity使用对象池技术来降低性能损耗. 这个对象池在之前的文章中已经讲过了, 可以参考这里.
事件分发
// 向对象身上所有满足要求的处理指定事件的处理器发送事件
// 使用对象池获取handler
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{var internalHandlers = s_HandlerListPool.Get();GetEventList<T>(target, internalHandlers);// if (s_InternalHandlers.Count > 0)// Debug.Log("Executinng " + typeof (T) + " on " + target);for (var i = 0; i < internalHandlers.Count; i++){T arg;try{arg = (T)internalHandlers[i];}catch (Exception e){var temp = internalHandlers[i];Debug.LogException(new Exception(string.Format("Type {0} expected {1} received.", typeof(T).Name, temp.GetType().Name), e));continue;}try{functor(arg, eventData);}catch (Exception e){Debug.LogException(e);}}var handlerCount = internalHandlers.Count;s_HandlerListPool.Release(internalHandlers);return handlerCount > 0;
}private static readonly List<Transform> s_InternalTransformList = new List<Transform>(30);// 收集对象和其所有父节点
private static void GetEventChain(GameObject root, IList<Transform> eventChain)
{eventChain.Clear();if (root == null)return;var t = root.transform;while (t != null){eventChain.Add(t);t = t.parent;}
}// 向对象身上和所有父节点身上的所有满足要求的处理指定事件的处理器发送事件[遇到第一个满足要求的对象后中断]
public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler
{GetEventChain(root, s_InternalTransformList);for (var i = 0; i < s_InternalTransformList.Count; i++){var transform = s_InternalTransformList[i];if (Execute(transform.gameObject, eventData, callbackFunction))return transform.gameObject;}return null;
}
使用
ExecuteEvents.Execute(gameObject, pointerData, ExecuteEvents.pointerEnterHandler);
ExecuteEvents.ExecuteHierarchy(gameObject, pointerEvent, ExecuteEvents.dropHandler);
有些事件只需要对象本身处理, 而另外一些事件需要对象的整个世系层级处理. 我们将在后面的输入模块分别介绍.
总结
今天介绍了事件系统中很重要的消息定义和消息系统部分, 我们对整个事件系统的了解又进入了新的层次.
同时, 我这里不得不感慨一下, 看了这么多Unity的源码之后, 渐渐对Unity的设计哲学有了更深入的认识, 特别是C#层, 大部分设计都比较简洁, 这点值得我们学习.
感觉Unity比较崇尚简洁和最大程度的开放(当然, 不开源是人家要恰饭, 可以理解), 他们基本上不会做保姆性质的功能, 尽可能简化框架代码, 把可定制化的部分交给客户.
现在很多框架和业务耦合比较严重, 导致复用性不高, 难以推广. 在框架设计上还是需要多多下功夫才行. 当然我自己的框架也差不多有同样的问题, 所以需要不断学习借鉴进步啊. 希望大家一起共勉.
好了, 今天就是这样, 希望对大家有所帮助.
更多推荐
Unity中的UGUI源码解析之事件系统(4)
发布评论