返回

在 Unity3D 中基于订阅者模式实现事件机制

各位朋友,大家好,欢迎大家关注我的博客,我是秦元培,我的博客地址是:http://qinyuanpei.com。今天博主想和大家分享的是在 Unity3D 中基于订阅者模式实现消息传递机制,我们知道 Unity3D 中默认提供了一种消息传递机制 SendMessage,虽然 SendMessage 使用起来的确非常简单,可是它的这种简单是建立在付出一定的代价的基础上的。经常有朋友提及不同的模块间如何进行通信的问题,可能答案最终会落到单例模式委托事件机制这些关键词上,在这种情况下本文所探讨的内容可能会帮助你找到最终的答案。

从生活中得到的启示

我们知道通过在 Unity3D 中通过 GetComponent 就可以获得某个模块的实例,进而引用这个实例完成相关任务的调用。可是显然这种方法,就像我们随身带着现金去和不同的人进行交易,每次交易的时候都需要我们考虑现金的支入和支出问题,从安全性和耦合度两个方面进行考虑,这种方法在面对复杂的系统设计的时候,非常容易造成模块间的相互依赖,即会增加不同模块间的耦合度。为了解决这个问题,大家开始考虑单例模式,因为单例模式能够保证在全局内有一个唯一的实例,所以这种方式可以有效地降低模块间的直接引用。单例模式就像是我们在银行内办理了一个唯一的账户,这样我们在交易的时候只需要通过这个账户来进行控制资金的流向就可以了。单例模式确保了各个模块间的独立性,可是单例模式更多的是一种主动行为,即我们在需要的时候主动去调用这个模块,单例模式存在的问题是无法解决被调用方的反馈问题,除非被调用方主动地去调用调用方的模块实例。说到这里我们好像看到了一种新的模式,这就是我们下面要提到的事件机制。

订阅者模式和事件机制

首先这里要提到一种称为“订阅者模式”的设计模式,这种设计模式在《大话设计模式》这本书中称为“观察者模式”或者“发布-订阅(Publish/Subscribe)模式”,我们这里暂且叫做“订阅者模式”吧!该模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个对象在状态发生变化时会通知所有观察者对象,使它们能够自动更新自己。针对这个模式,我们可以考虑事件机制的实现,事件机制可以理解为在一个事件中心(Subject)保存有对所有事件(Observer)的引用,事件中心负责对这些事件进行分发,这样每个事件就可以通过回调函数的方式进行更新,这样就实现了一个事件机制。下面给出基本的代码实现:

  1using System;
  2using System.Collections;
  3using System.Collections.Generic;
  4using UnityEngine;
  5
  6namespace UniEventDispatcher
  7{
  8    /// <summary>
  9    /// 定义事件分发委托
 10    /// </summary>
 11    public delegate void OnNotification(Notification notific);
 12
 13    /// <summary>
 14    ///通知中心 
 15    /// </summary>
 16    public class NotificationCenter
 17    {
 18
 19        /// <summary>
 20        /// 通知中心单例
 21        /// </summary>
 22        private static NotificationCenter instance=null;
 23        public static NotificationCenter Get()
 24        {
 25            if(instance == null){
 26                instance = new NotificationCenter();
 27                return instance;
 28            }
 29            return instance;
 30        }
 31
 32
 33        /// <summary>
 34        /// 存储事件的字典
 35        /// </summary>
 36        private Dictionary<string,OnNotification> eventListeners 
 37            = new Dictionary<string, OnNotification>();
 38
 39        /// <summary>
 40        /// 注册事件
 41        /// </summary>
 42        /// <param name="eventKey">事件Key</param>
 43        /// <param name="eventListener">事件监听器</param>
 44        public void AddEventListener(string eventKey,OnNotification eventListener)
 45        {
 46            if(!eventListeners.ContainsKey(eventKey)){
 47                eventListeners.Add(eventKey,eventListener);
 48            }
 49        }
 50
 51        /// <summary>
 52        /// 移除事件
 53        /// </summary>
 54        /// <param name="eventKey">事件Key</param>
 55        public void RemoveEventListener(string eventKey)
 56        {
 57            if(!eventListeners.ContainsKey(eventKey))
 58                return;
 59
 60            eventListeners[eventKey] =null;
 61            eventListeners.Remove(eventKey);
 62        }
 63
 64        /// <summary>
 65        /// 分发事件
 66        /// </summary>
 67        /// <param name="eventKey">事件Key</param>
 68        /// <param name="notific">通知</param>
 69        public void DispatchEvent(string eventKey,Notification notific)
 70        {
 71            if (!eventListeners.ContainsKey(eventKey))
 72                return;
 73            eventListeners[eventKey](notific);
 74        }
 75
 76        /// <summary>
 77        /// 分发事件
 78        /// </summary>
 79        /// <param name="eventKey">事件Key</param>
 80        /// <param name="sender">发送者</param>
 81        /// <param name="param">通知内容</param>
 82        public void DispatchEvent(string eventKey, GameObject sender, object param)
 83        {
 84            if(!eventListeners.ContainsKey(eventKey))
 85                return;
 86            eventListeners[eventKey](new Notification(sender,param));
 87        }
 88
 89        /// <summary>
 90        /// 分发事件
 91        /// </summary>
 92        /// <param name="eventKey">事件Key</param>
 93        /// <param name="param">通知内容</param>
 94        public void DispatchEvent(string eventKey,object param)
 95        {
 96            if(!eventListeners.ContainsKey(eventKey))
 97                return;
 98            eventListeners[eventKey](new Notification(param));
 99        }
100
101        /// <summary>
102        /// 是否存在指定事件的监听器
103        /// </summary>
104        public Boolean HasEventListener(string eventKey)
105        {
106            return eventListeners.ContainsKey(eventKey);
107        }
108    }
109}

注意到在这个“通知中心”中,我们首先实现了单例模式,这样我们可以通过 Get 方法来获取该“通知中心”的唯一实例,其次这里利用一个字典来存储对所有事件的引用,这样保证外部可以通过 AddEventListener 和 RemoveEventListener 这两个方法来进行事件的添加和移除,对于添加的事件引用我们可以通过 DispatchEvent 方法来分发一个事件,事件的回调函数采用委托来实现,注意到这个委托需要一个 Notification 类型,对该类型简单定义如下:

 1using System;
 2using UnityEngine;
 3
 4namespace UniEventDispatcher
 5{
 6    public class Notification
 7    {
 8        /// <summary>
 9        /// 通知发送者
10        /// </summary>
11        public GameObject sender;
12
13        /// <summary>
14        /// 通知内容
15        /// 备注:在发送消息时需要装箱、解析消息时需要拆箱
16        /// 所以这是一个糟糕的设计,需要注意。
17        /// </summary>
18        public object param;
19
20        /// <summary>
21        /// 构造函数
22        /// </summary>
23        /// <param name="sender">通知发送者</param>
24        /// <param name="param">通知内容</param>
25        public Notification(GameObject sender, object param)
26        {
27            this.sender = sender;
28            this.param = param;
29        }
30
31        /// <summary>
32        /// 构造函数
33        /// </summary>
34        /// <param name="param"></param>
35        public Notification(object param)
36        {
37            this.sender = null;
38            this.param = param;
39        }
40
41        /// <summary>
42        /// 实现ToString方法
43        /// </summary>
44        /// <returns></returns>
45        public override string ToString()
46        {
47            return string.Format("sender={0},param={1}", this.sender, this.param);
48        }
49    }
50}

对 Notification 的定义需要提供发送者和发送内容,这样可以保证所有的通知都按照这样的格式进行定义,如果有 Socket 开发经验的朋友可能会联想到通讯协议的定义,这里是比较相似啦,哈哈!

使用事件机制的一个示例

这里以一个简单的示例来验证事件机制的可行性,我们在场景中有一个球体,默认这个球体的颜色为白色,通过调整界面中的 RGB 数值,可以改变球体的颜色,在这个示例中 UI 是事件发送者,负责 UI 中 Slider 控件的数值发生变化时向球体发送消息,传递的数据类型是 Color 类型;球体为事件接收者,负责注册事件及接收到消息后的处理。因为代码较为简单,所以这里写在一个脚本中:

 1using UnityEngine;
 2using UnityEngine.UI;
 3using System.Collections;
 4using UniEventDispatcher;
 5
 6public class Example : MonoBehaviour 
 7{
 8    /// <summary>
 9    /// R数值的Slider
10    /// </summary>
11    private Slider sliderR;
12
13    /// <summary>
14    /// G数值的Slider
15    /// </summary>
16    private Slider sliderG;
17
18    /// <summary>
19    /// B数值的Slider
20    /// </summary>
21    private Slider sliderB;
22
23    void Start () 
24    {
25        //在接收者中注册事件及其回调方法
26        NotificationCenter.Get().AddEventListener("ChangeColor", ChangeColor);
27
28        //在发送者中分发事件,这里以UI逻辑为例
29        sliderR = GameObject.Find("Canvas/SliderR").GetComponent<Slider>();
30        sliderG = GameObject.Find("Canvas/SliderG").GetComponent<Slider>();
31        sliderB = GameObject.Find("Canvas/SliderB").GetComponent<Slider>();
32        //注册UI事件
33        sliderR.onValueChanged.AddListener(OnValueChanged);
34        sliderG.onValueChanged.AddListener(OnValueChanged);
35        sliderB.onValueChanged.AddListener(OnValueChanged);
36    }
37
38    public void OnValueChanged(float value)
39    {
40        //获得RGB数值
41        float r = sliderR.value;
42        float g = sliderG.value;
43        float b = sliderB.value;
44        //分发事件,注意和接收者协议一致
45        NotificationCenter.Get().DispatchEvent("ChangeColor", new Color(r, g, b));
46    }
47
48    /// <summary>
49    /// 改变物体材质颜色
50    /// </summary>
51    /// <param name="notific"></param>
52    public void ChangeColor(Notification notific)
53    {
54        Debug.Log(notific.ToString());
55        //设置颜色
56        renderer.material.color = (Color)notific.param;
57    }
58}

该示例运行效果如下:

事件机制的简单示例
事件机制的简单示例

小结

虽然目前这个事件机制在实现和使用上没有什么问题,可是从扩展性和可优化性上来考虑,这个设计目前存在以下问题:

  • 字符型的键名使用起来方便,可是对通知者和接收者由 1 个以上的人力来维护的时候双方需要通过沟通来确定键名,可以考虑使用 GameObject 或者 Transform 来替代现在的键名设计,可是这种设计带来的新问题是会增加不同模块间的 GameObject 或者 Transform 的相互引用。
  • 通知者和接收者在传递参数和接受参数的时候需要分别进行装箱和拆箱,所以这并非一个优秀的设计,同时需要双方保证传递的参数类型一致。解决方法是针对不同的类型对通知中心进行派生或者考虑对通知中心提供泛型约束,这样做的目的是使 Notification 中的通知内容变成具体的类型,这样就可以解决目前需要进行装箱和拆箱而带来的性能问题。
Built with Hugo
Theme Stack designed by Jimmy