• 【案例设计】事件分发器 — 实现跨类的事件响应思路分享与实现


    开发平台:Unity 2020
    编程平台:Visual Studio 2020

    前言


      类 与 类 之间的通讯是程序开发中经常遭遇的事。其目的是传递属性、字段等内容,以提供给另一类中的方法以执行。于是为了强化这过程,降低耦合度。出现了 事件分发器(EventDispatcher)的设计。

    区别:在 Unity 使用变量直接赋予 与 EventDispatcher 的不同点


    Unity 内置拖拽 或 脚本内赋值

      通常情况下,初学者会选择 public Component m_Component; 方式进行跨类的调用。假设,名为 Example_A.cs 的脚本内中有一个 名为 PrintDebug(string message) 的方法。则在名为 Example_B.cs 的脚本中应当按照以下进行引用与调用:

    public class Example_B : Monobehaviour
    {
    	public Example_A ExampleA;
    
    	public void Awake() { // 要么拖拽、要么transform/GameObject 查找赋值 }
    	
    	public void Start()
    	{
    		ExampleA.PrintDebug("This is a text");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    优势:上手简单,可快速构建关系。
    劣势:若涉及到一个类与多个类间的关系,这种方式是不可取,且麻烦至极。在后续的维护与更改上将增加难度与时长。

    使用 事件分发器 传递信息

      事件分发器,其某方面上与 MVC 设计模式有着异曲同工之处。在 MVC 框架设计中,关联 视图与控制器 的管理思想。即 注册、注销。将同类型对象,从场景内激活的那一刻起,添加至控制器中。交由控制器 发送信号,由对象自己接收信号判断是否拥有此类型信号,有则响应,无则静默。若该对象被禁用,则移除至队列外,不再受控制器管理。

    public class Example_C : Monobehaivour
    {
    	public void OnEnable() => EventDispatcher.AddObserver("PlayerDoit", OnPlayerDo);
    	public void OnDisable() => EventDispactcher.RemoveObserver("PlayerDoit", OnPlayerDo);
    	
    	public void OnPlayerDo() { Deug.Log("I have to do something which i really want to do"); }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      备注:与 UGUI EventSystem 有相同点。例如程序上 Button / Toggle / InputField 添加与移除 响应事件的监听方式,具体代码:addListener(callback)removeListener(callback)

    public class Example_D : MonoBehaviour
    {
    	public void Start() => EventDispatcher.SendMessage("PlayerDoit");
    }
    
    • 1
    • 2
    • 3
    • 4

      由 Example_D.cs 发送讯息,通知拥有该类型事件的监听器响应结果。即 Example_C.cs 中在 OnEnable 阶段注册的监听器响应方法 OnPlayerDo

    优势:仅需 SendMessage 即可实现消息跨类的进行。若期望于新增或禁用则需 Add/Remove + Observer。方便管理。
    劣势:需要合理的设计与应用,错误的使用 事件监听器 将导致代码冗余度增加。应避免一个发信内响应的是另一个发信方式等情况发生。

    思考:如何设计 EventDispatcher ?


    第一次设计:基于脚本对象的事件分发与监听

    在这里插入图片描述
      由 事件分发器 管理组件对象,根据消息类型,传递参数与响应。在第一设计中,构想使用 Dictionary> (消息类型,脚本对象)数据类型用于注册被添加至事件中的对象。即被添加至对应 string 中的 Component 对象,被通知执行其对应的方法。执行的方法依赖于各类对象中的信号记录。例如

    public class EventDispatcher
    {
    	public static Dictionary<string, List<Component>> EventResgisters = new Dictionary<string, List<Component>>();
    	
    	// 消息类型参考(无实际意义)	
    	private List<string> MessageRegister = new List<string>()
    	{
    		"Login",
    		"OpenMainPage",
    		"OpenDescription"
    	};
    
    	public static void SendMessage(string message, object[] data)
    	{
    		var targets = EventRegisters(name);
    		foreach(var item in targets)
    		{
    			item.OnReceiveMsg(message, data);
    		}
    	}
    	
    	public static void Register(string messageName, Component comp) {}
    	public static void Logout(string messageName, Component comp) {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

      而作为响应事件的对象,提供每类型信号下与之匹配的方法。在完成注册后,通过 确认是否在注册的消息机制中 - 使用 switch- case 筛选信号类别与响应事件类别,从而完成响应过程。如下图所示:

    public class Example_E : MonoBehaviour
    {
    	private void OnEnable() => EventDispatcher.Register("Login", this);
    	private void OnDisable() => EventDispatcher.Logout("Login", this);
    
        private List<string> MessageRegister = new List<string>()
    	{
    		"Login",
    		"OpenDescription"
    	};
    
    	public void OnReceiveMsg(string messageTarget, object[] data)
    	{	
    		if!MessageRegister.Contains(messageTarget)return;
    
    		switch(messageTarget)
    		{
    			case "Login":
    				DoLogin(data);
    				breake;
    			case "OpenDiscription":
    				DoOpenDiscription(data);
    				breake;
    			default:
    				break;								
    		}
    	}
    
    	private void DoLogin(object[] data) {}
    	private void DoOpenDiscription(object[] data) {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

      虽然一定程度上,确实有助于实现消息的管理与响应机制,但仍存在许多方面明显表现不足的地方。代码的冗余与操作上的复杂尤为突出:

    1. 信号注册、注销繁琐且事故率高。
      消息信号的添加与删除,均需要在 事件分发中心 与 响应对象 中完成。
    2. 信号命名复杂性。
      在出现多类型的信号上,因为已有数量的繁多导致命名性困难,或忽略重名等可能性。
    3. 信号数量冗杂导致的,难维护性。
      过多的信号将占据大片的程序内容,同时 Switch-case 语句的多次叠加下,显得不容易快速比对与快速定位。

      程序的设计目的是便利化实现过程,而非复杂化。于是在这样的要求和时间的洗涤中,接触到了更加完美的 事件分发器 设计机制。学习与巩固了 CSharp 知识。

    第二次设计:基于 CSharp 委托与事件特点的事件分发与监听

      Unity 监听机制 AddListener 是最为体现委托与事件的地方。例如 button.onClick.AddListener(delegate { DoLogin(); }); 这段代码行,其目的性是为 Button 对象添加事件监听选项。当 Button 的点击行为发生时,触发该 DoLogin() 方法。注意!监听器中添加的属于 UnityEvent 的委托事件类型。如下图所示:
    在这里插入图片描述
      于是,委托 + 事件 无疑是最佳的设计方案。通过给对象添加监听器,监听分发的事件是否符合己监听,从响应事件。在整体结构上,只需 OnEnable/OnDiable 周期中注册、注销监听器即可。解决了 第一次设计方案中,代码行多,后期冗杂大的问题。于是有以下程序设计:

    public class Dispatcher
    {
    	public delegate void EventHandler(param object[] _objects);
    
    	public static Dictionary<string, EventHandler> RegistrationEvents = new Dictionary<string, List<EventHandler>>();
    	
    	public static void SendMessage() {}
    	public static void AddObserver() {}
    	public static void RemoveObserver() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 委托的特性:降低耦合度,一次调用,其内注册的委托均调用。为避免出现 NULL 情况,多数下选择 EventHandler?.Invoke
    • 事件的特性:(特殊的委托)由事件本身作为条件对象,外部依据该条件注册事件,并在条件满足时,执行各自承担的事件内容。

    例如:实现 委托 的注册行为。即如下所示:

    public void AddObserver(string name, EventHandler eventHandler) => RegistrationEvents[name] += EventHandler;
    
    • 1

      同理情况下,注销委托 即 RemoveObserver(string name, EventHandler eventHandler) 通过 -= 方式完成。于是,在观察者(监听器)准备就绪后,剩下的关注焦点落到了事件的分发。

    如何分发?
      与 第一次设计 中采取的监听方式不同,委托的分发无需识别信息内容是否与现存文本匹配。因为委托的特性,凡监听名称匹配的 委托对象,其下的所有委托均接收消息内容。即仅需要考虑 委托消息,委托传递的参数 共两个内容。但就目前情况而言,需要思考委托的调用。正如 onClick.AddListener(delegate { OnClickDown() })。委托需要自己的执行方式。大致为以下顺序:

    1. 实例化委托对象。(委托 类似于抽象的类) var thisDelegate = new EventHandler(委托)
    2. 回调委托。thisDelegate.Invoke()
      值得注意的是 委托 存在 Null 的情况。在使用委托时,应注意空引用的判断。使用if语句或?.语法糖可判断。

      于是,事件分发 即SendMessage(string name, param object[] _objects) 得到实现。可以测试一下 类与类 之间的通讯行为。如下图所示,由 Example01.cs 发送名为 SayHello,内容为 来自Example01的问候。Example02.cs 则注册、注销监听器,实现监听响应的事件方法 OnSayHelloEventHandler。经拆箱后解析 Example01 传来的消息,并 Debug 至控制台。

    在这里插入图片描述

    其他:关于事件分发器使用的注意事项

    • 事件分发器的使用 应直接作用于具体对象下的具体方法。
      假设 A 传递 BCD 三者。但因为 B 有额外的内容需要传递给 CD,使得 A 传递给 B 中的委托方法中 嵌套了 B 传递给 CD。
      简易理解:避免或禁止监听响应的方法内出现事件的分发。
      理由:使用频繁后,易造成逻辑混乱、可维护性低。
    • 事件分发器 对事件的命名 应建立良好的命名规范。
      理由:意义不明的命名规范将造成开发者理解障碍问题,导致耗时维护成本提高。(无论是事件命名 亦或是 响应委托的方法命名 均需重视)
  • 相关阅读:
    用MinIO搭建对象存储服务
    使用Pytorch实现深度学习的主要流程
    python生成随机数
    Android 启动关闭GMS包
    程序员工作5年,还是个中级程序员,如何再快速晋升?
    鸿蒙Ability Kit(程序框架服务)【UIAbility组件与UI的数据同步】
    C语言中的文件操作
    Java字符串查找
    MySQL之数据库连接池(Druid)
    B站:TED-ED 世界人文历史英文动画100集【双语字幕】(第1集)
  • 原文地址:https://blog.csdn.net/qq_51026638/article/details/125829500