笔者是从十年前页游时代开始进入游戏行业的,那时候比较流行的框架是MVC框架,这些年陆续出现了很多类似的框架,总的来讲万变不离其宗,都是做到了数据和UI分离,模块化,高内聚,低耦合,模块间正交性越高,越容易解耦。以前做Flash游戏的时候,用了一个叫PureMVC的框架,很好用,那时候刚刚入门,主要了解如何使用,如何发送消息,随着在编程行业沉浸多年后,慢慢的对游戏框架和引擎设计有了新的认识。
这次主要聊聊在Cocos Creator 这样的ECS(实体组件)系统里如何和PureMVC结合使用,之前我也写过如何在Layabox里面用PureMVC框架。一年后的今天,再次谈谈如何在Cocos Creator中用这个框架
介绍 关于puremvc的介绍,网上一搜一大堆,说白了就是它是一个多语言实现的一个mvc轻量级架构
目前支持的语言:ACTIONSCRIPT C++ C# COLDFUSION DART DELPHI GO HAXE JAVA JAVASCRIPT KOTLIN OBJECTIVE C PERL PHP PYTHON RUBY SWIFT TYPESCRIPT
由此看出,市场上流行的语言基本都是支持的,当然官网没有实现的有的也可以在网上搜到其他非官方的版本。
自己是从AS3开始用的,后面经历的JavaScript 到Typescript,于是又开始使用这个框架。
仓库地址 https://github.com/PureMVC
Typescript 标准版地址:https://github.com/PureMVC/puremvc-typescript-standard-framework
TYpescript 多核版地址: https://github.com/PureMVC/puremvc-typescript-multicore-framework
我一般选择使用 标准版即可,下面看看标准版项目仓库的目录
开发团队做的比较好,给出了测试例子,编译工具 ant+yuicompressor 源码目录
简单的介绍一下这个目录:
bin: 编译后的.d.ts文件 .js文件.min.js文件
build: yuiant 和yuicompressor 压缩合并工具(项目构建使用)
test: 官方给了一个web 版本的 测试demo
其他说明信息配置
环境
Mac 系统
Cocos Creator 2.4.0
PureMVC Typescript 标准版
先说说如何使用吧。
配置步骤
新建一个项目:Cocos-Creator-PureMVC-Demo
首先下载仓库到本地,然后把bin 目录的 .d.ts文件 .js文件(当然用min.js也可以) 复制到Cocos的assets 目录,这里我新建了一个叫lib的文件夹,专门存放第三方类库。
然后在Cocos Creator编辑器里面选择插件方式
此时回到代码编辑器里面,这个导出的源代码需要重新指定一下全局变量 puremvc,具体操作如下
此时puremvc 的类配置工作已经做好,剩下的就是初始化工作:
编写步骤
编写我们额第一个初始化类:App.ts(文件挂在游戏内的常驻节点上面)
代码具体内容如下
App.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 import IMediator = puremvc.IMediator;import IProxy = puremvc.IProxy;import Browser from "./channel/Browser" ;import {AppFacade} from "./core/AppFacade" ;const {ccclass,disallowMultiple, menu,executionOrder} = cc._decorator;@ccclass @disallowMultiple ()@menu ('常住节点组件/App' )@executionOrder (-10000 )export default class App extends cc.Component { protected onLoad() { cc.game.addPersistRootNode(this .node); } start() { console .log("puremvc框架开始初始化..." ); App.instance.startup(); if (Browser.onAndroid) { console .log("Android添加监听返回键" ) cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this .onKeyDown, this ); } } private onKeyDown(event):void { switch (event.keyCode) { case cc.macro.KEY.back: cc.game.end(); break ; default : } } public static registerProxy( proxy:IProxy ):void { this .instance.registerProxy(proxy); } public static retrieveProxy(name: string ,data:any ): IProxy { let proxy = this .instance.retrieveProxy(name); proxy.setData(data); return proxy; } public static removeProxy(name: string ): IProxy { return this .instance.removeProxy(name); } public static registerMediator( mediator:IMediator ):void { this .instance.registerMediator(mediator); } public static retrieveMediator(name: string ,node:cc.Node): IMediator { if (!this .instance.hasMediator(name)){ console .error(`Mediator ${name} 未注册` ); } let mediator = this .instance.retrieveMediator(name); mediator.setViewComponent(node); return mediator; } public static removeMediator(name: string ): IMediator { return this .instance.removeMediator(name); } public static sendNotification(name:string , body?:any , type ?:string ):void { this .instance.sendNotification(name,body,type ); } public static get instance():AppFacade{ return AppFacade.getInstance(); } }
AppFacade.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import Facade = puremvc.Facade;import IFacade = puremvc.IFacade;import {NotificationConst} from "./NotificationConst" ;import StartupCommand from "./StartupCommand" ;export class AppFacade extends Facade implements IFacade { constructor ( ) { super () } public static getInstance(): AppFacade { if (!this .instance) this .instance = new AppFacade(); return <AppFacade>(this .instance); } public initializeFacade(): void { super .initializeFacade(); this .registerCommand(NotificationConst.START_UP, StartupCommand); } public startup(stage?: any ): void { this .sendNotification(NotificationConst.START_UP, stage); this .removeCommand(NotificationConst.START_UP); } }
StartupCommand.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /** * Created by jsroads on 2020/6/11.2:40 下午 * Note: */ import MacroCommand = puremvc.MacroCommand; import ControllerCommand from "./ControllerCommand"; import ModelPrepCommand from "./ModelPrepCommand"; import ViewPrepCommand from "./ViewPrepCommand"; export default class StartupCommand extends MacroCommand { constructor() { super(); } //添加子Command 初始化MacroCommand. public initializeMacroCommand(): void { /** * 命令会按照“先进先出”(FIFO)的顺序被执行. * 在用户与数据交互之前,Model必须处于一种一致的已知的状态. * 一旦Model 初始化完成,View视图就可以显示数据允许用户操作与之交互. * 因此,一般“ 开启”(startup )过程首先Model初始化,然后View初始化。 */ this.addSubCommand(ControllerCommand); this.addSubCommand(ModelPrepCommand); this.addSubCommand(ViewPrepCommand); } }
ControllerCommand.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import SimpleCommand = puremvc.SimpleCommand;import INotification = puremvc.INotification;import {NotificationConst} from "./NotificationConst" ;import LoginCommand from "../controller/LoginCommand" ;export default class ControllerCommand extends SimpleCommand { constructor ( ) { super (); } public execute(note: INotification): void { this .facade.registerCommand(NotificationConst.LOGIN, LoginCommand); } }
ModelPrepCommand.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import SimpleCommand = puremvc.SimpleCommand;import INotification = puremvc.INotification;import LoginProxy from "../model/proxy/LoginProxy" ;export default class ModelPrepCommand extends SimpleCommand { constructor ( ) { super (); } public execute(note: INotification): void { this .facade.registerProxy(new LoginProxy()); } }
ViewPrepCommand.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import SimpleCommand = puremvc.SimpleCommand;import AppMediator from "./AppMediator" ;export default class ViewPrepCommand extends SimpleCommand { public constructor ( ) { super (); } public execute(notification: puremvc.INotification): void { this .facade.registerMediator(new AppMediator(notification.getBody())); } }
NotificationConst.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export enum NotificationConst { START_UP = 'start_up' , SHOW_LOADING = 'show_loading' , LOADING_SUCCESS = 'loading_success' , LOGIN = 'login' , LOGIN_SUCCESS = 'login_success' , LOGIN_FAIL = 'login_fail' , }
Browser.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 export default class Browser { constructor ( ) { if (cc.sys.isNative) { console .log("本地平台" ); if (cc.sys.isMobile) { console .log("本地移动平台" ); if (cc.sys.os == cc.sys.OS_ANDROID) { console .log("本地Android平台" ); Browser.onAndroid = true ; return true ; } else if (cc.sys.os == cc.sys.OS_IOS) { console .log("本地ios平台" ); Browser.onIOS = true ; return false ; } } else { console .log("Web平台" ); return false ; } } else { console .log("未知Web平台" ); return false ; } } public static timeInterval: number ; public static get now(): number { return cc.sys.now() + Browser.timeInterval * 1000 ; } public static get clientWidth(): number { return cc.view.getCanvasSize().width; } public static get clientHeight(): number { return cc.view.getCanvasSize().height; } public static get width(): number { return cc.view.getDesignResolutionSize().width; } public static get height(): number { return Browser.width * (Browser.clientHeight / Browser.clientWidth); } public static get browserType(): string { if (Browser.onAndroid) { return "android" ; } else if (Browser.onIOS) { return "ios" ; } else if (Browser.onMiniGame) { return "wx" ; } else { return "web" ; } } public static get onMiniGame(): boolean { return cc.sys.platform === cc.sys.WECHAT_GAME; } public static onUserKey: boolean = false ; public static onAndroid:boolean = false ; public static onIOS:boolean = false ; }
LoginCommand.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import LoginProxy from "../model/proxy/LoginProxy" ;import SimpleCommand = puremvc.SimpleCommand;export default class LoginCommand extends SimpleCommand { constructor ( ) { super (); } public execute(notification: puremvc.INotification): void { let proxy: LoginProxy = <LoginProxy>this .facade.retrieveProxy(LoginProxy.NAME); proxy.login(notification.getBody()); } }
BaseProxy.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import Proxy = puremvc.Proxy;import IProxy = puremvc.IProxy;import {cccExtensionClass} from "../../../lib/ccc" ;import {Handler} from "../../utils/Handler" ;@cccExtensionClass export default abstract class BaseProxy extends Proxy implements IProxy { public static NAME: string = "BaseProxy" ; constructor (proxyName?: string , data?: any ) { super (proxyName, data); this .proxyName = cc.js.getClassName(this ); } public send(url: string , headers, params, cb: Handler) { cb.runWith({status:"OK" }) } }
LoginProxy.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import BaseProxy from "./BaseProxy" ;import {NotificationConst} from "../../core/NotificationConst" ;import {cccExtensionClass} from "../../../lib/ccc" ;@cccExtensionClass export default class LoginProxy extends BaseProxy { constructor (proxyName?: string , data?: any ) { super (null , data); LoginProxy.NAME = this .proxyName; } public login(data) { this .sendNotification(NotificationConst.LOGIN_SUCCESS); } }
ccc.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 export function cccExtensionClass (target: any ) { let frameInfo = cc['_RF' ].peek(); let script = frameInfo.script; cc.js.setClassName(script, target); }
Handler.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 export class Handler { protected static _pool: Handler[] = []; private static _gid: number = 1 ; caller: Object |null ; method: Function |null ; args: any []|null ; once = false ; protected _id = 0 ; constructor (caller: Object |null =null , method: Function |null =null , args: any []|null = null , once: boolean = false ) { this .setTo(caller, method, args, once); } setTo(caller: any , method: Function |null , args: any []|null , once=false ): Handler { this ._id = Handler._gid++; this .caller = caller; this .method = method; this .args = args; this .once = once; return this ; } run(): any { if (this .method == null ) return null ; let id: number = this ._id; let result: any = this .method.apply(this .caller, this .args); this ._id === id && this .once && this .recover(); return result; } runWith(data: any ): any { if (this .method == null ) return null ; let id: number = this ._id,result: any ; if (data == null ) result = this .method.apply(this .caller, this .args); else if (!this .args && !data.unshift) result = this .method.call(this .caller, data); else if (this .args) result = this .method.apply(this .caller, this .args.concat(data)); else result = this .method.apply(this .caller, data); this ._id === id && this .once && this .recover(); return result; } clear(): Handler { this .caller = null ; this .method = null ; this .args = null ; return this ; } recover(): void { if (this ._id > 0 ) { this ._id = 0 ; Handler._pool.push(this .clear()); } } static create(caller: any , method: Function |null , args: any []|null = null , once: boolean = true ): Handler { if (Handler._pool.length) return (Handler._pool.pop() as Handler).setTo(caller, method, args, once); return new Handler(caller, method, args, once); } }
以上是基本的代码初始化逻辑
基本结构图如下
代码输出:
1 2 3 4 load __quick_compile_project__: 270.85888671875ms Cocos Creator v2.4.0 puremvc框架开始初始化... puremvc框架 初始化完毕
3.构建发布微信小游戏
构建后用微信开发者工具打开查看效果
正常运行!!
小插曲 以前都直接拿bin 文件夹内的文件使用,但是最近随着小游戏的发展,发现微信小游戏用的时候 如果勾选了那个ES6转ES5编译会报错,错误如下:
解决办法:最后经查找删除 puremvc.js代码中 __extends 的定义即可
当然了由于篇幅有限,至于如何查找到这个问题和解决,就是另一个坎坷的故事了,后面有空的时候会把上面提到的build目录的重新编译的故事和操作再讲讲吧。
最后放上 源码项目地址 (点击进入 )
总结 上面就是关于PureMVC 如何在Cocos Creator里面使用,为了简单我省去了很多业务逻辑的干扰,和网络数据的传输逻辑。
后续更新 2020年8月20日
用了框架在微信小游戏内有时候无法获取到我们注册的Mediator和Proxy 这个是JavaScript代码压缩和混淆带来的问题,我们可以利用装饰器解决这个问题,代码如下
新建一个 exdecorators.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 let countDecorator = 0 export function ccExDecorator (target: any ) { let frameInfo = cc['_RF' ].peek(); let script = frameInfo.script; cc.js.setClassName(script, target); countDecorator++ }
用的时候这样 用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import Proxy = puremvc.Proxy;import IProxy = puremvc.IProxy;import {ccExDecorator} from "../../../lib/exdecorators" ;import {RequestData} from "../inter/NetWorkInterface" ;@ccExDecorator export default abstract class BaseProxy extends Proxy implements IProxy { public static NAME: string = "BaseProxy" ; constructor (proxyName?: string , data?: any ) { super (proxyName, data); this .proxyName = cc.js.getClassName(this ); } public abstract send(requestData: RequestData); public abstract failMessage(res: any ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import {ccExDecorator} from "../../../lib/exdecorators" ;import BaseMediator from "../../core/BaseMediator" ;import {NTConst} from "../../core/NTConst" ;import {EventsConst} from "../../events/EventsConst" ;import DaoFuDialog from "./DaoFuDialog" ;@ccExDecorator export default class DaoFuDialogMediator extends BaseMediator { constructor (viewComponent?: any ) { super (null , viewComponent); DaoFuDialogMediator.NAME = this .mediatorName; } public listNotificationInterests(): string [] { return [ NTConst.EXCHANGE_INFO_RESULT ]; } public handleNotification(notification: puremvc.INotification): void { let component: DaoFuDialog; switch (notification.getName()) { case NTConst.EXCHANGE_INFO_RESULT: component = <DaoFuDialog>this .viewComponent.getComponent("DaoFuDialog" ); component.refresh(notification.getBody()); break ; default : } } protected lazyEventListener() { this .viewComponent.on(EventsConst.EXCHANGE_CASH_EVENT, this .exchangeCashHandler, this ); } private exchangeCashHandler(event: cc.Event.EventCustom) { this .sendNotification(NTConst.RED_ENVELOPE_CMD, event.getUserData(), NTConst.EXCHANGE_GOLD_REQUEST); event.stopPropagation(); } }
这样 重新打包运行即可
参考链接