cocos2d裏面如何實現MVC

 

 

前言:

    衆所周知,現在MVC非常流行。現在只要隨便搜索一下,哪裏都是MVC的影子。剛開始在j2ee裏面,然後是rails,後面居然.net也出來了,ios更不用說,哪裏都是mvc,而且強制你必須使用mvc。但是,我們寫的那些程序,真正完全符合mvc嗎?呵呵,這個不好說,看個人理解程度而異。mvc實在是太火了,馬上就有人在cocos2d社區裏面討論,cocos2d該怎麼實現mvc呢?大家你一言,我一語,討論的是熱火朝天。有人支持,也有人反對。不管咋樣,今天讓我們也來見識一下cocos2d裏面的mvc,看看到底這玩意兒好使不。

 

    Model-View-Controller (MVC) 在web應用開發中非常流行,它是一種組合設計模式,目前被廣泛應用於帶有圖形交互用戶界面程序開發中。一些web開發框架,比如Ruby On Rails,Django 和 ASP.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就應該只負責遊戲邏輯。那麼,我們該怎麼做呢?在下一篇博文中,我將向大家展示我是怎麼做的,我打賭,你肯定差不多也想到呢,對吧?

模型類

    就像之前所討論的,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有更好的看法,歡迎補充。

這篇文章的寫作目的就是讓大家更好地理解如何在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的概念.

介紹模型

    在上一篇博文中,我們介紹了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的改變.

更新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當前的狀態來更新自己的顯示 

我們討論的主題是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更適合現在遊戲架構。由於本人水平和經驗有限,故只能翻譯這麼多。這篇博文提到了其它許多沒有翻譯過的文章,建議大家都讀一讀。我也不打算翻譯了。有興趣的同學可以翻譯出來,爲開發者社區貢獻一點力量。

 

   全劇終!




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章