开发平台: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) {}
}
虽然一定程度上,确实有助于实现消息的管理与响应机制,但仍存在许多方面明显表现不足的地方。代码的冗余与操作上的复杂尤为突出:
- 信号注册、注销繁琐且事故率高。
消息信号的添加与删除,均需要在 事件分发中心 与 响应对象 中完成。 - 信号命名复杂性。
在出现多类型的信号上,因为已有数量的繁多导致命名性困难,或忽略重名等可能性。 - 信号数量冗杂导致的,难维护性。
过多的信号将占据大片的程序内容,同时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() })
。委托需要自己的执行方式。大致为以下顺序:
- 实例化委托对象。(委托 类似于抽象的类)
var thisDelegate = new EventHandler(委托)
- 回调委托。
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。
简易理解:避免或禁止监听响应的方法内出现事件的分发。
理由:使用频繁后,易造成逻辑混乱、可维护性低。 - 事件分发器 对事件的命名 应建立良好的命名规范。
理由:意义不明的命名规范将造成开发者理解障碍问题,导致耗时维护成本提高。(无论是事件命名 亦或是 响应委托的方法命名 均需重视)