Unity 游戏框架搭建 2019 (四十六) 简易消息机制 & 集成到 MonoBehaviourSimplify 里
在上一篇,我们接触了单例,使用单例解决了我们脚本之间访问的问题。
脚本之间访问其实有更好的方式。
我们先分下脚本访问脚本的几种形式。
第一种,A GameObject 是 B GameObject 的 Parent,或者是中间隔着几个层级的 Parent。
那这种情况下,如果 A 脚本想调用 B 脚本的方法,直接通过 transform.Find(“XXX/YYY/ZZZ”).GetComponent<B>().DoSomething() 就可以了。
但是如果是 B 脚本想调用 A 脚本的方法,比较好的方式呢,是在 B 脚本中声明委托,然后在 A 中注册特定方法。当 B 想调用 A 脚本的方法的时候,通过委托通知就好。
除了使用委托,也可以使用消息机制,Unity 本身实现了一套消息机制,比如在 B 脚本中可以使用, this.SendMessageUpward(“MethedName”) 这样的方式。不过这种方式由于是使用字符串,并且可能用到了反射,所以网上大部分博客都不太推荐使用,但是也算是个不错的方式。
第二种情况呢是,A GameObject 和 B GameObject 是同级的,比如他们有共同的 Parent。这种情况下,笔者还是推荐用消息机制,不过不是 Unity 自带的消息机制,而是自己实现的消息机制。
第三种情况是,A GameObject 和 B GameObject 不在同一个 GameObject 树下。那么这种情况很可能就是跨模块通信了,这种情况下,还是推荐用消息机制。
所以,我们可以试试使用消息机制来解决我们的问题。
可是我们目前手里没有消息机制…
那就造一个吧。
消息机制要用到的知识:
- List 或 LinkedList 或者自己实现的链表。
- Dictionary
- 委托
关于第一条,我们选择 List 就好了,不过为了有更高的效率,我们最后会升级成链表。第三条,我们选择 Action,因为这是我们接触过的,以后也是用的比较多的。
而一般的消息机制会提供三个 API。
- 注册事件
- 注销事件
- 发送事件
我们先试着设计一下,假如我们想这样使用我们的 API
MsgDispatcher.Register("消息名",(obj)=>{ /* 处理消息 */ });
MsgDispatcher.Send("消息名","消息内容");
MsgDispatcher.UnRegister("消息名");
首先事件名,是一个字符串类型的,而事件名要对应一个委托。我们声明一个静态的字典变量就好了。
private static Dictionary<string, Action<object>> RegisteredMsgs = new Dictionary<string, Action<object>>();
为什么是静态的呢?因为,我们的消息机制呢不需要创建实例,而消息是要在整个项目内之间通信的,也就是全局的消息。全局的消息就需要放在唯一容器里注册。而这个容器就是我们的这个字典变量。
我们先实现注册事件功能。
public static void Register(string msgName, Action<object> onMsgReceived)
{
RegisteredMsgs.Add(msgName, onMsgReceived);
}
非常简单。
我们再实现注销功能。
public static void UnRegister(string msgName)
{
RegisteredMsgs.Remove(msgName);
}
也非常简单。
再实现发送功能。
public static void Send(string msgName, object data)
{
RegisteredMsgs[msgName](data);
}
非常简单。
第十二个示例代码如下:
using System;
using System.Collections.Generic;
using UnityEngine;
namespace QFramework
{
public class MsgDispatcher
{
private static Dictionary<string, Action<object>> RegisteredMsgs = new Dictionary<string, Action<object>>();
public static void Register(string msgName, Action<object> onMsgReceived)
{
RegisteredMsgs.Add(msgName, onMsgReceived);
}
public static void UnRegister(string msgName)
{
RegisteredMsgs.Remove(msgName);
}
public static void Send(string msgName, object data)
{
RegisteredMsgs[msgName](data);
}
#if UNITY_EDITOR
[UnityEditor.MenuItem("QFramework/12.简易消息机制", false, 13)]
#endif
private static void MenuClicked()
{
Register("消息1", data => { Debug.LogFormat("消息1:{0}", data); });
Send("消息1", "hello world");
UnRegister("消息1");
Send("消息1", "hello");
}
}
}
菜单执行结果如下
哈哈哈,报错啦,不过我们发现,第一次消息发送成功了,但是第二次发送的时候报错了。是因为消息进行注销了,也就是字典里没有消息名了,这时候直接从字典里取值当然会报错。
这个问题我们留在下一篇解决,在下一篇,我们要讲解关于这个消息机制的完善。
第十二个示例还没有完成。
集成到 MonoBehaviourSimplify 里。
还记得我们的简易消息机制是为了解决什么问题诞生的嘛?
是为了解决脚本间访问的问题。
我们回过头再看下 A 脚本如果想访问 B 脚本,使用消息机制,如何实现。
代码如下:
public class A : MonoBehaviour
{
void Update()
{
if(Input.GetMouseButtonDown(0))
{
MsgDispatcher.Send("DO","ok");
}
}
}
public class B : MonoBehaviour
{
void Awake()
{
MsgDispatcher.Register("DO",DoSomething);
}
void DoSomething(object data)
{
// do something
}
void OnDestroy()
{
MsgDispatcher.UnRegiter("DO",DoSomething);
}
}
用法还是很简单的。
不过假如我们的 B 脚本注册了非常多的消息,代码会变成如下:
public class B : MonoBehaviour
{
void Awake()
{
MsgDispatcher.Register("DO",DoSomething);
MsgDispatcher.Register("DO1",MsgReceiver);
MsgDispatcher.Register("DO2",MsgReceiver1);
MsgDispatcher.Register("DO3",MsgReceiver2);
}
void DoSomething(object data)
{
// do something
}
...
void OnDestroy()
{
MsgDispatcher.UnRegiter("DO",DoSomething);
MsgDispatcher.UnRegiter("DO1",MsgReceiver);
MsgDispatcher.UnRegiter("DO2",MsgReceiver1);
MsgDispatcher.UnRegiter("DO3",MsgReceiver2);
}
}
每次注册一个消息,对应地,在 OnDestroy 操作的时候就要注销一个事件。这个非常像我们写 C++ 的时候遵循的一个内存管理法则,每次申请内存就要在析构方法里进行释放。
而这样使用消息机制,初学者非常容易忘记消息的注销,从而导致引用异常等等。
那么如何解决呢?
用一个 Dictionary 记录这个脚本中已经注册过的消息,以及消息名对应的回调。
代码如下:
public class B : MonoBehaviour
{
Dictionary<string,Action<object>> mMsgRegisterRecorder = new Dictionary<string,Action<object>>();
void Awake()
{
MsgDispatcher.Register("DO",DoSomething);
mMsgRegisterRecorder.Add("DO",DoSomething);
MsgDispatcher.Register("DO1",MsgReceiver);
mMsgRegisterRecorder.Add("DO1",MsgReceiver);
MsgDispatcher.Register("DO2",MsgReceiver1);
mMsgRegisterRecorder.Add("DO2",MsgReceiver1);
MsgDispatcher.Register("DO3",MsgReceiver2);
mMsgRegisterRecorder.Add("DO3",MsgReceiver2);
}
void DoSomething(object data)
{
// do something
}
...
void OnDestroy()
{
foreach (var keyValuePair in mMsgRegisterRecorder)
{
MsgDispatcher.UnRegister(keyValuePair.Key,keyValuePair.Value);
}
mMsgRegisterRecorder.Clear();
}
}
这样,不管注册了多少个消息,只要在 OnDestroy 的时候, 进行一个遍历,这样消息就全部注销掉了。
但是这样写的话注册,就变得麻烦了,每次注册要先两行代码。
MsgDispatcher.Register("DO3",MsgReceiver2);
mMsgRegisterRecorder.Add("DO3",MsgReceiver2);
把两行提取成一个方法就好了。
提取的方法,代码如下:
private void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
mMsgRegisterRecorder.Add(msgName, onMsgReceived);
}
而注册消息的代码就会变成如下:
private void Awake()
{
RegisterMsg("Do",DoSomething);
RegisterMsg("DO1",MsgReceiver);
RegisterMsg("DO2", _=>{ });
RegisterMsg("DO3", _=>{ });
}
是不是精简了很多,而且也可以注册 Lambda 表达式了。
不过我们看下现在的 B 脚本全部代码:
public class B : MonoBehaviour
{
Dictionary<string, Action<object>> mMsgRegisterRecorder = new Dictionary<string, Action<object>>();
private void Awake()
{
RegisterMsg("Do",DoSomething);
RegisterMsg("DO1",_=>{ });
RegisterMsg("DO2", _=>{ });
RegisterMsg("DO3", _=>{ });
}
private void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
mMsgRegisterRecorder.Add(msgName, onMsgReceived);
}
void DoSomething(object data)
{
// do something
}
private void OnDestroy()
{
foreach (var keyValuePair in mMsgRegisterRecorder)
{
MsgDispatcher.UnRegister(keyValuePair.Key,keyValuePair.Value);
}
mMsgRegisterRecorder.Clear();
}
}
目前,每个要使用相同消息策略的脚本,都实现如上的代码,会产生很多的重复代码。所以这里我们要开始考虑如何让这个消息注册/注销的策略进行复用。首先用静态方法是不可能了,因为这个策略是有状态的(成员变量)。所以以我们目前掌握的知识来看,只能用继承的方式了。
继承也有两种,一种是继承一个新类,另一种是继承到 MonoBehaviourSimplify 里。
笔者选择后者,这样我们的脚本只要继承 MonoBehaviourSimplify 就会获得 API 简化和消息功能了,一举多得,而且很方便。
集成后的代码,也就是第十三个示例的代码如下:
using System;
using System.Collections.Generic;
namespace QFramework
{
public abstract partial class MonoBehaviourSimplify
{
Dictionary<string, Action<object>> mMsgRegisterRecorder = new Dictionary<string, Action<object>>();
protected void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
mMsgRegisterRecorder.Add(msgName, onMsgReceived);
}
private void OnDestroy()
{
OnBeforeDestroy();
foreach (var keyValuePair in mMsgRegisterRecorder)
{
MsgDispatcher.UnRegister(keyValuePair.Key,keyValuePair.Value);
}
mMsgRegisterRecorder.Clear();
}
protected abstract void OnBeforeDestroy();
}
public class B : MonoBehaviourSimplify
{
private void Awake()
{
RegisterMsg("Do", DoSomething);
RegisterMsg("DO1", _ => { });
RegisterMsg("DO2", _ => { });
RegisterMsg("DO3", _ => { });
}
void DoSomething(object data)
{
// do something
}
protected override void OnBeforeDestroy()
{
}
}
}
在以上代码里,笔者把 MonoBehaviourSimplify 添加了 abstract 关键字,这样用户在使用 MonoBehaviourSimplify 的时候就不能自己创建出来实例了。
而又添加了如下抽象方法:
protected abstract void OnBeforeDestroy();
做这步的目的呢,是为了提醒子类不要覆写了 OnDestroy。提醒是怎么做到的呢。
我们通过分析可以得出,使用 MonoBehaviourSimplify 的情况有两种。
一种是,在写脚本之前就想好了这个脚本要继承 MonoBehaviourSimplify,但是继承之后,编译会报错,因为有一个抽象方法,必须实现,也就是 OnBeforeDestroy。那么实现了这个,用户就会知道设计 MonoBehaviourSimplify 的人,是推荐用 OnBeforeDestroy 来做卸载逻辑的,并不推荐用 OnDestroy。这是第一种。
第二种呢,脚本本来就有了,但是在中途想要换成继承 MonoBehaviourSimplify,继承了之后,同样报错了,报错了之后发现 MonoBehaviourSimplify 推荐用 OnBeforeDestroy 来做卸载逻辑,这时候如果以前的脚本已经有了 OnDestroy 逻辑,用户就会把 OnDestroy 的逻辑迁移到 OnBeforeDestroy 里。这样也算达到了一个提醒的作用。
这就是 OnBeforeDestroy 的设计初衷,而 abstract 关键字,就应该这样用。
但是到这里呢,这套策略还是有一点小问题的。这个小问题就留在下一篇讲了。
今天的内容就这些,我们下一篇再见。
转载请注明地址:凉鞋的笔记:liangxiegame.com
更多内容
QFramework 地址:https://github.com/liangxiegame/QFramework
QQ 交流群:623597263
Unity 进阶小班:
- 主要训练内容:
- 框架搭建训练(第一年)
- 跟着案例学 Shader(第一年)
- 副业的孵化(第二年、第三年)
- 权益、授课形式等具体详情请查看《小班产品手册》:https://liangxiegame.com/master/intro
- 主要训练内容:
关注公众号:liangxiegame 获取第一时间更新通知及更多的免费内容。
Unity 游戏框架搭建 2019 (四十六) 简易消息机制 & 集成到 MonoBehaviourSimplify 里的更多相关文章
- Unity 游戏框架搭建 2019 (四十八/四十九) MonoBehaviourSimplify 中的消息策略完善&关于发送事件的简单封装
MonoBehaviourSimplify 中的消息策略完善 在上一篇,笔者说,MonoBehaviourSimplify 中的消息策略还有一些小问题.我们在这篇试着解决一下. 先贴出来代码: usi ...
- Unity 游戏框架搭建 2019 (二十六) 第一轮整理完结
昨天呢我们把第八个示例整理完了.整理之后学习了类的第一作用:方法的集合,还有 Obselete 这个 API.并且在进行整理的时候贯彻了我们新的约定和规则:先确保功能有效,再去做变更和删除. 今天我们 ...
- Unity 游戏框架搭建 2019 (五十六/五十七) 需求分析-架构中最重要的一环&从 EmptyGO 到 Manager Of Managers
我们的项目开始立项的时候,最常见的一个情况就是:几个人的小团队,一开始什么也不做,就开始写代码,验证逻辑,游戏就开始写起来了.而公司的一些所谓的领导层面一开始就把游戏定义为我们要做一个大作.这个事情本 ...
- Unity 游戏框架搭建 2019 (三十六~三十八) partial与public
在上一篇,我们把菜单的顺序从头到尾整理了一遍.在整理菜单顺序的过程中,记录了一个要做的事情. 要做的事情: (完成) 备份:导出文件,并取一个合理的名字. 整理完菜单顺序后,学习新的知识,解决随着示例 ...
- Unity 游戏框架搭建 2019 (四十二、四十三) MonoBehaviour 简化 & 定时功能
MonoBehaviour 简化 在前两篇,我们完成了第九个示例.为了完善第九个示例,我们复习了类的继承,又学习了泛型和 params 关键字. 我们已经接触了类的继承了.接触继承之前,把类仅仅当做是 ...
- Unity 游戏框架搭建 2019 (四十四、四十五) 关于知识库的小结&独立的方法和独立的类
在上一篇,我们完成了一个定时功能,并且接触了 Action 和委托.lambda 表达式这些概念. 到目前为止,我们的库作为知识收录这个功能来说,已经非常好用了,由于使用了 partial 关键字,所 ...
- Unity 游戏框架搭建 2019 (五十、五十一) 消息机制小结&MonoBehaviourSimplify 是框架?
我们花了 5 篇文章学习了消息机制的方方面面.并且完成了一个简易消息机制,之后集成到了我们的 MonoBehaviourSimplify 里. 现在 MonoBehaviourSimplify 有一点 ...
- # Unity 游戏框架搭建 2019 (三十四、三十五) 9 ~ 10 示例整理
第九个示例 目前代码如下: using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace QFramework { p ...
- Unity 游戏框架搭建 2019 (三十九、四十一) 第四章 简介&方法的结构重复问题&泛型:结构复用利器
第四章 简介 方法的结构重复问题 我们在上一篇正式整理完毕,从这一篇开始,我们要再次进入学习收集示例阶段了. 那么我们学什么呢?当然是学习设计工具,也就是在上篇中提到的关键知识点.这些关键知识点,大部 ...
随机推荐
- openwrite使用说明
访问官网https://openwrite.cn/注册登录 访问https://openwrite.cn/plugin-chrome/ 下载插件和安装插件"OpenWrite助手1.1.4& ...
- 动手学Transformer
动手实现Transformer,所有代码基于tensorflow2.0,配合illustrated-transformer更香. 模型架构 Encoder+Decoder Encoder Decode ...
- V - Infinite Prefixes CodeForces - 1295B math
天哪!!菜到家啦. 数学+思维. 首先求出一个周期内cnt0-cnt1=c的个数,如果C=0,那么只要在一个周期内有前缀等于x,那么答案就是-1,否则答案就是0 如果C!=0,列一下方程x=t*c+a ...
- 解决SpringMVC的乱码问题:CharacterEncodingFilter
在使用 SpringMVC 框架的过程中,如果前台有包含中文的请求,或者后台有包含中文的响应,有可能会出现乱码的情况.在以前的 Servlet 中,我们使用 request.setCharacterE ...
- jmeter DB2数据库连接与操作
1.需要把数据库连接jar包拷贝到 jmeter lib目录下 先创建一个数据库连接配置元件 2.添加jdbc请求(我用的后置处理器) 3.可以通过beanshell 对结果集进行操作 beanshe ...
- [PHP] excel 的导入导出
其实excel导入导出挺简单的,导出最简单! 其原理都是把数据读出来,导出是从数据库中读出数据,导入是从文件读出数据! 导出写入文件,导入写入数据库! 但是在导入表的时候,用的是PHPExcel, 不 ...
- Spring5:概念
1.Spring优点 spring是一个开源的免费的框架 spring是一个轻量级的 非入侵式的框架 控制反转(IOC).面向切面(AOP) 支持事务的处理,对框架整合的支持 **总之:spring就 ...
- python 进阶篇 函数装饰器和类装饰器
函数装饰器 简单装饰器 def my_decorator(func): def wrapper(): print('wrapper of decorator') func() return wrapp ...
- Android App安全渗透测试(一)
一. 实验环境搭建 1. 安装JDK 2. 安装Android Studio 3. 模拟器或真机 我的是夜神模拟器和nexus 工具 Apktool ...
- Vue 3.0 Composition API - 中文翻译
Composition API 发布转载请附原文链接 https://www.cnblogs.com/zgh-blog/articles/composition_api.html 这两天初步了解了下 ...