如何设计开发iPhone塔防游戏2-月黑之时

上面谈了下塔防游戏的基本设计机制,现在让我们开始攻城师的正业-编码吧。任何一款塔防游戏的第一步就是创建凶狠强悍的霸天虎。这些霸天虎们要入侵我们美丽的地球家园,所以必须消灭之。那么我们在这里要做点神马呢?既然这仅仅只是游戏开发的开始部分,哥会教你以下几点:

1.设置路点

2.载入平铺地图,并利用地图上的对象,而不是用硬编码的方式来实现。

3.创建凶猛的霸天虎(或者野兽,鬼子,僵尸,随便你怎么称呼)

4.让敌人走两步

5.让地图滚动起来

没有这些基本的东西,恐怕这个游戏离塔防游戏就差得太远了。

好吧,地图上的某些东西只是装饰品,不过我们还是需要它们。最开始哥会教你吗如何让一个基本的敌人沿着平铺地图预定的道路前进,但是要沿着一系列的路点走。到了本系列结束的时候,你就具备足够的经验来制作真正nb的塔防游戏。





为了方便大家的学习,先从这里下载这部分所需要的源代码吧:

Cocos2d iPhone Tower Defense Tutorial Part 1

在维基百科中,对waypoint(路点)的定义是,一系列坐标,可以对一个点在物理空间中的位置进行标识。这个定义我觉得还挺靠谱。它的意思是,我们的舞台就是所谓的物理空间,而路点的位置可以用舞台上的x,y坐标来定义。

接下来我们要在舞台上创建一系列的路点,然后让敌人沿着这些路点一路前行到达终点。听起来很复杂是吗?其实不复杂。为了让开发更方便,我们决定用Tiled Maps这个软件来创建平铺地图http://www.mapeditor.org/,然后用cocos2d来开发游戏http://www.cocos2d-iphone.org/。有了这两个工具,我们的游戏开发过程会乐趣无穷。

好吧,对于如何下载如何安装这些事,哥就不想废话了。关于刚才的源代码,虽然很多部分一看就懂,但是哥还是要大概做一下解释:

1.TowerDefnseTutorialAppDelegate.m

这个文件的作用是创建程序窗口,载入CCDirector导演,并载入游戏的第一个场景。

2.RootViewController.m

扩展UIViewController,以便我们修改设备的朝向。

3.GameConfig.h

当前只有几个关于设备朝向的基本变量

4.TutorialScene.m

游戏的主要实现类,里面会加载地图,并让霸天虎在正确的道路上前进

5.DataModel.m

一个简单的数据接口,里面存储了所需的主要数据。

6.Creep.m

游戏中的敌人,现在我们只加了两种霸天虎,后续会添加更多

7.Waypoint.m

简单的类,主要用于和瓦片地图编辑器对接

8. Wave.m

用来控制敌人出现的顺序。

看上去好像有点吓人,其实1,2,3基本上都是cocos2d默认的,而Waypoint.m和Wave.m的内容现在其实很少,一看就明白。特别是Waypoint,只是CCNode的一个子类,我们需要的信息只是它的x,y坐标而已。

如果你不信,直接看看Waypoint这个类的头文件和实现文件吧。

Waypoint.h:

#import "cocos2d.h"

@interface WayPoint : CCNode {

}

@end

Waypoint.m:

#import "WayPoint.h"

@implementation WayPoint

- (id) init
{
         if ((self = [super init])) {

         }
         return self;
}

@end

好吧,有经验的你一眼可以看出,这个类只是实现了初始化方法而已,没有任何其它的内容。

再看看DataModel这个类,如果你学过Objective-C,一定会知道NSMutableArrays(NS是next step的缩写,Foundation框架中的所有基础对象都以NS开头,NSString, NSArrays…想知道为什么吗?这其实是一个和苹果乔帮主有关的血泪故事,去看看乔布斯传记吧,你会懂的)就是可变数组。让我们看看DataModel.h的头文件代码吧:

#import "cocos2d.h"

@interface DataModel : NSObject  {
         CCLayer *_gameLayer;

    //定义了三个可变数组
         NSMutableArray *_targets;   

         NSMutableArray *_waypoints;     

         NSMutableArray *_waves;    

         UIPanGestureRecognizer *_gestureRecognizer;
}

@property (nonatomic, retain) CCLayer *_gameLayer;

@property (nonatomic, retain) NSMutableArray * _targets;
@property (nonatomic, retain) NSMutableArray * _waypoints;

@property (nonatomic, retain) NSMutableArray * _waves;

@property (nonatomic, retain) UIPanGestureRecognizer *_gestureRecognizer;;
+ (DataModel*)getModel;

@end

我想上面的代码很直白,先是定义了三个可变数组,分别用来存储代表敌人的目标,路点和敌人出现的波次,然后定义了一个UIPanGestureRecognizer,稍微解释下这个动作。这是处理使用者用一只手指(或多只)在屏幕上滑来滑去的动作。要侦测这个动作,只要加下面这段code进viewDidLoad或任何你需要的地方。(关于IOS中的手势识别,请参考这里:http://www.cocoachina.com/iphonedev/sdk/2010/1214/2471.html )

DataModel是一个单例类,且要实现NSCoding协议(关于NSCoding的详细知识,这里不多介绍了,只需知道它是iOS中存储数据的一种方式,请看看Objective-C里面的相关内容,或者看看这篇文章:http://bj007.blog.51cto.com/1701577/411420 )。之所以要创建为单例类,是因为:(1)我们需要用它在后面的教程中跟踪类中的对象,并记录对象的当前状态。(2)在游戏中我们只需要一个DataModel类。通过以下方法,我们可以在任一个类中访问DataModel类:

DataModel *m = [DataModel getModel];

下面是DataModel.m中单例的实现部分,:

static DataModel *_sharedContext = nil;

+(DataModel*)getModel
{
         if (!_sharedContext) {
              _sharedContext = [[self alloc] init];
         }

         return _sharedContext;
}

其它如encodeWithCoder方法和initWithCoder方法在这里并没有实现。Init和dealloc中的方法也是很明显的。

同时我们还需要记录所有的游戏参与者-targets是我们的敌人,waypoints是敌人要经过的路点,而waves则存储敌人的数量和出现速度等信息。

那么UIPanGestureRecognizer和CCLayer在这儿是干嘛用的呢?CCLayer是我们所定义的真正游戏层,你和敌人的真正战斗将发生在这里,因此我们务必要随时追踪这个层,以便其它类都可以访问。UIPanGestureRecognizer可以让你滚动地图,而不是让地图限制在区区480*320这么小的范围内,当然,关于这一点要在后面慢慢来实现。

现在你再来看这些代码,是不是没那么恐怖了?好了,该研究下我们的敌人了。让我们看看Creep这个类具体的代码吧,先来看看Creep.h这个头文件:

#import "cocos2d.h"

#import "DataModel.h"
#import "WayPoint.h"

@interface Creep : CCSprite  {
    int _curHp;
         int _moveDuration;

         int _curWaypoint;
}

@property (nonatomic, assign) int hp;
@property (nonatomic, assign) int moveDuration;

@property (nonatomic, assign) int curWaypoint;

- (Creep *) initWithCreep:(Creep *) copyFrom;
- (WayPoint *)getCurrentWaypoint;
- (WayPoint *)getNextWaypoint;

@end

@interface FastRedCreep : Creep {
}
+(id)creep;
@end

@interface StrongGreenCreep : Creep {
}
+(id)creep;
@end

在这个头文件中,我们定义了敌人的基本信息变量,如血条,移动速度和当前所在的路点。到目前为止,我们只需要这些信息。然后我们创建了两类敌人,因为塔防游戏里面的敌人永远有很多种。一种敌人是红色飞毛腿,但血条少,另一种敌人是传说中的坦克-很慢,但是血条长。当然,你还可以添加更多变态的敌人,不过为了让例子更简单一点,我们只选了这两种敌人。

好吧,上面的代码总体来说就是这个意思,但如果你对IOS开发的基础不是很牢固,你可能在郁闷,NSCopying这个协议是搞神马的?简单来说,这个协议是用来复制对象的,但更详细的呢?你需要在这里补充一下基础知识了,好吧,哥也承认这些东西还是有点难度的。但作为攻城师,还是啃下这块硬骨头吧,对你有好处的。http://www.apple.com.cn/developer/iphone/library/documentation/UserExperience/Conceptual/MemoryMgmt/Articles/mmImplementCopy.html

顺便说一下,这个内存管理编程指南中的其它知识也有必要了解一下,特别是经常开发ios应用的。虽然,的确不简单。

既然头文件理解了,哥是不是该把实现文件也详细解释一下?不过为了简化起见,还是只把相关的部分提一下吧。首先看看我们的飞毛腿敌人是如何实现的:

@implementation FastRedCreep

+ (id)creep {

    FastRedCreep *creep = nil;
    if ((creep = [[[super alloc] initWithFile:@"Enemy1.png"] autorelease])) {
        creep.hp = 10;
        creep.moveDuration = 4;
              creep.curWaypoint = 0;
    }
    return creep;
}

@end

是的,代码很明显,我们用Enemy1.png这个图片来初始化了一个敌人(精灵),并设定它的血量为10,速度为4,当前所在路点为1。只要我们调用[FastRedCreep creep]这个类方法,我们就会在屏幕上得到飞毛腿。因为Creep这个类是从CCSprite类继承来的,所以它具备该类的所有特性。当然,如果为了更简单起见,我们甚至无需单独定义这样一个类,而直接生成一个CCSprite对象,但对于面向对象的编程来说,是不合适的,同时也不利于游戏的扩展。

再来看看Creep这个类的另外两个方法的实现:

- (WayPoint *)getCurrentWaypoint{

         DataModel *m = [DataModel getModel];

         WayPoint *waypoint = (WayPoint *) [m._waypoints objectAtIndex:self.curWaypoint];

         return waypoint;
}

- (WayPoint *)getNextWaypoint{

         DataModel *m = [DataModel getModel];
         int lastWaypoint = m._waypoints.count;

         self.curWaypoint++;

         if (self.curWaypoint > lastWaypoint)
              self.curWaypoint = lastWaypoint - 1;

         WayPoint *waypoint = (WayPoint *) [m._waypoints objectAtIndex:self.curWaypoint];

         return waypoint;
}

这里我们充分利用了DataModel类和WayPoint类,给我们的敌人一些具体的行为。注意curWaypoint的初始值是0,也就是数组的起点。

我们获取了敌人的当前路点位置和下一个路点的位置,从而让他们沿着路径走向终点。到现在为止,当达到路点数组的终点时,我们会把终点的值传给curWaypoint(也就是说不会再动了)。你可以尝试把getNextWaypoint里面的”self.curWaypoint = lastWaypoint -1”改成”self.curWaypoint =0”,看看会发生什么。是的,如果防御炮塔没有击中飞毛腿和坦克,它们会无休止的重复波次攻击。

好吧,上面的代码要求敌人沿着路点走向终点,那么下面就是在TutorialScene这个类里面具体实现走的动作了。

具体见TutorialScene.m中的FollowPath方法:

-(void)FollowPath:(id)sender {

         Creep *creep = (Creep *)sender;

         WayPoint * waypoint = [creep getNextWaypoint];

         int moveDuration = creep.moveDuration;
         id actionMove = [CCMoveTo actionWithDuration:moveDuration position:waypoint.position];
         id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(FollowPath:)];
         [creep stopAllActions];
         [creep runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];
}

如果你学习过cocos2d的基础知识,就不难看懂上面的代码,但是如果你不是很熟悉,那么会有点小麻烦,所以,先去打好基础吧。

当我们通过AddTarget这个方法添加了敌人后,可以通过调用这个方法让敌人前行。它会检查”sender”参数(实际上就是它自己),并获取下一个路点。既然敌人处在当前的路点上,很显然我们需要让curWaypoint+1,从而获取下一个路点的位置。接下来我们会让敌人执行两个动作。首先是MoveTo动作,让精灵从当前的x,y坐标位置移动到下一个x,y坐标位置。然后我们再次调用FollowPath,并执行这个循环。

好吧,既然TutorailScene这个类如此重要,还是让我们来解释下里面的代码。

#import "cocos2d.h"

#import "Creep.h"
#import "WayPoint.h"
#import "Wave.h"

// Tutorial Layer
@interface Tutorial : CCLayer
{
    CCTMXTiledMap *_tileMap;
    CCTMXLayer *_background;  

         int _currentLevel;
}

@property (nonatomic, retain) CCTMXTiledMap *tileMap;
@property (nonatomic, retain) CCTMXLayer *background;

@property (nonatomic, assign) int currentLevel;

+ (id) scene;
- (void)addWaypoint;

@end

好吧,麻烦又来了,如果你之前没有用过传说中的瓦片地图,哥建议你还是要先学学这篇教程:Ray Wenderlich’s – Collision and Collectables Tile Based Tutorial

先来看看初始化方法吧;

// on "init" you need to initialize your instance
-(id) init {
    if((self = [super init])) {
              self.tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"TileMap.tmx"];
        self.background = [_tileMap layerNamed:@"Background"];
              self.background.anchorPoint = ccp(0, 0);
              [self addChild:_tileMap z:0];

              [self addWaypoint];
              [self addWaves];

              // Call game logic about every second
        [self schedule:@selector(update:)];
              [self schedule:@selector(gameLogic:) interval:1.0];         

              self.currentLevel = 0;

              self.position = ccp(-228, -122);

    }
    return self;
}

首先我们载入了一个自己创建的”CCTMXTiledMap”地图,并通过四个步骤把它添加到游戏层。我们调用了addWaypoint方法,给敌人添加了路点(下面有解释)。然后调用了addWaves方法,这个方法现在只是创建和存储两个默认的进攻波次。

然后我们使用游戏的定时机制和游戏逻辑来实时更新游戏状态。最后我们设定当前的关卡为0,同时让游戏层显示在屏幕之中,获得一个最佳的视角。

好吧,现在我们需要对代码做进一步的分析了。现在让我们打开资源文件夹中的.tmx文件。别忘了从这里下载地图编辑器mapeditor.org



好吧,让我们开始。你从上图中可以看到,我们在瓦片地图(或者也叫平铺地图)上放置了一些对象。以上是敌人要经过的基本路径。由于瓦片地图没法很好的显示只有1个像素的对象,所以我们可以看看TileMap.tmx文件中的这些对象究竟是神马样子(直接在xcode里面看吧,请无视图中data 标签里面的内容):



 

这些对象神奇吗?不!简直可以说太枯燥了,也就是0-7共8个路点和它们的坐标值而已。那么好吧,让我们使用addWaypoint这个方法来添加这些路点吧。

-(void)addWaypoint {
         DataModel *m = [DataModel getModel];

         CCTMXObjectGroup *objects = [self.tileMap objectGroupNamed:@"Objects"];
         WayPoint *wp = nil;

         int spawnPointCounter = 0;
         NSMutableDictionary *spawnPoint;
        while ((spawnPoint = [objects objectNamed:[NSString stringWithFormat:@"Waypoint%d", spawnPointCounter]])) {
              int x = [[spawnPoint valueForKey:@"x"] intValue];
              int y = [[spawnPoint valueForKey:@"y"] intValue];

              wp = [WayPoint node];
              wp.position = ccp(x, y);
              [m._waypoints addObject:wp];
              spawnPointCounter++;
         }

         NSAssert([m._waypoints count] > 0, @"Waypoint objects missing");
         wp = nil;
}

好吧,上面的代码是什么意思?我们使用while循环遍历了tmx文件中的对象,并获得了所有需要的8个路点。然后我们创建了wayPoint类的实例变量,并设置了每个路点的值,然后将其存储在DataModel类中以便查找。

路点有了,用addTarget这个方法让那些敌人也登场吧:

-(void)addTarget {

         DataModel *m = [DataModel getModel];
         Wave * wave = [self getCurrentWave];
         if (wave.totalCreeps < 0) {
              return; //[self getNextWave];
         }

         wave.totalCreeps--;

    Creep *target = nil;
    if ((arc4random() % 2) == 0) {
        target = [FastRedCreep creep];
    } else {
        target = [StrongGreenCreep creep];
    }

         WayPoint *waypoint = [target getCurrentWaypoint ];
         target.position = waypoint.position;
         waypoint = [target getNextWaypoint ];

         [self addChild:target z:1];

         int moveDuration = target.moveDuration;
         id actionMove = [CCMoveTo actionWithDuration:moveDuration position:waypoint.position];
         id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(FollowPath:)];
         [target runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];

         // Add to targets array
         target.tag = 1;
         [m._targets addObject:target];

}

当我们调用addTarget方法的时候,我们会获取当前的敌人波次信息,并决定放出哪种敌人。为了方便起见,这里我先保留波次信息,并随机创建了飞毛腿或坦克两种敌人。然后根基第一个路点设置敌人的当前位置(你该记得,Creep类中的curWaypoint变量的初始值是0,那么它将从tmx文件中获取Waypoint0的位置信息。然后我们把这个精灵添加到当前的游戏层,并指定一个动作,让它移动到下一个路点(和FollowPath中的代码是一样的)。最后我们设置了目标的标示tag为1(代表敌人),并把它添加到DataModel中,便于后续的数据检索。

现在addTarget方法也有了,那么谁来调用它呢。别急,我们有游戏的定时机制。

-(void)gameLogic:(ccTime)dt {

         DataModel *m = [DataModel getModel];
         Wave * wave = [self getCurrentWave];
         static double lastTimeTargetAdded = 0;
    double now = [[NSDate date] timeIntervalSince1970];
   if(lastTimeTargetAdded == 0 || now - lastTimeTargetAdded >= wave.spawnRate) {
        [self addTarget];
        lastTimeTargetAdded = now;
    }

}

- (void)update:(ccTime)dt {

}

gamelogic方法用于判断何时添加新的敌人,当然,要根据当前波次的spawnRate(刷屏率)和简单的时间逻辑来判断。关于spawnRate,请参考Wave.h和Wave.m

虽然Update方法现在的内容是空的,不过在后面会变得非常重要,这里先要摆出来备用。

现在,我们已经有了一款塔防游戏的雏形,虽然很多代码并没有做最详细的解释。不过还有一点要终点谈一下,UIPanGestureRecognizer。

我曾想过只用480*320的小地图来写这篇教程,不过后来想了想,这个实在不太靠谱,这么小的地图咋玩呀?玩家还不得骂死我们攻城师?虽然附带的tmx地图不大,不过你完全可以自己用地图编辑器弄一个更大的地图,而且我们的示例代码可以让你滚动到地图的任何位置。实现这个功能的代码如下:

- (CGPoint)boundLayerPos:(CGPoint)newPos {
    CGSize winSize = [CCDirector sharedDirector].winSize;
    CGPoint retval = newPos;
    retval.x = MIN(retval.x, 0);
    retval.x = MAX(retval.x, -_tileMap.contentSize.width+winSize.width);
    retval.y = MIN(0, retval.y);
    retval.y = MAX(-_tileMap.contentSize.height+winSize.height, retval.y);
    return retval;
}

使用boundLayerPos方法,可以避免游戏层移动出自己的边界。这样即便我们的瓦片地图尺寸从27*20扩展到50*50,仍然可以很好的实现。不过这样的话你就得修改路点,否则那些霸天虎会如神兵天降,突然从屏幕的某个位置就冒了出来。好吧,别着急,我们帮你搞定了这些:

- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer {

    if (recognizer.state == UIGestureRecognizerStateBegan) {   

        CGPoint touchLocation = [recognizer locationInView:recognizer.view];
        touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
        touchLocation = [self convertToNodeSpace:touchLocation];               

    } else if (recognizer.state == UIGestureRecognizerStateChanged) {   

        CGPoint translation = [recognizer translationInView:recognizer.view];
        translation = ccp(translation.x, -translation.y);
        CGPoint newPos = ccpAdd(self.position, translation);
        self.position = [self boundLayerPos:newPos];
        [recognizer setTranslation:CGPointZero inView:recognizer.view];   

    } else if (recognizer.state == UIGestureRecognizerStateEnded) {

              float scrollDuration = 0.2;
              CGPoint velocity = [recognizer velocityInView:recognizer.view];
              CGPoint newPos = ccpAdd(self.position, ccpMult(ccp(velocity.x, velocity.y * -1), scrollDuration));
              newPos = [self boundLayerPos:newPos];

              [self stopAllActions];
              CCMoveTo *moveTo = [CCMoveTo actionWithDuration:scrollDuration position:newPos];
              [self runAction:[CCEaseOut actionWithAction:moveTo rate:1]];           

    }
}

这段代码的作用让我们可以清楚的知道滑动(swipe)动作开始和结束的地点,以及滑动的速度。如果我们不用boundLayerPos这个方法,很可能你食指一动,游戏层就飞出了屏幕之外。目前我们还不需要使用UIGestureRecognizerStateBegan状态,但是我们需要使用UIGestureRecognizerStateChanged来判断边界,以及手势的改编(方向改变)。最后再使用UIGestureRecognizerStateEnded,根据手指的滑动速度,来让游戏层平滑的在屏幕上滚动。

小结:

或许你对示例代码还有很多头疼的地方,多半是因为iOS开发的基础知识,以及cocos2d的基础知识还有待加强。当然,也是因为对某些细节没有做过于详细的解释。不管怎样,一款真正的塔防游戏的框架搭起来了。我们到现在已经学会了:

1.如何设置路点

2.如何载入瓦片地图(平铺地图),并使用其中的对象,而不是使用硬编码的方式来实现这些对象。

3.如何创建敌人

4.怎样让敌人沿着我们预设的路径千金

5.怎样让游戏场景平滑的在iphone屏幕上滚动。

怎么样?是不是已经有些感觉了?

在后续的学习中我们还需要解决以下问题:

1.怎样让敌人平滑的度过弯道?

2.当我们达到路点的终点时会发生些什么?

标签: cocos2d教程, 塔防游戏教程

?>