Cocos2d里面如何实现MVC

Cocos2d里面如何实现MVC

这里只是将cnblogs上面的文章汇总成一个Markdown文件而已。

第一章 {: #chap1}

前言:

众所周知,现在MVC非常流行。现在只要随便搜索一下,哪里都是MVC的影子。刚开始在j2ee里面,然后是rails,后面居然.net也出来了,ios更不用说,哪里都是mvc,而且强制你必须使用mvc。但是,我们写的那些程序,真正完全符合mvc吗?呵呵,这个不好说,看个人理解程度而异。mvc实在是太火了,马上就有人在cocos2d社区里面讨论,cocos2d该怎么实现mvc呢?大家你一言,我一语,讨论的是热火朝天。有人支持,也有人反对。不管咋样,今天让我们也来见识一下cocos2d里面的mvc,看看到底这玩意儿好使不。

Model-View-Controller (MVC) 在web应用开发中非常流行,它是一种组合设计模式,目前被广泛应用于带有图形交互用户界面程序开发中。一些web开发框架,比如Ruby On Rails,DjangoASP.NET MVC, 它们是不同语言平台上面的web开发框架,但是,它们都共用同样的原则--那就是把用户表示层和逻辑层分离开来。关注点分离(SoC),这个原则在现代软件工程方法中是一个非常重要的设计理念--不要迷失于实现细节,遇到一个实际问题的时候,要划分不同的关注点,且这些关注点必须隔离开来,这样才能达到更好的代码重用度,以获得鲁棒性、可适配性和可维护性。所有这些软件属性对于软件质量来说都是至关重要的。

Cocos2d本身并不是基于mvc的理念来设计的,但是,这并不防碍你在自己的游戏开发中使用mvc。实现方式肯定是多种多样的,在这篇博文中, 我只是向大家分享一下我是怎么在cocos2d里面实现mvc的,同时,在最后,我会写一个简单的游戏demo,当然,里面使用的是cocos2d+mvc。

现有问题

cocos2d里面有这样一些类,CCSprite,CCLayer,CCScene,所有这些,都是CCNode的子类。基本上,大家在使用cocos2d开发游戏的时候,都会采用下面的步骤来实现游戏逻辑:

  1. 通过应用程序代理类来初始化第一个CCScene(即AppDelegate里面的第一个CCScene),
  2. CCScene里面实例化一个或者多个CCLayer,并把它们当作孩子添加进去。
  3. CCLayer 里面实例化一个或者多个CCSprite,也调用addChild添加进去
  4. CCScene 处理用户输入(比如touch事件和加速计的改变),同时更新CCLayer和CCSpirte的属性,比如更改CCSprite的position,让sprite运行一个或多个actioin等。
  5. CCScene里在运行一个游戏循环(game loop,一般是1/60更新一次),然后CCLayer和CCSprite就在这个game loop里面做一些更新和游戏逻辑。

这个过程看起来非常简单,而且也可以很快地做出游戏来。这也是为什么cocos2d这么流行的原因,它实在是太简单了。但是,当你的游戏逻辑越来越复杂的时候,你的代码会变得越来越难以维护。这里面最突出的问题就是,CCScene这个类负责的事情太多了---同时要处理用户交互,还有负责游戏逻辑(逻辑层)和画面显示(表示层)。(译者:根据SoC的原则,这显然是不合理的,我们应该把职责分离开来,这样代码才更容易维护。同时SRP(单一职责原则)也是这么要求的,一个类只负责一件事情)

模型(Model)

MVC它会把一个系统划分为以下几个组件:

  • Model ,它负责与领域相关的逻辑处理代码,也可以说是逻辑层,或者领域层。
  • View ,只负责界面显示。
  • Controller ,它负责处理用户交互。

让我们先从model开始。Model代表了游戏逻辑。因为我现在正在制作一个platform游戏,所以,我讲的一些东西也是与platform游戏相关联的。我的游戏里面的model包含下面一些类(当然,仅仅是一部分类)

  • Player,
    • 包含一些属性,比如:player的位置、当前速度(x轴速度、y轴速度)等。
    • 包含一些与player有关的处理逻辑,比如:run,walk,jmup等。
    • 包含一个update方法,该方法会被游戏主循环每一帧刷新时所调用,它主要负责更新player model。
  • Platform,
    • 包含一些属性,比如:platform位置、宽度、高度等。
    • 包含一些与platform有关的处理逻辑,比如:倾塌等
    • 包含一个update方法,该方法会被游戏主循环每一帧刷新时所调用,它主要负责更新patform的model。
  • GameModel,
    • 包含一些游戏世界的属性,比如重力等。
    • 包含一些方法来执行游戏逻辑。
    • 包含一个update方法,该方法会在每一帧刷新的时候被game loop所调用,然后它就可以更新自己的状态,同时还会触发游戏世界里面的其它对象也相应地更新自己的状态。

你可能会问:有些属性你完全没有必要重复定义,你可以直接从CCSprite里面得到,比如position、width、height等。我想说:有对有错。说对呢,是因为它们确实差不多,可以拿来就用。说不对呢,那是因为,model有可能使用一些不同的计量单位,比如米,而不是像素。(比如box2d这样的,就不是使用像素作为单位)。在我的model里面,我使用的是米,当然,你也可以使用英尺,或者其它单位。渲染引擎对于model来说是透明的,model完全不用关心。

视图(View)

根据mvc的原则,view应该只负责界面显示。它实际上也是在cocos2d里面实现mvc时,最简单的一个。如果你有一个model,你可以使用CCLayer,然后添加一些CCSprite或者其它coocs2d类来处理显示问题。把model和view分开的好处就是,你没必要把model的属性直接映射到view的属性上面去。比如,你的玩家在x轴方向上移动,但是,你想让它总是在距离屏幕左边10px的位置。这时候,你就可以移动CCLayer了,而不是真的在移动sprite。当把model对象显示出来的时候,你必须考虑单位,如果你使用的是米作为计量单位,你在渲染的时候必须转化为像素。(你可以像box2d里面一样,定义一个PTM_RATIO)那么你的model怎么和view打交道呢?你可以从controller里面得到view,或者你可以把game model制作成一个单例,然后使用静态方法来处理它。

控制器(Controller)

controlller负责把view和model联系起来。它的主要职责就是处理用户输入。由于我们需要实例化model和view,我发现在controller里面来做非常合适。我是把controller类继承到CCScene类,然后我们需要建立一个初始的controller类,它由appDelegate来实例化。然而,这里会有一个问题,touch事件是由CCLayer来处理的,而它在我的设计里面的角色是view。而我又不想让view来处理用户输入,所以,我需要传递一个view的引用给controller(不是直接传递,而是通过delegate),然后通过delegate来执行controller的touch事件处理代码,以此来处理view里面的touch事件。好了,现在我的controller类就能够处理来自view的用户事件了。然后,它可以根据用户的输入来操作model,要么通过修改model的属性,或者调用model的方法。再更新完model之后,我们的view也需要得到通知并更新。所有这些,我都在game loop里面完成,实际上它就是一个controller。controller的职责只是负责调用view的update方法,然后剩下的就交给view去完成啦。

还有一件事情…

游戏并不仅仅是根据model状态的更改来更新一下view就可以了,它还需要播放音乐和音效。由于controller负责处理用户交互,它肯定知道何时该播放什么音效。但是,有些时候也会有例外。如果一个player掉到platform上面,但是controller并不知道,因为这部分逻辑判断在model里面。那我们可以从model里面播放音效吗?。。。不,我们不能这样做。因为这样就破坏了SoC的原则了,model就应该只负责游戏逻辑。那么,我们该怎么做呢?在下一篇博文中,我将向大家展示我是怎么做的,我打赌,你肯定差不多也想到呢,对吧?


第二章 {: #chap2}

上一篇博文中,我提到了《如何在cocos2d里面实现mvc》,但是,都是一些纯理论的东西,我们需要看一些代码才能理解地更清楚。这篇博文是基于上一篇来写的,所以我建议你先阅读完上一篇再接着往下看。

模型类

就像之前所讨论的,GameModel类存储了游戏世界里面的一些属性,比如当前的重力。但是,它同时也负责创建和连接游戏里面的对象,比如Player和Platforms。它们之间的关系如下图所示:(译者:这里采用了针对接口编程的方法,所有的游戏对象都继承至updateable接口,这样就可以在game loop里面更新自己了。同时GameModel类提供了一个工厂方法createGameObjects,用来创建游戏里面的对象。)

你可能已经注意到了,所有的model类都实现了updateable protocol,并实现了update方法。这样它们就可以在game loop里面更新自己的状态了。比如,在Player类里面,我们需要根据当前x轴和y轴的速度来更新player的位置信息。在我的游戏里面,我把它委托给Physics组件,它是我实现的一个简单的物理引擎。但是,假如你的游戏很简单的话,你可以不用分开你的物理代码,然后可以直接在update方法里面来做碰撞检测等物理操作。

@implementation Player
- (void)update:(ccTime)dt
{
    [_physics updateModel:self dt:dt];
    // detect collisions with game objects, etc.
}

GameModel实现的update方法,不仅仅用来更新自己的状态,同时,它还调用player的update方法和所有platform的update方法。这个update方法,之后会被game loop所调用。

@implementation GameModel
- (void)update:(ccTime)dt
{
    // modify game model properties here
    // update player
    [self.player update:dt];
    // update platforms
    for (Platform *platform in _platforms) {
        [platform update:dt];
    }
    // ...
}

视图和控制器类

对于我的游戏里面的每一个场景(CCScene),都关联了一个Controller类,它负责处理用户交互、创建视图和管理场景的跳转。控制器会schedule一个游戏主循环,在这个loop里面,所有的model和view的update方法都会被调用。

@implementation GameplayController
- (id)init
{
    if((self=[super init])) {
        GameplayView *view = [[GameplayView alloc] initWithDelegate:self];
    // retain view in controller
    self.view = view;
    // release view
    [view release];
 
    // init model
    GameModel *model = [GameModel sharedModel];
    [model createGameObjects];
    [model.player run];
 
    [self scheduleUpdate];
    }
}
 
- (void)update:(ccTime) dt
{
    GameModel *model = [GameModel sharedModel];
 
    if (model.isGameOver) {
    [[CCDirector sharedDirector] replaceScene:[GameOverController node]];
    }
 
    // process model
    [model update:dt];
 
    // update view
    [self.view update:dt];
}

View主要负责根据model的状态来渲染游戏画面。但是,同时,因为cococs2d的实现方式,我们还需要把touch事件传递给controller类。你应该注意到了,view不并直接依赖controller。view类调用controller的方法是通过GameViewDelegate协议来实现的。这也是为什么我们要在init方法里面传递一个delegate的原因。

@implementation GameplayView
- (id)initWithDelegate:(id)theDelegate
{
    if ((self = [super init])) {
        self.delegate = theDelegate;
 
    // initialize layers
    _backgroundLayer = [GameplayBackgroundLayer node];
    [self.delegate addChild: _backgroundLayer];
 
    _platformLayer = [GameplayPlatformLayer node];
    [self.delegate addChild:_platformLayer];
 
    _playerLayer = [GameplayPlayerLayer node];
    _playerLayer.delegate = theDelegate;
    [self.delegate addChild: _playerLayer];
 
    _hudLayer = [GameplayHudLayer node];
    _hudLayer.delegate = theDelegate;
        [self.delegate addChild:_hudLayer];
    }
 
    return self;
}

// 更新:我忘了告诉大家layer本身是怎么实现的了。其实很简单,就是创建一些sprite、action和animation等。

@implementation GameplayPlayerLayer
- (id)init
{
    if ((self = [super init])) {
        self.isTouchEnabled = YES;
    self.isAccelerometerEnabled = YES;
        ResourceManager *resources = [ResourceManager sharedResourceManager];
 
    [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:PLAYER_SPRITE_SHEET_PLIST];
 
    CCSpriteBatchNode *spriteSheet = [resources playerSpriteSheet];
    [self addChild:spriteSheet];
        // ... 
       // initialize sprites
       // initialize animations
}

层里面的精灵都会在layer的update方法里面被更新,如下所示:

- (void)update:(ccTime)dt 
{
    // update player sprite based on model
    GameModel *model = [GameModel sharedModel];
 
    _playerSprite.position = ccp((model.player.position.x - model.viewPort.rect.origin.x) * PPM_RATIO,  (model.player.position.y - model.viewPort.rect.origin.y) * PPM_RATIO);
}

注意,在渲染player的位置的时候,我们使用了PPM_RATIO,用来把米转换成point。(为什么是point而不是pixel,因为cocos2d使用的是point而不是pixel,不明白的可以看看源代码和官方文档)

touch事件被传递给了controller类,如下所示:

@implementation GameplayPlayerLayer
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.delegate playerBeginJump];
}

然后下图就是view和controller交互的完整的UML图:

处理模型事件

上一篇博文中,我留下了一个问题,就是怎么处理model和controller之间的交互。其它很简单,就是使用观察者模式,controller只要订阅model的事件,然后定义相应的处理方法即可。当model更新的时候,会触发事件,然后所有侦听了该事件的controller都能被通知到。下面给出实现:(译者:很多童靯不知道对象之间该怎么交互,其实使用NSNotification可以大大地解耦对象的交互,使代码更容易维护。)

@implementation Player
- (void)beginJump
{
    if ([_gameModel isOnGround:self]) {
        [[NSNotificationCenter defaultCenter] postNotificationName:EVENT_PLAYER_BEGIN_JUMP object:nil];
    ...
}

controller订阅事件,当事件发生的时候会得到通知,同时相应的事件处理函数将会被调用。

@implementation GameplayController
- (id)init
{
    if ((self = [super init])) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onPlayerBeginJumpNotification:) name:EVENT_PLAYER_BEGIN_JUMP object:nil];
        ...
    }
}
 
- (void)onPlayerBeginJumpNotification:(NSNotification *)notification
{
    [[SimpleAudioEngine sharedEngine] playEffect:PLAYER_JUMP_SOUND];
}

就这么多!

乍一看,可能会觉得有点复杂。而且,要创建这么多类,却只是为了实现一个简单的功能,确实有点划不来。而且,你还记得吗?如果在系统里面添加太多的类,其实是一种反模式(anti-pattern),叫做Fear of Adding Classes。但是,从长远的角度来看,从可维护性的角度来看,加这么多类是值得的。后面的教程我将向大家展示出来。如果大家对于如何在cocos2d里面使用mvc有更好的看法,欢迎补充。


第三章 {: #chap3}

引子:前面两篇文章介绍了一些关于在cocos2d里面如何实现mvc的理论知识,接下来的这三篇教程,我将用一个简单的教程示例,给大家演示一下具体代码实现细节。

这篇文章的写作目的就是让大家更好地理解如何在cocos2d里面实践mvc模式(当然,这里演示的不一定是标准的mvc,因为cocos2d特殊的编程方式。但是,这并不妨碍我们编写更好的代码,你们说对吧?),本文是基于前两篇文章的,所以,在继续阅读之前,我强烈建议你先读一下第一篇的理论介绍。

情景

我们将制作一个简单的面板解谜游戏(board puzzle game),当然,我们不是简单地开发一个游戏,而是要利用mvc开发出一个简单的“游戏框架”,而且这个框架将会在我的新游戏里面使用到,它具有如下一些特性:

  1. 一个n行n列的游戏面板(game board),n可以随着游戏难度进行变化。
  2. 这个游戏面板里面会包含一些“小方块(game pieces)”,而且每一个game board上都只能放一个game piece。
  3. 这个游戏面板可以初始化一些固定的小方块,玩家在游戏过程中,是不能移动这些小方块的。
  4. 这里还定义了一个“工具箱(toolbox)”,它上面可以放置许多小工具(toolbox item),它们可以看作是“可放置可移动小方块的槽子”。
  5. 小工具(或者叫槽子)上面可以放置许多同一类型的小工具。
  6. 这些小工具可以从工具箱上面移动,并且可以放置到game board 上面。

基本概念

来自 wikipedia:

model负责管理应用领域的数据和行为逻辑,同时负责响应对自己的状态数据请求(这些请求通常是从view过来的),然后响应一些指令来更改自身的状态(这些请求通常是来自controller的)。在一个事件驱动的系统中,model会通知订阅者(observers)(通常是views)它的状态改变,这样view就可以做相应的显示更新。

view则根据model的状态来合理地显示,通常是一些UI元素。一个model可以对应多个view,比如,同一数据的柱状图、饼状条、曲线图等。

controller负责接收多用户输入和调用model的一些方法。一个controller通过从用户那里获得输入,然后操作model对象,最后,model通知view来更新显示。

从维基百科的定义中,我们可以识别出以下几个主要的类(我们会在后面把model给加上去):

  1. GameBoardView 代表应用程序的主视图
  2. GameBoardController 是GameBoardView的一个控制器。

请注意,这里的实线代表一种直接的关联关系(controller里面包含一个view的引用),而虚线则代表了一种间接的关联(通过观察者模式)。这里的直接关联后面会用来实现touch事件处理。

实现

项目组织结构

在XCode4里面基于cocos2d的默认模板创建一个新的项目之后,我们又创建了下面这些Groups:

  1. View – views & controller 组 (我们也可以把view和controller放在那个不同的group里面,但是,由于我们两个有直接的关联关系,为了方便,我就把它们放在一些了)
  2. Model – 之后,我们会把model类放在这个group下面。

GameBoardView 的实现

接下来,我们开始实现GameBoardView。首先,我们把GameBoardView继承至CCNode。

@interface GameBoardView : CCNode {
}

然后,实现它的init方法,然后简单地显示一串字符来验证程序的正确性。(译者:这就和我们有时候会在方法的第一句加一个CCLOG一样,只是为了验证函数是否被调用了,确保每一步都是按照你的想法去走的,这样比那种埋头编写2个小时代码不编译,而后花一晚上修改编译错误和bug要好很多。有时候只是输出还不够,还必须要做单元测试,这样才能提高效率)

- (id)init {
    if ((self = [super init])) {
        // create and initialize a Label
    CCLabelTTF *label = [CCLabelTTF labelWithString:@"Hello World from view" fontName:@"Marker Felt" fontSize:48];
 
    // ask director the the window size
    CGSize size = [[CCDirector sharedDirector] winSize];
 
    // position the label on the center of the screen
    label.position =  ccp( size.width /2 , size.height/2 );
 
    // add the label as a child to this Layer
    [self addChild: label];
    }
 
    return self;
}
GameBoardController 实现

GameBoardController负责初始化view,所以它里面包含了一个GameBoardView的引用,将来就可以非常方便地直接使用了。

@interface GameBoardController : CCNode {
    GameBoardView *view;
}

因为我们的GameBoardController继承到CCNode,所以,我们可以把GameBoardView当作GameBoardController的孩子给添加进去。

- (id)init {
    if ((self = [super init])) {
        view = [GameBoardView node];
 
        [self addChild:view];
    }
 
    return self;
}
最后的修改

我们然后修改AppDelegate类,然后运行我们新创建的contorller:

[[CCDirector sharedDirector] runWithScene: [GameBoardController node]];

好了,现在编译并运行。当程序跑起来的时候,这个结果和cocos2d自带的模板运行效果差不多。但是,有个很重要的区别,那就是我们创建了一个mvc的骨架,在接下来的游戏逻辑中,我们可以在上面做很多文章。

接下来做什么

该项目进行到现在,已经为我们引入一些更高级的概念打下了良好的基础,所以,在下一篇教程里,我们将涉及下面两个东西:

  1. 处理touch事件.
  2. 引用model的概念.

第四章 {: #chap4}

在上一篇文章中,我们使用cocos2d基于mvc做了一个简单了游戏架子,这个架子还非常简单,还有许多东西有待实现。

介绍模型

在上一篇博文中,我们介绍了view和controller。为了实现mvc模式,我们还需要添加一个model类来维护游戏的状态。我们的实现应该要包含下列这些类:

  1. GameBoardView - 也就是View,
  2. GameBoardController - 也就是Controller.
  3. GameBoard – 也就是Model.

Model 实现

GameBoard 实现

我们在第一部分所描述的需求是这样子的:

。。。一个game board是通过n行n列组成的,它会随着游戏难度有所变化。

因此,我们按照下面的编码方式来实现之:

@interface GameBoard : NSObject {
    NSInteger numberOfRows;
    NSInteger numberOfColumns;
}
 
- (id)initWithRows:(NSInteger)aNumberOfRows columns:(NSInteger)aNumberOfColumns;
 
@property (nonatomic) NSInteger numberOfRows;
@property (nonatomic) NSInteger numberOfColumns;
 
@end

请注意,model是从NSObject继承过来的---因为model只需要关注game board的状态就行了(当然,还有相应的更新状态的方法)---我们不应该把其它东西也放进来,比如继承到CCNode就不行,我们并不需要CCNode的东西,所以,为了纯粹性,我们这里继承到game board。

GameBoardView 的实现

我们现在需要修改View,同时它包含一个model的引用,我们可以通过initWithGameBoard方法来初始化这个成员变量:

@interface GameBoardView : CCNode {
    GameBoard *gameBoard;
}
 
@property (nonatomic, retain) GameBoard *gameBoard;
 
- (id)initWithGameBoard:(GameBoard *)aGameBoard;
 
@end

具体GameBoardView的实现细节如下:(为了演示方便,我们忽略了实际渲染每一个小方块的代码)

- (id)initWithGameBoard:(GameBoard *)aGameBoard {
    if ((self = [super init])) {
        // retain gameboard
        self.gameBoard = aGameBoard;
 
        // render gameboard background
        CCSprite *gameboardSprite = [CCSprite spriteWithFile:@"gameboard.png"];
        gameboardSprite.anchorPoint = CGPointMake(0, 0);
 
        [self addChild:gameboardSprite];
 
        // render spaces
        for (int i = 0; i < gameBoard.numberOfRows; i++) {
            for (int j = 0; j < gameBoard.numberOfColumns; j++) {
                // position and render game board spaces
            }
        }
    }
 
    return self;
}
GameBoardController
最后,我们要更新GameBoardController的init方法,因为view需要把GameBoard对象通过init方法注入进去,所以,我们在controller的init方法里面,就应该定义好model对象,然后传递给view。
- (id)init {
    if ((self = [super init])) {
        // initialize model
        gameBoard = [[GameBoard alloc] initWithRows:7 columns:9];
 
        // initialize view
        view = [[GameBoardView alloc] initWithGameBoard:gameBoard];
 
        [self addChild:view];
    }
 
    return self;
}

处理touch事件

GameBoardView updates

为了能够处理touch事件,我们需要再稍微修改一下View。我们让它继承至CCLayer,而不是CCNode。因为CCLayer内置了处理touch事件的方法:

@interface GameBoardView : CCLayer {
...
}

而view本身是不应该处理用户的交互(touch事件)的,所以,我们需要定义一个代理(GameBoardViewDelegate)。(译者:为什么这里要定义代理呢?所谓代理代理,当然就是你不想做的事,找别人去做,这就是代理。所以,当你写代码的时候,你想保持类的简单性、重用性,你就可以把事件尽量都交给其它类去做,自己只管做好自己的事。也就是SRP,单一职责原则。如果一个类关注的点过多,做的事情太多。这些事情不管是你直接做的,还是调用别的对象去完成的。这都不行,自己做这些事,那就会使类的功能复杂化,维护不方便。而过多地调用其它对象来完成一些事情,表面上看起来好像不错,实际上是过度耦合了。我们编写类的原则应该是追求高内聚,低耦合的。可能你会说,用代理不也是交给别人做吗?没错,问的好。但是,代理是接口,我们是针对接口编程,所以它的重用性会非常好。因此,下次当你想写可扩展和可重用的代码的时候,不妨先想想代理这个东西吧。objc里面delegate是用protocol实现的,而java和c++则是用接口实现的,具体他们之间怎么转换的,比较一下应该就可以了。)

@protocol GameBoardViewDelegate
- (void)gameBoard:(GameBoard *)gameBoard touchedAtRow:(int)row column:(int)column;
@end

我们还需要再修改一下GameBoardView的init方法,通过传送一个delegate进来处理touch事件。

- (id)initWithGameBoard:(GameBoard *)aGameBoard delegate:(id)aDelegate;

下面是touch事件的具体实现:

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint point = [self convertTouchToNodeSpace:touch];
 
    // calculate row and column touched by the user and call a delegate method
    // ...
    [self.delegate gameBoard:self.gameBoard touchedAtRow:row column:column];
}
GameBoardController 更新

GameBoardController将会负责处理用户touch事件,所以,我们需要让GameBoardController实现GameBoardViewDelegate接口:

@interface GameBoardController : CCNode<GameBoardViewDelegate>
- (void)gameBoard:(GameBoard *)gameBoard touchedAtRow:(int)row column:(int)column {
    // do the game logic here and update view accordingly
}

还有最后一步,那就是修改view的init方法,把controller传递进去。

// initialize view
view = [[GameBoardView alloc] initWithGameBoard:gameBoard delegate:self];

总结

在这篇文章中,我们实现了Model,同时还通过一些代码把view和controller联系起来了。同时,我们还在view和controller,以及用户touch事件之间建立了联系,这样controller类就可以来响应游戏里面的用户输入了。(译者:为什么费这么多劲,无非就是职责分离,这个非常重要!)。在接下来的文章里面,我们会谈到下面两个问题:

  • 在Controller里面更新Model,
  • 通知View关于Model的改变.

第五章 {: #chap5}

本文基于前面两篇文章,如果您还没有看过,建议先阅读下面两篇文章:

更新Model

当用户从工具箱中选一个小工具,然后把它放置到game board上面去时,我们需要编码响应这些事件。在上一篇文章中,我们已经实现了GameBoardViewDelegate的touchedAtRow方法。我们还需要给这个协议再添加一个接口方法。如下所示:

@protocol GameBoardViewDelegate
 
- (void)gameBoard:(GameBoard *)gameBoard touchedAtRow:(int)row column:(int)column;
- (void)gameBoard:(GameBoard *)gameBoard toolboxItemTouchedAtIndex:(int)index;
 
@end

我们需要修改touch事件处理器,这样就可以判断我们到底是触摸了工具箱还是game board。

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint point = [self convertTouchToNodeSpace:touch];
 
    // touched on a game board
    if (CGRectContainsPoint(gameBoardRectangle, point)) {
        int row, column;
        // calculate row and column based on a touch coordinate
        // ...
        // call controller
        [self.delegate gameBoard:self.gameBoard touchedAtRow:row column:column];
    }
    // touched on a toolbox
    else if (CGRectContainsPoint(toolboxRectangle, point)) {
        int index;
        // calculate toolbox item index based on a touch coordinate
        [self.delegate gameBoard:self.gameBoard toolboxItemTouchedAtIndex:index];
    }
}

在controller类里面处理touch事件是非常简单的,我们只需要持有一个model的引用,然后基于touch事件来调用model的方法就行了。我们的接口看起来和下面差不多,只是省略掉了一些实现细节:

@interface GameBoard : NSObject {
// ...
}
 
// ...
- (void)putGamePiece:(GamePiece *)gamePiece row:(int)row column:(int)column;
- (GamePiece *)getGamePieceFromToolboxItemAtIndex:(int)index;
@end

然后,我们在GameBoardController里面完全实现GameBoardViewDelegate的两个方法。

- (void)gameBoard:(GameBoard *)aGameBoard toolboxItemTouchedAtIndex:(int)index {
    // keep the toolbox selection state in the Model
    gameBoard.selectedToolboxItemIndex = index;
}
 
- (void)gameBoard:(GameBoard *)aGameBoard touchedAtRow:(int)row column:(int)column {
    // if the toolbox item is selected move item from toolbox to game board
    if (gameBoard.selectedToolboxItemIndex != -1) {
        GamePiece *gamePiece = [gameBoard getGamePieceFromToolboxItemAtIndex:gameBoard.selectedToolboxItemIndex];
        [gameBoard putGamePiece:gamePiece row:row column:column];
    }
}
到目前为止,我们实现了,用户可以点击工具箱中的小工具,然后把它们放置到game board中的一个小方块上面,同时model类在中间起了桥梁作用。

通知view关于model的改变

为了在view里面反映出model的状态更改,我们可以在model有变化的时候给view发送通知消息,然后view就可以根据不同的消息来作出不同的响应了。和我们在实现view通过controller一样,这里我们也定义了一个GameBoardDelegate,用来通知view model的变化。

@protocol GameBoardDelegate;
@interface GameBoard : NSObject
// ...
@property (nonatomic, assign)
id&lt;GameBoardDelegate&gt; delegate;
// ...
@end
 
@protocol GameBoardDelegate
- (void)gameBoard:(GameBoard *)gameBoard didPutGamePiece:(GamePiece *)gamePiece row:(int)row column:(int)column;
@end
 
@implementation GameBoard
 
- (void)putGamePiece:(GamePiece *)gamePiece row:(int)row column:(int)column {
    // ...
    // store game piece
    // notify that the game piece was put on a gameboard
    [delegate gameBoard:self didPutGamePiece:gamePiece row:row column:column];
}
 
@end

在GameBoardView里面实现GameBoardDelegate的时候,当我们需要在game board上面放置一个小工具的时候,我们定义了一个CCSprite。

@interface GameBoardView : CCLayer
// ...
@end
 
@implementation GameBoardView
 
- (id)initWithGameBoard:(GameBoard *)aGameBoard delegate:(id)aDelegate {
    if ((self = [super init])) {
        // retain gameboard
        self.gameBoard = aGameBoard;
        self.gameBoard.delegate = self;
 
        // assign delegate
        self.delegate = aDelegate;
    }
}
 
- (void)gameBoard:(GameBoard *)gameBoard didPutGamePiece:(GamePiece *)gamePiece row:(int)row column:(int)column {
    // create CCSprite and put it on a game board at corresponding position
    CCSprite *gamePieceSprite = [CCSprite spriteWithFile:fileName];
    // ...
    [self addChild:gamePieceSprite];
}
 
@end

总结

现在框架中所有的部分都联系起来了,model、view和controller三者组成了著名的MVC模式

  • View接收touch事件,然后把事件传递给controller,
  • Controller 响应用户的touch事件,然后更新model
  • model 更新它自身的状态, 处理游戏逻辑,然后告诉view它改变了哪些东西。
  • View则基于Model当前的状态来更新自己的显示

第六章 {: #chap6}

本文将会比较简短。如果你对于前面几篇介绍cocos2d里面如何实现mvc有什么不清楚的地方,请跟我讲,这样我就可以补充地更详细一点。我(原作者)最近接了一些非常大的外包项目,所以,用来写博客的时间不是很多。在此,我简单地总结一下前面写的这些文章:

  1. 我们实现了Model,它可以帮助我们封装游戏的逻辑,这样可以使我们的关注点集中于游戏逻辑,而不是其它的(比如渲染)
  2. 我们实现了Controller来处理用户交互,同时相应地更新model。
  3. 我们实现了View,而它的职责仅仅是负责显示model的状态。

它们三者之间的交互关系如下:

  1. Controller负责初始化Model和View
  2. View负责显示Model
  3. View负责接收用户touch事件,然后传递给controller来处理。
  4. Controller实现view的代理,同时可以操作Model。
  5. model则执行一些游戏逻辑处理并通知view它的状态改变。
  6. View根据model的当前状态来更新它里面的所有对象,比如精灵。

留几个开放性的问题:

  1. 如果你想使用NSUodoManager来实现“撤销”功能,你会把该功能放在哪里呢?Model?Controller?View?
  2. 你会怎么保存当前场景的状态信息呢?
  3. 或者你还可以补充其它问题?

结束语

今天我们讨论的主题是Model-View-Controller (MVC)设计模式,以及如何在cocos2d里面实现它。来自波兰的Bartek Wilczyński写了一系列的文章来介绍这个模式,同时说明了为什么要使用mvc,以及如何在cocos2d里面使用mvc。

这个波兰人写的文章已经被我全部翻译过来了,请点击传送门查看。

当我在读他写的这些文章的时候,我记得Jeremy Flores在github上面有一个cocos2d里面实现mvc的版本库。他把它取名为Cocos2D-MNC,全名是Model-Node-Controller。并且代码是开源的,MIT许可。

这个MVC模式和游戏实体组件系统差不多,我在这篇文章里面就有介绍过了。对于这两个系统来说,它的思想都是统一的,那就是不要继承CCSprite并把游戏logic全部塞到sprite里面去。CCSprite应该只负责渲染显示。而且有时候,你可能需要创建很多sprite,我们最好是创建一个CCNode类,然后里面聚合许多sprites。这样CCNode成为了Controller,控制view。当view(比如sprite,effect,gL drawings等等)在屏幕上面移动的时候,controller结点会轮询所有它包含的结点来查询一些游戏相关的状态信息,并且做一些游戏逻辑,然后反过来再更新view。

对于小游戏来说,mvc模式确实可以运行地很好。它比起直接继承CCSprite,并把一大堆处理逻辑放到CCSprite里面要强多了。如果你发现,你还是不停地继承ccsprite,然后把一大堆处理逻辑塞到一个ccsprite的子类里面,那么你就应该考虑一下mvc设计模式了。

当我们在cocos2d论坛里面提到“是否继承CCSprite还是使用一些model类来构建你的游戏对象结构?”这样的问题的时候,我还是要再强调一点,多用组合,少用继承!如果一味地使用继承,那么当游戏世界里面的对象种类变多,功能变复杂以后,会导致整个继承树“头重脚轻”,严重破坏了良好的面向对象设计原则---我们设计的类层次结构应该是扁平结构的,而不是一个头很大的树。

那么,我们需要使用怎样的架构来处理游戏里面的对象呢?答案就是使用组合。现在已经有一些非常好的引擎,比如TorqueXPushButton Engine、Unity3D等,它们都是基于组合的实体组件系统。

你可以从PushButton的文档里面得到有关实体组件系统的介绍。同时,可以读一读《Components in TorqueX and what the differences are to XNA Game Components》这篇文章来加深对实体组件系统的理解。

你还可以从维基百科上获得更多的信息。实际上,objc语言本身就是被设计为一种可重用的软件组件。

Scott Bilas在2002的GDC大会上提出了一种基于数据驱动的游戏对象系统,同时Dungeon Siege使用了这个新理念,它指出了为什么继承对于游戏开发者来说非常不好,还说明了基于对象组合的组件系统的优点。事实上,在2002年,我开始与SpellForce一起工作的时候,我们已经有一个组件系统了,我们把它叫做Aspects、Abilities和Spells。它可以帮助我们把所有的游戏数据都存储到数据库里面,程序员只需要写一些泛型代码来统一处理这些数据就行了。

在2009年的GDC大会上面,Radical Entertainment’s Marcin Chady也做了一个类似的ppt,大家可以点此查看

Mick West还写了一篇文章,《重构游戏实体为游戏组件》,在这篇文章里面,它很好地描述了,为什么要更改以前的继承模型,转而投向组件系统的怀抱。

还有一些更高级的读物,比如一些paper《Dynamic Game Object Component System for Mutable Behavior Characters》 ,它基于finite state machines讨论了组件系统,而且引入了基于规则的NPC行为系统。

在Game Architect 博客里面把它称之为Anatomy of Despair,并且指出了基于继承的类设计的一些缺点,同时展示了使用组合如何来解决这些问题。

后记:《如何在cocos2d里面实现mvc》这一系统的文章到此就全部结束了,非常感谢大家的耐心阅读。mvc在cocoa开发中被广泛使用,几乎没有哪一个ios开发者不知道mvc。但是,mvc不是银弹,没有银弹!我个人觉得基于组件的实体系统和fsm更适合现在游戏架构。由于本人水平和经验有限,故只能翻译这么多。这篇博文提到了其它许多没有翻译过的文章,建议大家都读一读。我也不打算翻译了。有兴趣的同学可以翻译出来,为开发者社区贡献一点力量。