笔者是从十年前页游时代开始进入游戏行业的,那时候比较流行的框架是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 19 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 (); } }
这样 重新打包运行即可
参考链接