游戏架构之模块间通信(消息机制)

版权声明:欢迎大家留言讨论共同进步,转载请注明出处 https://blog.csdn.net/qq_39108767/article/details/83447911

一、

先谈一谈个人对游戏框架的一点理解,顾名思义,框架是一个项目的骨架,如同大树的主干,搭建框架,在此基础上再加入各个功能模块,构成有一个完整的项目。如同一棵树有一个健壮的主干,再从主干上生长出一个一个的分支,最终长成一颗枝繁叶茂的大树。此外,框架会设定好模块的基本格式,更加有利于功能的模块化;框架还负责各个模块之间的交互,每个模块作为一个独立的个体,内部是独立运行的,如果模块间需要进行一些交互,则需要通过框架来实现,避免模块间直接通信,最终模块关系错综复杂,难以维护。

二、

模块间的数据交互、信息传递是框架中比较重要的一部分,最近根据做过的几个项目和一些资料,编写了一套简单的模块间信息传递机制,在此之前也发过几篇关于模块封装的博文,组装到一起,应该也是可以用了。

关于消息机制(消息/广播/通知···有多个叫法,不过实现的功能都是类似的),主要应用了观察者模式(https://blog.csdn.net/qq_39108767/article/details/83386856),在此基础上在做一下功能扩展,大概原理是:

1. 消息:由唯一消息ID、消息体组成(有的写法也会将消息ID分离出消息体,不包含在消息体内,这样方便消息转发都多个不同模块,但不便于管理)。消息ID,用int值表示,根据需求划分一定数量的ID给每个模块,模块内部单独管理;消息体:数据信息的载体,一般是一个子类,这样方便不同模块自定义数据格式。

要注意一点,跨模块消息,A模块需要B模块的数据,就需要注册B模块的消息,这样B在发送消息之后,只要注册过这条消息的模块,都会接收到消息,这也要求模块内定义ID后,不能随意变动ID,建议采用枚举表示,使用时将枚举转为Int。

2. 建立消息中心,保存所有的消息及对应接收回调函数,各模块通过管理者将消息注册到消息中心,有对外的发消息接口,供各模块调用,当然同样要有注销接口。在收到消息之后执行对应的回调函数,将参数传递到注册过消息的多个具体模块,模块内部自行处理。

3. 各个模块管理者,在脚本运行开始,注册所需要的消息,在脚本待销毁的时候注销,提供一个消息接收回调,消息中心会将消息下发到回调,然后内部处理消息。

4. 关于消息中心保存记录消息,我用的字典Dictionary<int,委托>保存对应的ID和回调,利用委托的一个优点就是委托的“+=”和“-=”,比如有多个模块注册了同一个消息,可以将callback+=newCallback,这样来把所有的回调记录下来,在注销时减掉。

但委托减法具有不可预测的结果,虽然改成Event事件可以避免程序报错,但结果与委托一样也会有这种问题,为了避免出现问题,在使用减法时,每次只减掉一个元素(即 a-= b,不要a-=(b+c)  ),就不会发生意外了,可以忽略代码里的警告了

官方文档解释  Code Inspection: Delegate subtractions

http://www.jetbrains.com/help/resharper/2018.2/DelegateSubtraction.html

Demo代码如下,写的比较简单,功能还待完善~~

//消息中心主要负责注册、注销消息,发送消息到对应模块的回调

public delegate void CallbackDele(Msg msg);

    //消息体 父类
    public class Msg
    {
        public int msgId { get; protected set; }
        public object sender { get; protected set; }
    }

    //消息中心
    public class NotifyManager : MonoBehaviour
    {
        //单例
        static NotifyManager instance;
        public static NotifyManager Instance
        {
            get
            {
                if (instance == null)
                {
                    GameObject newObj = new GameObject("NotifyCenter");
                    instance = newObj.AddComponent<NotifyManager>();
                    DontDestroyOnLoad(newObj);
                }
                return instance;
            }
        }

        //记录已注册消息
        Dictionary<int, CallbackDele> callbackDic = new Dictionary<int, CallbackDele>();

        //记录待处理事件
        Queue<Action> todoCallback = new Queue<Action>();


        //注册消息
        public bool Attach(CallbackDele callback, int[] msgIds)
        {
            if (callback == null)
                return false;

            for (int i = 0; i < msgIds.Length; i++)
            {
                Attach(callback, msgIds[i]);
            }
            return true;
        }
        public bool Attach(CallbackDele callback, int msgId)
        {
            if (callback == null)
                return false;

            if (!callbackDic.ContainsKey(msgId))
            {
                callbackDic.Add(msgId, callback);
            }
            else
            {
                callbackDic[msgId] += callback;
            }
            return true;
        }

        //注销消息
        public bool Detach(CallbackDele callback, int[] msgIds)
        {
            if (callback == null)
                return false;
            for (int i = 0; i < msgIds.Length; i++)
            {
                Detach(callback, msgIds[i]);
            }
            return true;
        }
        public bool Detach(CallbackDele callback, int msgId)
        {
            if (!callbackDic.ContainsKey(msgId) || callback == null)
                return false;

            //委托减法具有不可预测的结果:官方文档解释
            //http://www.jetbrains.com/help/resharper/2018.2/DelegateSubtraction.html
            //每次减掉一个委托,不会发生意外,可忽略该警告
            callbackDic[msgId] -= callback;

            if (callbackDic[msgId] == null)
                callbackDic.Remove(msgId);

            return true;
        }

        //通知/广播/分发消息
        public bool PostMsg(Msg msg = null)
        {
            if (msg.msgId > 0 && callbackDic.ContainsKey(msg.msgId) && null != callbackDic[msg.msgId])
            {
                //加入队列
                lock (todoCallback)
                {
                    todoCallback.Enqueue(() => callbackDic[msg.msgId](msg));
                }
                return true;
            }
            return false;
        }

        //刷新待处理消息事件
        void Update()
        {
            if (todoCallback.Count == 0)
                return;

            lock (todoCallback)
            {
                while (todoCallback.Count > 0)
                {
                    todoCallback.Dequeue()();
                }
                todoCallback.Clear();
            }
        }

    }

//每个消息对应唯一ID,每个模块分配一定数量的ID,定义模块的起始ID

    public class MsgIdSetting
    {
        public const int mgrIdSpan = 100;
    }
    
    public enum MgrId
    {
        //分模块划分消息ID,定义Id起点个长度,每个模块单独管理自己的Id
        
        //0~~99
        demo01MgrId = 0,
        
        //100~~199
        demo02MgrId = MsgIdSetting.mgrIdSpan * 1,
        
        //200~~299
        demo03MgrId = MsgIdSetting.mgrIdSpan * 2,
        
        // ··· ···
    }

//单例模板,每个模块管理者继承模板

public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
    {
        //单例
        private static T instance;
        public static T Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = FindObjectOfType(typeof(T)) as T;
                    if (instance == null)
                    {
                        GameObject newObj = new GameObject(typeof(T).ToString());
                        instance = newObj.AddComponent<T>();
                    }
                }
                return instance;
            }
        }
    }

//测试Demo

//模块管理者需要定义自己的消息体格式,消息ID,在指定的时机注册、注销所需消息

//任何脚本都可以发送消息,发送后会执行对应注册的callback回调

public enum Demo01MsgId
{
    //模块消息Id,获取起点Id,依次取值
    dufaultId = MgrId.demo01MgrId,
    creatRole = MgrId.demo02MgrId + 1,
    deleteRole = MgrId.demo03MgrId + 2,
    // ······
}
public class Demo01Msg : Msg
{
    //模块自定义消息体
    public Demo01Msg(int newmsgId, string newname, bool newsexual, int newage, object newsender = null)
    {
        msgId = newmsgId;
        name = newname;
        sexual = newsexual;
        age = newage;
        sender = newsender;
    }
    public string name;
    public bool sexual;
    public int age;
}
public class Demo01Manager : MgrSingle<Demo01Manager>
{
    //Awake
    protected override void Awake()
    {
        NotifyManager.Instance.Attach(Callback, new int[] { (int)Demo01MsgId.creatRole, (int)Demo01MsgId.deleteRole });
    }
    void OnDestroy()
    {
        NotifyManager.Instance.Detach(Callback, new int[] { (int)Demo01MsgId.creatRole, (int)Demo01MsgId.deleteRole });
    }
    void Callback(Msg msg)
    {
        if (msg == null || msg.msgId <= 0)
        {
            Debug.Log("Receive A Empty Msg");
        }
        else
        {
            switch(msg.msgId)
            {
                case (int)Demo01MsgId.creatRole:
                    Demo01Msg creatRoleMsg = msg as Demo01Msg;
                    Debug.Log("Creat Role: " + creatRoleMsg.name + "-" + creatRoleMsg.sexual + "-" + creatRoleMsg.age);
                    break;
                case (int)Demo01MsgId.deleteRole:
                    Demo01Msg deleteRoleMsg = msg as Demo01Msg;
                    Debug.Log("Delete Role: " + deleteRoleMsg.name + "-" + deleteRoleMsg.sexual + "-" + deleteRoleMsg.age);
                    break;
                default:
                    Debug.LogWarning("Receive A Msg Without Callback");
                    break;
                //······
            }
        }
    }
    public void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            SendMsg();
    }
    void SendMsg()
    {
        Demo01Msg msg = new Demo01Msg((int)Demo01MsgId.creatRole, "XiaoMing", true, 18, this);
        NotifyManager.Instance.PostMsg(msg);
    }
}
public class Demo02Manager : MgrSingle<Demo02Manager>
{
    //Awake
    protected override void Awake()
    {
        NotifyManager.Instance.Attach(Callback, (int)Demo01MsgId.creatRole);
    }
    void OnDestroy()
    {
        NotifyManager.Instance.Detach(Callback, (int)Demo01MsgId.creatRole);
    }
    void Callback(Msg msg)
    {
        if (msg == null)
        {
            Debug.Log("Receive A Empty Msg");
        }
        else
        {
            switch (msg.msgId)
            {
                case (int)Demo01MsgId.creatRole:
                    Demo01Msg creatRoleMsg = msg as Demo01Msg;
                    Debug.Log("Demo02 Receive Demo01 Msg: Creat Role");
                    break;
                default:
                    Debug.LogWarning("Receive A Msg Without Callback");
                    break;
                    //······
            }
        }
    }
}

三、

以上只是一种比较常见的消息机制,在此基础上还可以进行改进、封装,因为涉及到一些项目,这里不粘代码了,简单说一下设计思路吧 

1. 消息分类:这一点与上面Demo一样,按模块对消息进行分类

2. 消息中心:每个模块的管理者作为一个消息中心,负责本模块消息的 注册、注销、处理。总消息中心,不处理具体消息,只负责不同模块间的消息流转。

要注册一条消息,模块先判断是否是本模块消息,是的话直接注册到本模块,若不是,则转发到上级的消息中心,消息中心再将消息识别下发到对应的模块,对应模块进行注册。

原本所有的消息都是在消息中心进行处理,现在在模块内部处理,消息中心只负责将消息下发到对应的模块即可。

比如说北京有一个快递中心,一天,在朝阳区的A要寄快递给海淀区的B,A找到朝阳区的快递员上门取件,快递员取件后将快递送到快递中心,再由快递中心识别快递物品,委派海淀区的快递员将快递配送给B。但第二天,朝阳区的A想要寄快递给同在朝阳区的C,同样朝阳区的快递员会上门取件,然后将快递送到快递中心,经识别后将快递委派给朝阳区的快递员,再配送给C。这样就显得比较繁琐了,快递中心的负荷也会非常大。

快递中心感觉这样好心累,要进行改革,于是增大了快递员的权利,可以直接处理自己负责地区的快递,无需再经过快递中心。这样A在寄快递给C的时候,朝阳区的快递员从A取件之后,发现这是朝阳地区内的快递,是寄给C的,就可以直接配送给C,省时又省力。如果A再给B寄快递,朝阳区的快递员取件之后,识别快递是其他地区的,就直接将快递送到快递中心,快递中心收到之后,只需识别是海淀区的,无需关注收件人是谁,再将快递流转到海淀区的快递员,由该快递员配送到C手中。这样来,整个快递流程就完美了~~

3. 记录消息及其回调:

网上搜到的大都是用委托或事件来记录消息的,前面也提到过,利用“+=”“-=”计算可以记录一条消息和多个回调。

也想过用每个消息用一个List来记录所有的回调,但明显这样是不可取的。

这里介绍另一种记录方式:

记录的不是具体某个回调函数,而是采用链表的方式记录回调函数所在的类。

3.1 写一个父类或接口,定义一个Callback函数,所有的模块管理者,都重写或实现该函数,用作消息的回调。

3.2 定义一个消息节点类(包含两个属性,本节点的回调脚本,下一个节点Next),注册消息的第一个回调类后,其Next指向第二个回调类,依次类推,记录一个消息的所有回调类。

3.3 只需记录消息ID和第一个节点,获取第一个节点后依次获取节点的Next节点,知道Next节点为空,即遍历完所有节点。

3.4 在收到消息之后,遍历所有的节点,执行回调类的回调函数。

关于游戏架构,消息/通知机制只是其中的一部分,还有很多很多需要去学习去实践,希望以上的内容可以帮助到大家~~~

猜你喜欢

转载自blog.csdn.net/qq_39108767/article/details/83447911