Flash插件编写体验

做游戏开发,少不了工具。以前一说起工具,少不了上面MFC、QT、WPF啥的。做出来的工具好不好用且不说,光是要美术和程序习惯都是个问题。

以前做过一款将SWF转换成程序动画文件的方式。初衷是想无缝连接美术和程序的工作流程。但是由于SWF文件本身比较复杂,而且多数是矢量的图形,在工具里面难处理。所以最终工具没有得到应用。

后来发现DragonBones可以让美术直接在Flash里面制作2D骨骼动画,然后输出JSON和texture atlas。这是一个非常好的做法,美术根本不用熟悉其他工具了,而且直接导出程序使用的数据格式。

DragonBones虽然很不错,但是没有我需要的场景制作的功能。所以我决定自己写一个Flash插件。

准备

Flash插件接口

Flash的插件API使用的javascript接口。

其层次结构简单描述是这样的:

flash(fl)
    ->docuement
        ->timeline(场景)
    ->library
        ->items
            ->timeline
timeline
    ->layer
        ->frame
            ->element

其中,在文档对象下的timeline就是场景,在library下面的timeline就是元件。

有了上面等结构之后,你就可以轻松的看《Extending ADOBE® FLASH® PROFESSIONAL

有用的东西

xJSFL是一个开源的Flash插件快速开发框架。这次我的工具没有太复杂的东西,所以最终只使用了里面的JSON代码。

在Flash CS6以上,提供了SpriteSheetExporter用于将元件打包成texture atlas。所以,如果你需要texture atlas的话,就可以不用自己手写MaxRects之类等2D打包算法了。

陷阱

  • jsfl文件要保存成带BOM带UTF-8,才能在Flash中显示中文。
  • Flash CS5、Flash CS6有bug,无法显示异常的错误信息。
  • Docuement.timelines中的场景可以用index和名字访问,遍历的时候应该使用for (var i = 0; i < doc.timelines.lenght; i++)
  • SpriteSheetExporter输出的JSON文件是UTF16的,你可将exportSpriteSheet()第三个参数设置为false,然后自己将其返回的字符串用FLfile.write()写到文件。

A good textmate theme editor

I have found that the tmtheme-editor which is very usefull.

It can edit tmTheme files online, and dowload result tmTheme files and use in Textmate or Sublime Text.

But it currently lacks of feature that I wanted:

  • A list of the preivew file for different language.
  • Save tmTheme files online (and best under an account).

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 &lt; gameBoard.numberOfRows; i++) {
            for (int j = 0; j &lt; 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更适合现在游戏架构。由于本人水平和经验有限,故只能翻译这么多。这篇博文提到了其它许多没有翻译过的文章,建议大家都读一读。我也不打算翻译了。有兴趣的同学可以翻译出来,为开发者社区贡献一点力量。

Detect if bat file is running via double click or from cmd window

Problem

Some times we need to see the batch file result. It is easy to see the output
if we run the batch file from the cmd window. But to open a cmd windows and cd
to the directory then type the batch file name is very boring. And we'd like to
run the batch file by just one click. So we just add a pause at the end of the
batch file (or just before each exit).

:: normal batch stuffs
bala
bala

:: add the pause to see the result when double click form the explorer.
pause

Now the the double click way works fine. But life is not that easy! Now if we run
it from cmd windows, we needs to press a key to continue. So if we can detect if
bat file is running via double click or from cmd window, then we can make both
case happy.

But how?

Solution

I googled for answers, and I found this stackoverflow question.
The answer says:

%cmdcmdline% gives the exact command line used to start the current Cmd.exe.

  • When launched from a command console, this var is "%SystemRoot%\system32\cmd.exe".
  • When launched from explorer this var is cmd /c ""{full_path_to_the_bat_file}" ";

But how to do the check?

I began with this:

if %cmdcmdline% == "%SystemRoot%\system32\cmd.exe" (
    echo from cmd window...
) else (
    pause
)

But that failed because when launched from explorer %cmdcmdline% have " and tailing space.

After a lot of test, finally I work out it like this:

if "%cmdcmdline:~4,2%"=="/c" (pause)

Clear and simple!

We just check the /c in cmd /c ""{full_path_to_the_bat_file}" " when from dobule click in exploter.
This is valid because when launched from a command windows, %cmdcmdline% is "%SystemRoot%\system32\cmd.exe".
Where the %SystemRoot% is usually C:\Windows. And it can hardly have something like C:\/cndows.

I tested on Windows XP and Windows 7 both works.

SVN Ruby bindings on Windows

Dan has write a post about how to use the SVN Ruby bindings on Windows. Here is how:

  1. Download the ruby bindings (svn-win32-1.y.z_rb.zip) from the svn site
  2. From the zip - copy ruby\lib\svn into c:\ruby\lib\ruby\siteruby\1.8\svn_
  3. From the zip – copy ruby\ext\svn\ext into c:\ruby\lib\ruby\siteruby\1.8\svn\ext_
  4. copy libeay32.dll and ssleay32.dll from your subversion 1.5 directory into c:\ruby\bin
  5. run irb and test with: require ‘svn/core’

10 skills developers will need in the next five years

If you’re a developer looking to get ahead in your field (or in some cases,
to simply stay employed), this is not a good time to be complacent. Justin
James
lists the skills you’ll want to work on now to maximize your future
job prospects.

  1. One of the “Big Three” (.NET, Java, PHP)
  2. Rich Internet Applications (RIAs)
  3. Web development
  4. Web services
  5. Soft skills
  6. One dynamic and/or functional programming language
  7. Agile methodologies
  8. Domain knowledge
  9. Development “hygiene”
  10. Mobile development

Here is the Chinese translation for the original article.

How to convert SVN repository fs type form bdb to fsfs.

To convert bdb repository to fsfs, follow the steps below:

Step 1, stop you svn server to prevent commits.

This is a optional step, if you are sure that there will be no commits during
converting.

Step 2, dump from the bdb repository and load to fsfs repository.

using the follow commands if your are using linux-like system.

svnadmin create --fs-type=fsfs fsfs-repo

svnadmin dump repo | svnadmin load fsfs-repo

mv repo backup-repo

mv fsfs-repo repo

Windows Batch File Programming Make You Mad!

If you are a programmer from C/C++ or other advanced programming language, you might found that the Windows batch file programming is extremely madding.

Here is a list of the perplexing part of the Windows batch file programming that may make you mad.

Comments

You will find that the Windows batch files starts a comment line by "rem".
That is boring! A simple way to start a comment is "::".

:: This is a comment line

:: This is another comment.

Echo a empty line

To echo a empty line, you might think the follow would work.

echo

but it just report echo status. Actually you should use this.

echo.

The if...else...block

The right way to write a if...else...block is.

if exist "a.txt" (

    echo deleting...

    del a.txt

) else (

    echo already done!

)

Note, you must use "(" and ")" here.

How to check if a directory is exist

This code

if exist "path\to\dir" (

    echo Yes

) else (

    echo No

)

will print Yes **when a directory or file named "dir" were exist at path "path\to". So you can use this to check a folder's existence. To do so append a "\NUL**" at the of the path.

if exist "path\to\dir\NUL" (

    echo Yes

) else (

    echo No

)

The string substitute

To substitute a string, you should use

set VAR=%VAR:str1=str2%

This will replace all str1 in %VAR% with str2, and then assign the result to VAR.

The !VAR! doesn't work

The !VAR! (delayed environment variable expansion) usually don't work for you, because the "delayed environment variable expansion" feature is not enabled by default. You will find this is really boring. There is 2 way to enable the "delayed environment variable expansion", so far as I know.

  • using /V:ON switch to invoke the cmd.exe
  • add a DWORD registry key named DelayedExpansion under HKLM\Software\Microsoft\Command Processor (or HKCU), and set the key value to 1.

I prefer the former:

set TEST=any-string

if not "!TEST!"=="%TEST%" (

    %COMSPEC% /V:ON /C %~dpnx0 %*

    goto :eof

)

:: normal process code here...

The if statement is checking if the "delayed environment variable expansion" if work or not. If no, just call the %COMSPEC% (cmd.exe) with /V:ON and execute this batch itself with the same command line arguments.

The multipurpose for command

To be updated!

How to update multi-working-copy one time.

AlanTop talk about "How to update multi-working-copy one time." on his blog. I found that is not the most effective way.

To update1 multi-working-copy one time just:

  • If we use TortoiseSVN, just multi-select the top folders of these working copies2 and the (right click) shell menu -> update (or other commands). That's it.
  • Or, if you use command line, the most powerful effective way.

svn up d:/project01 e:/path/project02 f:/dir/dir2/project03

  1. 1 For commands like commit will not work.
  2. 2 Of course this needs the top folders of working copies are in the same folder due to the windows shell limitation.