消息处理机制(一)
消息处理机制
从中国移动的电话交流开始说起吧,先通过下面一个图了解它们消息交互的原理
信息交流有两种方式:
第一种:在同一个基站,不通过服务器
第二种:在不同基站,必须通过服务器转发。
在我们的游戏中也可以按照这个原理设计一个消息处理机制
1. 第一种方式 不通过服务器
Msg消息----------->对应的Manager--------------->对应的Script
第二种方式 通过服务器
Msg消息--------对应的Manager---------->服务器------------>对应的Manager----------->对应的script
下面是原理图:
考虑到现在大部分游戏都用到热更新技术,所以Lua版的消息处理框架也是类似,那么C#与Lua的交互就是两个消息处理中心去做对接的。
我们这里的核心是MsgCenter,那么它的职责主要做什么呢?(要实现这个机制,涉及到其他一些模块和底层的实现,后面会慢慢提及)
1.怎么样找到对应的Manager(借用计算机网络的网络七层协议中的网络层的原理,SocketIP 192.168.0.1 ---192标记标志你在哪个网段上,168表示你在哪个区域,0表示局域网下的哪个区域。受IP和端口号的启发。100)
下面是公网下不同终端的消息交互的原理图:
借鉴这个原理,我让每个Manger处理3000条消息,这在游戏中已经够用了。
然后呢我定义了一个枚举,通过这个枚举把每个模块的处理消息区间大致抽象出来,每个Manager分别对应着自己的ManagerId。
定义一个消息ID 是3003,怎么样找到对应的UIManager?
通过下面的GetManager()方法,这里涉及到一个数学公式,就不多做说明了。
2.怎么样在Manager中找到对应的script?
这里我先说一下Manger的功能
a. 存储对应的注册进来的msg。
b. msg来了以后找到对应的脚本。
要满足这两种条件的数据结构是什么?
首先考虑的是第一种数据结构Dictionary<ushort,MonoBehaviour> 但是这种方式只能满足一个mono脚本对应一个msg,
那么如果有多个mono脚本,对应一个msg,比如说多个脚本(不仅仅是manager,还有它管理的script)要监听一个消息msg,
那么一个mono脚本可以注册多条消息,这种方式就满足不了需求,所以这里我们采用另一种数据结构字典套链表
Dictionary<ushort,EventNode>。
链表:
那么我们在C#中怎么实现这种数据链路?
这里我借助了节点,定义节点类,然后里面有个数据域和指针域。
如下:
涉及到的其他类
节点类
因为每个manager里面都会有存储消息,注册消息等相关功能,所以我把它们提取在ManagerBase父类里面,其他Mnager只需要继承就可以了。
/// <summary>
/// Manager的基类
/// </summary>
public class ManagerBase : MonoBase{
注意:这里之所以用ushort,是因为每个msg都有一个msgId,我们可以根据msgId去匹配相应的msg对象,所以我们这个字典可以理解为以msgId作键,以可以监听这条消息的脚本链表作值。这样我们就可以做到一个消息msg对应多个mono脚本,那么一个脚本呢,也可以包含多个消息msg。用数据结构来说,就是可以做到多对多。
/// <summary>
/// 存储注册消息
/// </summary>
public Dictionary<ushort, EventNode> eventTree = new Dictionary<ushort, EventNode>();
/// <summary>
/// 往一个脚本里注册若干消息
/// </summary>
/// <param name="mono">要注册的脚本</param>
/// <param name="msgs">一个脚本可以注册多条消息,数组里面的值就是每个消息的msgId</param>
public void RegisterMsg(MonoBase mono,params ushort[] msgs )
{
for (int i = 0; i < msgs.Length; i++)
{
EventNode node = new EventNode(mono);
RegisterMsg(msgs[i], node);
}
}
/// <summary>
/// 注册一个消息链表
/// </summary>
/// <param name="msgId">根据msgId</param>
/// <param name="data">node链表</param>
public void RegisterMsg(ushort msgId, EventNode node)
{
if (!eventTree.ContainsKey(msgId))
{
eventTree.Add(msgId, node);ss
}
else
{
EventNode temp = eventTree[msg];
//如果当前键对应的空间不止一个节点
//那么遍历节点,找到最后为空的那个节点,
while (temp.next != null)
{
temp=temp.next;
}
//把node添加在最后
temp.next = node;
}
}
/// <summary>
/// 去掉一个脚本的若干个消息
/// </summary>
/// <param name="mono"></param>
/// <param name="msgs"></param>
public void UnRegisterMsg(MonoBase mono, params ushort[] msgs)
{
for (int i = 0; i < msgs.Length; i++)
{
UnRegisterMsg(msgs[i], mono);
}
}
/// <summary>
/// 去掉一个消息链表
/// </summary>
/// <param name="msgId"></param>
/// <param name="node"></param>
public void UnRegisterMsg(ushort msgId, MonoBase node)
{
if (!eventTree.ContainsKey(msgId))
{
Debug.LogWarning("not contail id==" + msg);
return;
}
else
{
EventNode temp = eventTree[msgId];
//去掉头部,包含两种情况
if (temp.data == node)
{
EventNode header = temp;
//后面多个节点
if (header.next != null)
{
header.data = temp.next.data;
header.next = temp.next.next;
}
else //只有一个节点的情况
{
eventTree.Remove(msgId);
}
}
else //去掉尾部和中间的节点
{
while (temp.next != null && temp.next.data != null)
{
temp = temp.next;
} //表示已经找到该节点
//没有引用 ,自动释放
if (temp.next.next != null) //去掉中间
{
temp.next = temp.next.next;
}
else //去掉尾部
{
temp.next = null;
}
}
}
}
/// <summary>
/// 来了消息,通知整个消息链表
/// </summary>
/// <param name="msg"></param>
public override void ProcessEvent(MsgBase msg)
{
if (!eventTree.ContainsKey(msg.msgId))
{
//方便找错
Debug.LogError("not contains msgid==" + msg.msgId);
Debug.LogError("Msg Manager==" + msg.GetManager());
return;
}
else
{
EventNode temp = eventTree[msg.msgId];
//通过key 找到链表,然后全部通知
do
{
if (temp.next != null && temp.next.data != null)
{
temp = temp.next;
}
//策略模式 消息自己处理
temp.data.ProcessEvent(msg);
} while (temp!=null);
}
}
}
这里涉及到一个知识点:链表中移除值的时候,有一下三种情况:
1. 在尾部 怎么移除
2. 在中间 怎么移除
3. 在头部怎么移除
那么这里怎么处理的我在UnRegisterMsg()方法里面做了处理。