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

开发平台: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");
	}
}

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

使用 事件分发器 传递信息

  事件分发器,其某方面上与 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"); }
}

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

public class Example_D : MonoBehaviour
{
    
    
	public void Start() => EventDispatcher.SendMessage("PlayerDoit");
}

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

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

思考:如何设计 EventDispatcher ?


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

在这里插入图片描述
  由 事件分发器 管理组件对象,根据消息类型,传递参数与响应。在第一设计中,构想使用 Dictionary<string, LIst<Component>> (消息类型,脚本对象)数据类型用于注册被添加至事件中的对象。即被添加至对应 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) {
    
    }
}

  而作为响应事件的对象,提供每类型信号下与之匹配的方法。在完成注册后,通过 确认是否在注册的消息机制中 - 使用 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. 信号数量冗杂导致的,难维护性。
    过多的信号将占据大片的程序内容,同时 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() {
    
    }
}
  • 委托的特性:降低耦合度,一次调用,其内注册的委托均调用。为避免出现 NULL 情况,多数下选择 EventHandler?.Invoke
  • 事件的特性:(特殊的委托)由事件本身作为条件对象,外部依据该条件注册事件,并在条件满足时,执行各自承担的事件内容。

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

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

  同理情况下,注销委托 即 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。
    简易理解:避免或禁止监听响应的方法内出现事件的分发。
    理由:使用频繁后,易造成逻辑混乱、可维护性低。
  • 事件分发器 对事件的命名 应建立良好的命名规范。
    理由:意义不明的命名规范将造成开发者理解障碍问题,导致耗时维护成本提高。(无论是事件命名 亦或是 响应委托的方法命名 均需重视)

猜你喜欢

转载自blog.csdn.net/qq_51026638/article/details/125829500