Unity Messaging System

Messaging System 是 Unity 全新设计的一套通信系统。旨在解决一些使用 SendMessage 可能会出现的问题。最重要的是 Messaging System 设计之初就是为游戏开发过程提供一套完整的通信解决方案,所以它不仅仅局限于在 UI 系统中使用,在游戏代码中的任何地方我们都可以使用它。

接下来就来看看为什么要使用这套 Messaging System。

传统方式进行 Message 通信

传统消息通信,通常我们会使用 GameObject 类中的 SendMessageSendMessageUpwardsBroadcastMessage 方法,每个方法用途不一样:

  • SendMessage(string methodName, object value, SendMessageOptions options);

调用当前 GameObject 中所有 MonoBehaviour 脚本中的 methodName 方法。

  • SendMessageUpwards(string methodName, object value);

调用当前以及所有祖先 GameObject 对象中的所有 MonoBehaviour 脚本中的 methodName 方法。

  • BroadcastMessage(string methodName, object parameter);

调用当前以及所有子孙 GameObject 对象中的所有 MonoBehaviour 脚本中的 methodName 方法。

上面是传统方式进行通信常用的三个方法,总结一下:

SendMessage SendMessageUpwards BroadcastMessage
自身节点
兄弟节点 × × ×
父/祖先节点 × ×
子/孙节点 × ×

接下来看看传统方式进行 Message 通信可能遇到的一些问题。

首先,在使用以上三种方式通信时,都将需要回调的方法名以字符串的形式传入作为第一个参数。以 SendMessage 方法为例,首先会寻找当前 SendMessage 方法所在的 GameObject 中所有的 Component;然后在使用第一个参数 methodName,遍历比较每个 Component 中所有的方法,如果方法名相同,则执行该 Component 中的 methodName 方法。SendMessageUpwardsBroadcastMessage 也大同小异,不同之处在于这两个方法还要分别首先找到多个接收执行该方法的 GameObject(祖先对象或子孙对象),然后在对每个对象执行类似 SendMessage 的步骤。

看到这里,你可能会想这样如果需要接收消息的 GameObject 很多,并且方法也很多,那么可能会有性能问题。为什么都得到了 GameObject 的所有的 Component,缓存这些 Component 以后直接执行消息对应的方法不就可以了吗?确实可以,但是这样的代码好像就不太优雅了,破坏了 Unity 原有的 Component/Object 模式。

另一个可怕问题,如果消息发送者没有管理好消息的出口,那么某些可能你不希望的接收者却被激活触发了未知的逻辑。

最后还有一点,传统方式进行 Message 通信虽然代码结构上看很简洁,但是对刚接触项目代码新人来说,要想找到一个消息发送来源以及所有可能得接收者却稍显麻烦。

综合以上几点可能会出现的问题,下面就一起来看看能解决这些问题的 Messaging System。

Messaging System

The new UI system uses a messaging system designed to replace SendMessage. The system is pure C# and aims to address some of the issues present with SendMessage.

上面是 Unity 官方文档对 Messaging System 的介绍。在 UGUI 的 Event System 中,所有的事件通信都是用了 Messaging System 来实现,它也解决了前面我们所说的传统方式进行 Message 通信中可能会遇到的一些问题。下面就来让我们好好看看这套 Messaging System。

使用 ExecuteEvents 通信

首先,要想让 Component 能够从 Messaging System 接收消息,Component 要实现 IEventSystemHandler 这个接口。

接着就可以使用以下几个方法发送事件。

  • Execute(GameObject target, EventSystems.BaseEventData eventData, EventFunction<T> functor);

执行 GameObject 上挂载的所有实现了 IEventSystemHandler 接口的脚本中的特定事件。

  • ExecuteHierarchy(GameObject root, EventSystems.BaseEventData eventData, EventFunction<T> callbackFunction);

以 GameObject 为根节点开始向父节点递归的执行 Execute 方法,直到找到一个可以处理 IEventSystemHandler 事件的 GameObject 为止。

Messaging System 内部以 C# 代理为基础,实现了整个通信系统。下面就来分析以下整个 ExecuteEvents 类的源码部分。

  • 在 ExecuteEvents 类内部定义了一个代理 EventFunction<T1>(T1 handler, BaseEventData eventData),这个代理就是用来执行自定义消息响应操作。它有两个参数: 第一个是泛型的一个 handler,第二个是 BaseEventData 类型的事件数据。

  • 然后来到最关键 Execute 方法。方法定义如下:

1
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler;

该方法需要三个参数: 第一个参数 target 是当前接收事件的 GameObject 对象;第二个参数 eventData 是携带的 BaseEventData 类型事件数据;最后一个参数 functor 是 EventFunction 类型的一个代理(上面讲过,这个代理通常是我们的用来回调事件的方法)。

Execute 方法首先使用 GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) 获取对象 target 上实现了自定义 IEventSystemHandler 接口并处于 Active 和 Enable 的 Component;然后循环将每个 Component 和 eventData 作为参数执行一遍代理方法 functor(代理的方法中具体代码可以自己实现,一般情况下是回调 Component 中的相关事件方法);最后返回结果是一个 bool 值表示是否有合适处理消息事件的 Component 被找寻到,有则返回 true

下面是处理 UI 事件按下的一个方法,其内部实现就是回调了 IPointerDownHandlerOnPointerDown 方法。这种处理 UI 事件的方法在 ExecuteEvents 类内部定义了很多,后面会有介绍。

1
2
3
4
private static void Execute(IPointerDownHandler handler, BaseEventData eventData)
{
handler.OnPointerDown(ValidateEventData<PointerEventData>(eventData));
}
  • 接下来看看 ExecuteHierarchy 方法。定义如下:
1
public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler;

该方法同 Execute 方法类似,同样需要三个参数。前面提到过 ExecuteHierarchy 方法是以某个节点为起点向祖先节点递归,直到找寻到某个对象上的 Component 能够处理 IEventSystemHandler 类型的消息事件就停止,并返回这个 GameObject。所以,这个方法中的第一个参数就是起始节点 GameObject。

ExecuteHierarchy 方法的代码也很简单,首先调用 GetEventChain(GameObject root, IList<Transform> eventChain) 方法得到 root 节点(包括本身)所有的祖先节点,然后循环对每个节点 GameObject 调用 Execute 方法,如果节点 GameObject 上有合适处理消息事件的 Component,则返回这个节点 GameObject 并结束循环。

  • GetEventHandler 方法。定义如下:
1
public static GameObject GetEventHandler<T>(GameObject root) where T : IEventSystemHandler;

GetEventHandler 方法作用是从指定的节点 GameObject 开始向上寻找,直至找到一个能够接收指定类型事件的节点 GameObject 就返回这个 GameObject,否则返回 null。

GetEventHandler 内部实现也很简单。遍历当前节点以及祖先节点,获取每个节点 GameObject 上的所有 Components,然后在判断每个 Component 是否能接收指定类型事件并且是否处于激活状态,如果找到的符合条件的 Component 数量大于 0,则说明当前节点 GameObject 能够接收指定类型事件,返回当前 GameObject;否则继续向上寻找。

内置 UI 事件

ExecuteEvents 类内部定义了很多通用的 EventFunction<T1>(T1 handler, BaseEventData eventData) 类型的代理方法,用来执行 Event System 指定的事件。比如:

1
2
3
4
5
6
private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{
handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}

这些代理方法在 Event System 内部会用到,我们也可以实现类似 IPointerClickHandler 这些接口去监听某些事件。比如 Image 组件本身不能监听鼠标点击事件,现在我们需要为 Image 组件添加点击点击响应,实现就很简单了: 给 Image 组件添加一个实现了 IPointerClickHandler 接口的脚本,当这个 Image 上有点击事件时,Event System 会将找到这个 Image 然后执行 Execute 方法(传入的第一个参数就是 Image 所在的对象,第二个参数是 PointerEventData,最后的参数就是 ExecuteEvents 类成员 s_PointerClickHandler),接下来的步骤就回到了之前分析的 Execute 执行过程,最终回调了我们自己实现的 OnPointerClick 方法。

Unity 中提供的常用的 UI 事件能让开发变得更方便,更多具体事件接口说明参考文档

参考