MsgQ机制,实现H5游戏的模块彻底分离

5 篇文章 0 订阅
2 篇文章 1 订阅

这是绝对是个好东西!!!

做捕鱼时,有种心得,就是感觉puremvc真的只适合用来开发表现层,做做界面之类的东西,想以它作为整个游戏架构的支撑,是行不通的,因为会存在很多诟病。所以在项目接近收尾的时候,我就一直在思考如何对H5游戏中的模块进行彻底解耦。

在另一篇博文中我己提到了项目的三层结构,但层归层,分层不等于分模块,如果想要降低项目维护的风险,除了分层,合理的模块化也很重要,尤其在一个多人合作项目中,如何保持模块的独立性为开发的重中之重。

回到捕鱼项目,重新啰嗦一句,我有意将这个项目在结构上分三层:网络层、数据逻辑层和表现层,表现层又分为一般界面和游戏界面,另外有个公共库为Common,再上一张手稿意思下。。。

作下简单说明

1. Common: 公共库,这里面提供了一些所有模块都可以共作的公共方法

2. Network:网络层(即NET模块)

3. Framework:逻辑层(包括OSL模块)

4. MMI:表现层(包括CUI和GUI模块)

其中

1. DB为底层数据接口,可以被任意模块读取,但建议只在OSL层写入(未实现强制机制)

2. Application和Component为MMI层的公共库

取故这个游戏从架构上分有三层,从业务上分有四个模块,现在的主要想法就是实现模块间的彻底分离,禁止模块之间直接联系,保持模块的独立性。由于个人比较偏爱观察者模式和命令模式,所以整个框架还是基于puremvc的命令模式驱动的,由于命令贯穿了整个框架的所有业务,故这种思路还是较简单的。

跨模块消息的传递互斥:

若想禁止消息的跨模块传递,只要设计一种机制让消息在模块之间互斥就行了,我的做法是根据消息前缀来实现消息的互斥,先上接口:



    /**
     * 互斥体,用于实现模块之间的消息互斥
     */
    namespace Mutex {
        /**
         * 激活互斥体的MsgQ模块
         * 说明:
         * 1. 当此变量的值为-1时,允许激活互斥体
         * 2. 首次引用互斥体视为激活互斥体
         * 3. 激活互斥体的模块将被记录在此变量中
         * 4. 若激活消息的模块为MMI模块,则此记录值允许被替换成其它MMI模块的消息,仅第一次生效
         * 5. 此变量会在互斥引用为0时重新置为-1
         */
        let actMsgQMod: MsgQModEnum;

        /**
         * 是否校验消息前缀,默认为false
         */
        let checkPrefix: boolean;

        /**
         * 激活互斥体
         */
        function active(msgQMod: MsgQModEnum): void;

        /**
         * 关闭互斥体
         */
        function deactive(): void;

        /**
         * 锁定互斥体
         */
        function lock(name: string): void;

        /**
         * 释放互斥体
         */
        function unlock(name: string): void;

        /**
         * 判断是否允许执行MMI的行为
         */
        function enableMMIAction(): boolean;

        /**
         * 为对象初始化一个互斥量
         */
        function create(name: string, target: Object): void;

        /**
         * 释放互斥量
         */
        function release(name: string, target: Object): void;
    }

简单的说,当消息通过sendNotification开始进行传递的时候,先调用Mutex.active来激活互斥体,锁定首次派发消息的模块为消息活动的模块,并记录消息前缀,在消息传递的过程中,每次传递都会先调用lock来获取互斥体,若嵌套的消息前缀与当前记录的消息前缀一致,则视为同一个模块的消息,若不一致,则视为跨模块传递,举个例子:

class AXCommand extends puremvc.SimpleCommand {

	execute() {
		// 发送同模块消息
		this.facade.sendNotification("A_Y");
		// 发送跨模块消息
		this.facade.sendNotification("B_X");
	}
}

puremvc.Facade.getInstance().registerCommand("A_X", AXCommand);
puremvc.Facade.getInstance().sendNotification("A_X");

当消息A_X被发送时,Mutex.lock会进行校验,因为是首次校验,故会将MsgQ模块指定为默认类型,然后再将A标记为当前模块的消息前缀,在AXCommand执行时,A_Y消息首先被发送,Mutex.lock会再次校验,获取消息前缀与当前模块活动的消息进行对比,由于一致,故视其为模块内的消息,A_Y消息将会成功被发送,当B_X消息被发送时,由于消息前缀为B,与记录不一致,故Mutex.lock会抛出错误,阻断消息的传递,并提示:

Error: 禁止跨模块监听消息,src:A, dest:B

当激活互斥体的消息完成传递时,Mutex.deactive会被执行,互斥体也会被彻底释放。

禁止消息的跨模块监听:

当然啦,光禁止消息的跨模块传递还是不够的,还需要实现禁止消息的跨模块监听才行,接口还是上面的接口,思路略讲一下:

消息监听被注册时,会调用Mutex.create来生成互斥量,移除监听时会调用Mutex.release来释放互斥量,若消息在注册时,互斥量不存在,则会记录监听的MsgQ模块,若互斥量己存在,则互斥体会校验新消息是否与之前注册的消息属于一个模块,若不是则抛出错误,否则允许注册。

这样一来,消息的跨模块发送和监听都己经被禁止了,现在应当着手解决模块间的通讯问题。

MsgQ机制:

按照惯例,还是先上接口:

    /**
     * MsgQ的模块枚举
     */
    enum MsgQModEnum {
        /**
         * 通用界面
         */
        CUI = 1,

        /**
         * 游戏界面
         */
        GUI,

        /**
         * 逻辑层
         */
        OSL,

        /**
         * 网络层
         */
        NET
    }

    /**
     * MsgQ的消息对象
     */
    interface IMsgQMsg {
        /**
         * 发送消息的模块
         */
        src: MsgQModEnum;

        /**
         * 接收消息的模块
         */
        dest: MsgQModEnum;

        /**
         * 消息编号
         */
        id: number;

        /**
         * 消息挂载的数据
         */
        data: any;
    }


    /**
     * MsgQ接口类
     * 设计说明:
     * 1. 设计MsgQ的主要目的是为了对不同的模块进行彻底的解耦
     * 2. 考虑到在实际环境中,网络可能存在波动,而UI层可能会涉及到资源的动态加载与释放管理,故MsgQ中的消息是以异步的形式进行派发的
     * 3. 由于MsgQ的异步机制,故每条消息的处理都必须考虑并避免因模块间的数据可能的不同步而带来的报错问题
     */
    namespace MsgQ {

        /**
         * 发送消息(异步)
         */
        function send(src: MsgQModEnum, dest: MsgQModEnum, id: number, data: any): void;

        /**
         * 获取消息(内置方法)
         * @id: 只获取指定ID消息,若为void 0则不校验
         */
        function fetch(mod: MsgQModEnum, id?: number): IMsgQMsg;

        /**
         * 判断模块是否己激活(内置方法)
         */
        function isModuleActive(mod: MsgQModEnum): boolean;

        /**
         * 设置模块是否己激活(内置方法)
         */
        function setModuleActive(mod: MsgQModEnum, active: boolean): void;
    }

在MsgQModEnum中枚举了所有的模块ID,IMsgQMsg则是消息结构,也贼简单,msgQ.send则是发送接口,这里有三个标记为内置方法的方法,实际上在不公开的方法,因为MsgQ机制中不应该公开这些方法出来,防止你在开发过程中,这边发送那边接口。

那么,MsgQ消息是如何被接收并处理的呢?

MsgQService介绍:

MsgQ消息在发送出去之后,其实并没有直接传递到目标模块,而是被缓存在消息队列中的

        /**
         * 发送消息(异步)
         * export
         */
        export function send(src: MsgQModEnum, dest: MsgQModEnum, id: number, data: any): void {
            if (isModuleActive(dest) === false) {
                console.warn(`消息发送失败,模块己暂停 mod:${MsgQModEnum[dest]}`);
                return;
            }
            if (check(dest, id) === false) {
                console.warn(`消息发送失败,消息ID非法 mod:${dest}, id:${id}`);
                return;
            }
            let array: IMsgQMsg[] = $queues[dest] || null;
            if (array === null) {
                array = $queues[dest] = [];
            }
            const msg: IMsgQMsg = {
                src: src,
                dest: dest,
                seqId: seqId,
                id: id,
                data: data
            };
            array.push(msg);
        }

为了防止外部直接获取MsgQ的消息,故我设计了MsgQService类,然后在MsgQ中内置了一个fetch方法,来允许MsgQService来主动获取消息,直接上代码吧:


    /**
     * MsgQ服务类(主要用于模块间的解偶)
     * 说明:
     * 1. 理论上每个MsgQ模块都必须实现一个MsgQService对象,否则此模块的消息不能被处理
     * export
     */
    export abstract class MsgQService extends BaseService {

        /**
         * 启动回调
         * export
         */
        protected $onRun(): void {
            MsgQ.setModuleActive(this.msgQMod, true);
            this.facade.registerObserver(NotifyKey.MSG_Q_BUSINESS, this.$onMsgQBusiness, this);
        }

        /**
         * 停止回调
         * export
         */
        protected $onStop(): void {
            MsgQ.setModuleActive(this.msgQMod, false);
            this.facade.removeObserver(NotifyKey.MSG_Q_BUSINESS, this.$onMsgQBusiness, this);
        }

        /**
         * 响应MsgQ消息
         */
        private $onMsgQBusiness(mod: MsgQModEnum): void {
            let msg: IMsgQMsg = null;
            while (true) {
                msg = MsgQ.fetch(this.msgQMod);
                if (msg === null) {
                    break;
                }
                this.$dealMsgQMsg(msg);
            }
        }

        /**
         * 处理MsgQ消息
         * export
         */
        protected abstract $dealMsgQMsg(msg: IMsgQMsg): void;
    }

这样一来,如果你想响应MsgQ发送的消息,就必须定义一个Service来继续MsgQService,然后实现它的$dealMsgQMsg接口,在这个接口中处理消息,如:

export class OslService extends suncore.MsgQService {

    protected $dealMsgQMsg(msg: suncore.IMsgQMsg): void {
        switch (msg.id) {
            case OslMsgQIdEnum.LOGIN_REQ:
                this.facade.sendNotification(NotifyKey.MSG_LOGIN_REQ, msg.data);
                break;
            case OslMsgQIdEnum.ENTER_FISHERY_REQ:
                this.facade.sendNotification(NotifyKey.MSG_ENTER_FISHERY_REQ, msg.data);
                break;
        }
    }
}

可能你会问,为什么OslService能派发MSG事件,那么用它来派发MMI事件行不行,答案是不行,为什么呢,因为从sendNotifacation的调用方式上就可以看出,MsgQService其实是继承puremvc.Notifier的,它的消息也会触发互斥机制,那么,MsgQService在处理MsgQMsg的时候,如何确定自己的所属模块呢?上面的代码缺了一段,我补上你就知道是为啥了:

const service:OslService = new OslService(suncore.MsgQModEnum.OSL);

OslService在构建的时候,是需要指定所属的MsgQ模块的,实际上是因为它是间接继承puremvc.Notifier的,emmmmmmmm,上接口吧:

    class Notifier implements INotifier {

        constructor(msgQMod?:suncore.MsgQModEnum);

        protected readonly facade: IFacade;

        protected readonly msgQMod: suncore.MsgQModEnum;
    }

所以,sendNotification就是这么确认第一次被执行时,自己的消息应当属于哪个MsgQ模块的。

最后还有个问题,就是:

如何将puremvc的接口限制在表现层?

这是很有必要的,因为我们在架构上做了三层设计,而puremvc中的Mediator只与视图有关,Model大多数也与视图中需要存储的数据有关,而OSL层己经有DB的设计了,故Model实际上并不应该被非表现层访问。

我的做法是这样的,若没有为puremvc.Notifier的对象指定MsgQ模块,则默认它发送的消息为表现层消息,即MMI消息,这个消息类型是内置的,MMI消息在传递过程中,若遇到某个表现层的模块消息,则会锁定为对应的表现层模块,模块一旦锁定,就再也不能重新锁定了;但若传递的消息为非MMI层消息,则会抛出错误。互斥行为同样对Mediator和Proxy接口生效,这样就实现了puremvc除了sendNotification之外,其它所有接口都只向MMI模块开放的限制。

代码好像没啥上的,因为只有一句Mutex.deactive,那就这样吧

若有喜欢,欢迎下载使用,git地址:

puremvc请下载suncore的分支来使用:https://github.com/syfolen/laya-puremvc-typescript

基础库:https://github.com/syfolen/suncom

suncore:https://github.com/syfolen/suncore

结语:遵重原著版权,puremvc版权归原作者 Frederic Saunier 所有,若有侵犯,烦告知

PureMVC Standard Framework for TypeScript - Copyright © 2012 Frederic Saunier

PureMVC Framework - Copyright © 2006-2012 Futurescale, Inc.

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值