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

做捕鱼时,有种心得,就是感觉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.

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

  1. H5游戏调试技巧归类总结

    使用CocosCreator开发H5手游大半年了 工作中调试占了很大比重,尤其在上线前 H5游戏调试有两个难点:环境和模块 环境困难是指不同操作系统.不同浏览器产生的表现差异 比如以下常见环境 Win ...

  2. 技术干货丨《大天使之剑H5》主程与项目总监:H5游戏的压缩与优化经验

    2019独角兽企业重金招聘Python工程师标准>>> 2018年3月,三七互娱在其主办的中国国际互动娱乐大会上称,<大天使之剑H5>最高单日流水超4000万元,而单月最 ...

  3. 极光会客厅:大型H5游戏如何登陆微信小游戏及游戏性能优化分享

    上周末,由极光网络主办的首期"极光会客厅"正式开门迎客.在本次的"2D小游戏开发实战技术沙龙"上,极光网络客户端主程陈策以及极光网络项目总监陈源向一众与会者分享 ...

  4. android h5游戏图片不缓存,H5小游戏资源缓存方法与流程

    本发明涉及H5资源缓存领域,尤其涉及H5小游戏资源缓存方法. 背景技术: 随着移动互联网的发展和手机硬件性能的不断提升,H5小游戏这种不需要下载安装即可使用的全新游戏应用得到了爆发式发展.这种用完即走 ...

  5. H5游戏性能测试工具 选择与实践总结

    概要 本文会对本人在使用白鹭做h5游戏进行性能测试的过程送使用的工具做一些简单记录. 包括 内存,cpu,耗电,启动时间,网络监控,弱网络,流量几个方面介绍. 背景 玩吧提测有一个性能需要求列表.需要 ...

  6. 王哲iWeb峰会演讲:Cocos引擎不玩概念,厚积薄发助创业者把握H5游戏浪潮

    大家好,我是Cocos引擎创始人王哲.今年我对外的title改叫「首席客服」了,就是为了时刻提醒自己服务好在座的各位开发者.大家有问题可以随时在引擎论坛上找到我. 按照以前iWeb峰会的经验,在座有很 ...

  7. “是男人就下一百层”h5游戏全网最详细教学、全代码,js操作

    "是男人就下一百层"h5游戏全网最详细教学.全代码,js操作 博主的话 游戏展示 编程工具介绍 游戏代码 代码讲解 js 第一步 切换div的显示与隐藏 js 第二步 在菜单页面用 ...

  8. 开源h5游戏 宠物_释放:宠物和动物的开源技术

    开源h5游戏 宠物 我今天早上和猫一起讨论开源技术时,他提出了一个很好的观点:"为什么不写一篇关于动物开源技术的文章?" 你知道,唐纳德的权利. 动物开源技术值得关注. 毕竟,动物 ...

  9. H5游戏开发包括哪些游戏类型

    H5在微信还没有诞生的时候,就已经在各大手机应用方面展露头角.最早的H5小游戏,例如4399公司开发的,开始了新一轮的热潮.随着微信的推出,很多H5游戏开始嵌入到微信公众号.微信群.朋友圈. H5因其 ...

最新文章

  1. 昌宁一中高考成绩表查询2021,昌宁县一中20182019学年上学期高二数学月考试题含解析.docx...
  2. 网站实现个人支付宝即时到帐POST页面
  3. 基于 Consul 实现 MagicOnion(GRpc) 服务注册与发现
  4. 前端学习(1366):express入门
  5. PWN-PRACTICE-CTFSHOW-5
  6. linux生产上线工具,Linux 产能工具及其使用技巧
  7. python使用opencv实现人脸识别系统
  8. MYSQL中5.7.10ROOT密码及创建用户
  9. python图片拼接
  10. android+蓝牙手柄+驱动+win10,jetion手柄驱动万能版
  11. 国外互联网公司大数据技术架构研究
  12. 查看服务器ip配置信息,怎么查看服务器ip地址,怎么查看ip地址和端口
  13. 关于背包问题的递归解法
  14. unity 3d实例:创建游戏对象、旋转的立方体、Unity3D Button、图片按钮、Box控件、Label控件、Background Color、Color
  15. 金山也推隐私保护器,我的隐私谁做主?
  16. createFont(STSong-Light, UniGB-UCS2-H,BaseFont.NOT_EMBEDDED);
  17. linux删除pdf密码
  18. eCharts好看的 响应式 圆环饼图 及文字 附vue源码代码
  19. HTML 文档可以映射为,将PDF文档转换为可通过URL访问的HTML文档的最佳方法
  20. RN学习和开发笔记(一)

热门文章

  1. HTML基础标签总结(仅用作复习,持续补充扩展)
  2. 浙大概率第四版,证明正态分布随机变量不相关等价于独立
  3. python图像处理opencv笔记(二):视频基本操作
  4. oracle bloom过滤,[20180112]11g关闭bloom filter.txt
  5. xwiki开发者指南-编写一个XWiki组件
  6. java springboot 32位的UUID
  7. SEOer应该学着诊断自己的网站
  8. 破解打开证书加密的PDF文档-数字证书(电子书私钥)下载和导入教程
  9. 进入DFU模式恢复教程 iOS9强制降级iOS8教程
  10. 一声叹息,jdk竟然有4个random