MVC框架其实更多是在web前端使用的,一个web页面就是一个view嘛,然后可以很好地将数据、界面、控制分成MVC三层。但是游戏开发中会有点不一样,就我的感觉来说,我觉得PureMVC更适合休闲益智类的游戏,因为这类游戏就几个页面,页面中的元素也比较简单,我在网上搜索发现,有人开发棋牌游戏就用过这个框架的。而RPG类的游戏可能就不太适用这个框架了,像游戏场景中的角色的行为,就无法很好的划分为MVC三层。当然,上面所说的都是我学习了解他人总结的基础上得出的自己的总结,最佳体会还是来自于实践。每个框架都不是万能的,学习这个框架,可能有用,也可能没用,并不是学了之后做什么都要生搬硬套上去。下面举的例子,是我用ooc-lang写的,模拟了一个很简单的场景,仅供演示用。
由于ooc-lang并不是某游戏引擎绑定的用来开发游戏的语言,所以无法写几行简单的代码就可以展示出来一个界面,虽然它可以用SDL这个库来写界面,但是太过底层,需写的代码较多,因此我这个例子就没有界面演示了,只是在控制台输出打印下日志。
看代码前,先讲下整个代码的结构和这个程序的执行过程,这样看代码的时候才知道怎么看。用PureMVC框架写代码的时候,只需自己继承并实现一个自己的Facade类就行了,该Facade会初始化好MVC三层,一般这三层也不需要我们自己再实现了,然后我们就可以实现各种需要的中介、命令、代理类了,在然后就在需要的地方注册这些类或者发送相关通知。界面元素的引用一般放在单独的一个类里,常量什么的放在一个配置文件里。最后,在程序启动的时候,调用一下该Facade的初始化命令就行了。给个截图看下我这个例子的代码结构:
你可以简单想象一下,就是程序启动后会有一个界面,这个界面就是菜单界面,界面显示有草鞋、草帽的数量和价格。程序启动后会立马发送一个更新菜单的通知,更新菜单后会立马发送一个打开菜单的通知,之后就会显示价格和数量了。整个流程就是这样,但实际上是没有界面的。好了,接下来就看代码了,不多。
先从程序入口开始:
/*
* Main.ooc
*/
// 这句是为了描述PureMVC框架源码的详情的,没有它编译的时候会找不到对应文件的源码
use PureMVC
// 引入模块,这个是我们自己实现的外观类
import AppFacade
Main: class {
init: func
start: func {
// 程序启动后,调用外观类的启动方法
// 这里就包括了外观类的初始化和MVC三层的初始化,以及startUp()中的启动逻辑。具体看外观类
AppFacade getInstance() startUp()
}
}
// 程序入口,就像C中的main()一样。
main: func {
Main new() start()
}
全局唯一的一个外观类:
/*
* AppFacade.ooc
*/
// 引入相关模块
import patterns/facade/Facade // PureMVC框架中的Facade类,我们需要继承它
import controller/StartupCommand // 自己实现的启动命令类
import controller/ShowPriceCommand // 自己实现的显示价格的命令类
AppFacade: class extends Facade {
// 字符常量,用于映射对应的命令和发送通知时用
// 如果通知多的话,一般是独立放在一个模块里,而不会全集中到这个外观类里
startup := static "startup" // 启动命令
updateMenuView := static "update_menu_view" // 更新菜单命令
openMenu := static "open_menu" // 打开菜单命令
showPrice := static "show_price" // 显示价格命令
// 类的构造函数,ooc-lang中,子类需要调用父类的构造函数的话,只需前面加个super就行了
init: super func
// 重新定义一个和父类一样的获取单例的方法,注意静态方法不能被重写,所以这里是重新定义
getInstance: static func -> AppFacade {
if (This instance == null) {
This instance = AppFacade new()
}
return This instance as AppFacade
}
// 重写父类中初始化Controller层的方法,因为我们需要额外做其它事
initializeController: func {
// 除了在func前加super,函数体内也可以通过调用super()来调用父类中的同一个方法
super()
// 注册启动命令
registerCommand(This startup, StartupCommand new())
// 注册显示价格的命令
registerCommand(This showPrice, ShowPriceCommand new())
/* 那些命令注册好后就会有一个观察者监听着,一旦收到通知,就立马执行这些命令 */
}
// 程序入口中调用到的启动方法
startUp: func {
// 启动后发送一个启动命令
sendNotification(This startup)
}
}
一个简单的外观类就实现了,其中我们重写了initializeController()
这个方法,另外两个initializeModel()
和InitializeView()
是直接用默认的就行了,因为我们不需要实现额外的逻辑。然后还额外实现了一个startUp()
方法,一般都需要一个方法来初始化整个框架的。
接下来看controller文件夹下的模块了,前面的文章说过,Controller层并不需要自己实现一个Controller类,我们只需实现一个个的命令就行了:
/*
* StartupCommand.ooc
* 启动命令
*/
// 引入相关模块
import patterns/command/SimpleCommand
import patterns/observer/Notification
import view/components/MenuView // 菜单类,存放界面元素的引用
import view/MenuViewMediator // 菜单界面的中介,用来操作菜单界面
import model/vo/Inventory // 库存类,存放数据
import model/InventoryProxy // 库存代理,通过代理来访问库存数据
// 启动命令,继承自SimpleCommand,因为这里只需执行一条命令就行了
StartupCommand: class extends SimpleCommand {
init: func
// 重写方法,写上具体需要执行的逻辑
execute: func(notification: Notification) {
// 打印跟踪
"[ StartupCommand ] execute()" println()
// 启动之后,注册菜单界面中介和库存代理
// 一般字符常量放在一个独立的模块中,这里我就懒得写多一个文件,就直接写这里了
facade registerMediator(MenuViewMediator new("MenuViewMediator", MenuView new()))
facade registerProxy(InventoryProxy new("InventoryProxy", Inventory new()))
}
}
/*
* ShowPrice.ooc
* 显示价格的命令
*/
// 引入相关模块
import patterns/command/MacroCommand
import SellShoesCommand, SellHatCommand
// 为了演示MacroCommand命令,我这里将显示价格命令分开来了,分别为卖草鞋的命令和卖草帽的命令
ShowPriceCommand: class extends MacroCommand {
// 子类构造函数需要调用一下父类的构造函数
init: super func
// 重写该方法,其实就是往里头添加子命令就行了
initializeMacroCommand: func {
addSubCommand(SellShoesCommand new())
addSubCommand(SellHatCommand new())
}
}
/*
* SellShoesCommand.ooc
* 卖草鞋的命令
*/
import patterns/command/SimpleCommand
import patterns/observer/Notification
// 继承SimpleCommand就行了
SellShoesCommand: class extends SimpleCommand {
init: func
// 重写方法
execute: func(notification: Notification) {
// 简单输出打印一下价格就行了,如果有界面的话,可以在这里更新界面上的价格
// 一般数据是从代理中获取的,但是为了偷懒,就没写价格代理了,直接引用数据就行了
"[ SellShoesCommand ] 草鞋3元一双,10元三双" println()
}
}
/*
* SellHatCommand.ooc
* 卖草帽的命令
*/
import patterns/command/SimpleCommand
import patterns/observer/Notification
// 同卖草鞋的命令一样
SellHatCommand: class extends SimpleCommand {
init: func
execute: func(notification: Notification) {
"[ SellHatCommand ] 草帽5元一顶" println()
}
}
controller文件夹下的模块讲完了就讲model文件夹下的模块了,Model层也不用我们实现一个Model类,只需实现真实的数据类和数据的代理类就行了:
先看vo文件夹下的,这个文件夹一般是放实际的数据类,我也不清楚vo的全称是什么,但我看官方例子都是将实际的数据类放这里。
/*
* Inventory.ooc
* 库存数据类
*/
// 数据可能是从远程获取到的,也可能是启动后就初始化了的,这里是直接初始化了。
Inventory: class {
numShoes := 10 // 草鞋数量
numHats := 8 // 草帽数量
init: func
}
/*
* InventoryProxy.ooc
* 库存数据的代理,通过代理访问数据,而不是直接访问
*/
import patterns/proxy/Proxy
import AppFacade
import vo/Inventory
// 我看官方例子中,有直接将数据写代理类里的,也有将数据独立成一个类的
// 可能简单的数据就直接放代理类里头了吧,复杂的就独立成一个类
InventoryProxy: class extends Proxy {
// 库存数据的引用,避免每次都通过函数来获取
inventory: Inventory
// 构造是需要设置该代理的名称和数据
init: func(proxyName: String, data: Pointer = null) {
super(proxyName, data)
}
// 重写该方法,注册代理时调用
onRegister: func {
// 获取数据
inventory = getData() as Inventory
// 注册库存代理时会获取到库存信息,然后发送一条更新菜单界面的通知
sendNotification(AppFacade updateMenuView, inventory)
}
}
最后就是view文件夹下的模块了,components文件夹都是用来放界面元素的引用的,通过中介来管理和更新这些元素:
/*
* MenuView.ooc
* 菜单界面(CocosCreator里不是有很多脚本吗,那些脚本中有声明场景中的各个对象,你可以将此模块想象成类似的脚本)
*/
import AppFacade
MenuView: class {
// 假设是界面中需显示的文本,这里我们只是将它打印出来,并不是显示到界面上
numShoes: Int = 0
numHats: Int = 0
init: func
// 界面的更新方法,更新界面的时候调用
update: func(numShoes, numHats: Int) {
this numShoes = numShoes
this numHats = numHats
// 输出打印
"[ MenuView ] 草鞋数量:#{this numShoes}" println()
"[ MenuView ] 草帽数量:#{this numHats}" println()
// 更新库存后发送一条打开菜单的通知
AppFacade getInstance() sendNotification(AppFacade openMenu)
}
}
/*
* MenuViewMediator.ooc
* 菜单界面中介,管理和更新菜单界面
*/
import patterns/mediator/Mediator
import patterns/observer/Notification
import AppFacade
import components/MenuView
import model/vo/Inventory
MenuViewMediator: class extends Mediator {
// 界面类的引用,避免每次都通过函数来获取
menuView: MenuView
// 构造一个中介时需要设置中介的名称和界面元素
init: func(mediatorName: String, viewComponent: Pointer = null) {
super(mediatorName, viewComponent)
}
// 列出该中介感兴趣的通知,注册中介时会注册观察者来监听这些通知
listNotificationInterests: func -> String[] {
[AppFacade updateMenuView, AppFacade openMenu] as String[]
}
// 收到通知时需要执行的逻辑,一般界面的更新就是在这里了
handleNotification: func(notification: Notification) {
// 获取通知名称
match(notification getName()) {
// 如果是更新菜单界面的通知
case AppFacade updateMenuView =>
// 获取发通知时携带的消息体,在这里是库存数据,可以看发通知的地方,会附带该参数的
body := notification getBody() as Inventory
// 如果菜单界面不为空
if (menuView != null) {
// 更新菜单界面的库存数据
menuView update(body numShoes, body numHats)
}
// 如果是打开菜单界面的通知
case AppFacade openMenu =>
// 打开了菜单界面,当然是发送一条显示价格的通知来显示价格了
sendNotification(AppFacade showPrice)
// 缺省情况下的处理
case =>
super(notification)
}
}
// 注册中介时调用
onRegister: func {
if (getViewComponent() != null) {
// 注册中介时会设置viewComponent嘛,我们保存一个引用,避免每次都调用函数来获取
menuView = getViewComponent() as MenuView
}
}
}
例子的所有代码都贴出来了,编译运行的结构看截图:
看日志就知道程序是怎么执行了的吧,我把框架的初始化过程也打印出来了。用ooc-lang实现一遍PureMVC框架并写个简单例子的系列就完结了。收获不能说没有,但是大不大就不清楚了。