如何设计开发iphone塔防游戏3-炮塔就位

本文由eseedo(泰然骷髅会成员)翻译,泰然授权转载,转载请通知eseedo(http://blog.sina.com.cn/eseedo)。(-by Iven)

欢迎继续我们的塔防游戏学习之旅,今天我们要学习另一块重要的内容-搭建工事,安放炮塔。好吧,我向大家保证,下一章我们会把之前的东西整合在一起。如果之前的两部分还没有看过,最好先去看看吧。



你从上一章中学到了很重要的一点,开发游戏并不需要那么的复杂,只需要让它看上去像那么回事。



对于目前的游戏来说,我们并不需要复杂的A*(http://baike.baidu.com/view/7850.htm), DFS/BFS(传说中令人生畏的深度优先搜索和广度优先搜索算法,如果你是个数学狂人或者算法爱好者,还请移步这里:http://baike.baidu.com/view/288277.htm 和这里:http://baike.baidu.com/view/825760.htm )或者最佳优先搜索算法(http://baike.baidu.com/view/298415.html),一些简单的代码足以让这些敌人在地图上动起来。当然,现在不用并不意味着不能用,只是我们无论是设计还是实现都要尽量遵循K.I.S.S懒人原则(http://baike.baidu.com/view/1649155.htm)。

 

代码狂人们,觉悟吧,不要一天到晚纠缠于该如何完善哪个算法,你不是科学家,你是攻城师。作为一个攻城师,你的主要任务是满足用户需求,解决用户的问题,而不是成为下一个图灵奖的得主。当然,如果你就是这么计划的,建议你不要和开发游戏的攻城师们混,而要和真正的天才黑客们打拼,否则无论是你的老板,你的客户,还是你自己,都会非常痛苦。

 

废话少说,先把这一部分的源代码下载下来吧:Cocos2d iPhone Game Tower Defense Tutorial Part 2.

 



 

玩家要安放炮塔很简单,只需要从地图上的”Game HUD(游戏信息显示器)“部分将炮塔拖曳到地图的可放置区域即可。每个炮塔都有自己的作用范围,当玩家从game hud中拖曳炮塔放置在地图上时可以看到。好吧,激动人心的时刻到了,如果玩家把炮塔拖曳到一块沙地上,那么我们就在那里放置了一挺机枪。如果玩家把炮塔拖曳到沙地之外的其它地方,那么我们就认为玩家并不想放置武器,只是手抖了点错了而已。至于你信不信,我反正是信了。

你可以发现,只要是有沙地的地方,就可以在那里安放炮塔。我们通过两个方法来实现这一点-“tileCoordForPosition”和”addTower”。”tileCoordForPosition”这个方法可以快速判断当前所在的瓦片,然后使用addTower方法来把炮塔实际放置在地图上(假定那里可以放置)。

 

如何设计开发iphone塔防游戏3-炮塔就位

 

好了,让我们打开TutorialScene.m这个文件,来看看这两个方法的具体实现:

- (CGPoint) tileCoordForPosition:(CGPoint) position
{
         int x = position.x / self.tileMap.tileSize.width;
         int y = ((self.tileMap.mapSize.height * self.tileMap.tileSize.height) - position.y) / self.tileMap.tileSize.height;

         return ccp(x,y);
}

-(void)addTower: (CGPoint)pos {
         DataModel *m = [DataModel getModel];

         Tower *target = nil;

         CGPoint towerLoc = [self tileCoordForPosition: pos];

         int tileGid = [self.background tileGIDAt:towerLoc];
         NSDictionary *props = [self.tileMap propertiesForGID:tileGid];
         NSString *type = [props valueForKey:@"buildable"];

         NSLog(@"Buildable: %@", type);
         if([type isEqualToString: @"1"]) {
              target = [MachineGunTower tower];
              target.position = ccp((towerLoc.x * 32) + 16, self.tileMap.contentSize.height - (towerLoc.y * 32) - 16);
              [self addChild:target z:1];

              target.tag = 1;
              [m._towers addObject:target];

         } else {
              NSLog(@"Tile Not Buildable");
         }

}

当用户想要在某处放置炮塔的时候,会调用addTower方法,其参数就是该处的坐标值。然后我们使用tileCoordForPosition获取实际的瓦片位置,再使用该位置获取该瓦片。然后我们来查看该瓦片的属性,判断该处是否属于“buildable(可修建的)”。如果该瓦片的类型是“1”,我们就知道这里可以修建炮塔。然后我们会在该处放置一个新的炮塔,并将其位置设置在瓦片的中心点上。当然,如果瓦片的类型是”0”,就意味着此处不能安放炮塔,我们就心安理得的喝茶去了。

 

既然上面谈到了炮塔这个类,那么就得看看其中的代码了。

 

先看看Tower.h这个头文件吧:

#import "cocos2d.h"
#import "SimpleAudioEngine.h"
#import "DataModel.h"

@interface Tower : CCSprite {
         int _range;

         CCSprite * selSpriteRange;
}

@property (nonatomic, assign) int range;

@end

@interface MachineGunTower : Tower {

}

+ (id)tower;
- (void)towerLogic:(ccTime)dt;

@end

Tower类继承自CCSprite精灵类,有一个range(范围)属性,它的作用是限制炮塔可以发射炮弹的范围,而selSpriteRange这个精灵则是用图形的方式来视觉化呈现出这个范围。当然,我们还定义了一个MachineGunTower类,它直接继承自Tower类,是对Tower类的扩展。为毛要这么做呢?既然每种炮塔都有自己独一无二的属性,我们应该为之设计独立的分类,从而一目了然。但这些炮塔同时又都具备着一些共性,比如都有攻击范围,都有各自的伤害值。我们将在后续的教程中对Tower基本类和各种炮塔子类进行扩展。

 

现在来看看Tower.m这个文件的代码:

#import "Tower.h"

@implementation Tower

@synthesize range = _range;

@end

@implementation MachineGunTower

+ (id)tower {

    MachineGunTower *tower = nil;
    if ((tower = [[[super alloc] initWithFile:@"MachineGunTurret.png"] autorelease])) {
              tower.range = 200;

              [tower schedule:@selector(towerLogic:) interval:0.2];

    }

    return tower;

}

-(id) init
{
         if ((self=[super init]) ) {
              //[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
         }
         return self;
}

-(void)towerLogic:(ccTime)dt {

}

@end

上面的代码需要解释的并不多。只是一个基本的类实现。我们载入了一个图像,设置了炮塔的攻击范围,然后创建了一个定时器,从而每隔0.2秒调用一次towerLogic方法。当然,现在这个炮塔只是个花架子,它不需要做任何事情,所以这个方法的内容目前是空的,后续的教程中我们会对此进行补充。

 

炮塔的事情搞定了,让我们来看看Game Hud(游戏信息显示器)吧。这个hud里面包含了所有可选的炮塔,你可以从中挑选一种炮塔将其拖曳到游戏层。从现在起,我们在屏幕上将拥有两个不同的层。

 

让我们看看GameHud的头文件:

#import "cocos2d.h"

@interface GameHUD : CCLayer {
         CCSprite * background;

         CCSprite * selSpriteRange;
    CCSprite * selSprite;
    NSMutableArray * movableSprites;
}

+ (GameHUD *)sharedHUD;

@end

来看看上面的代码吧:background就是放置到CCLayer的背景图片,selSprite是炮塔图片的复制,当玩家从游戏层选择这个炮塔的时候,我们会在屏幕上拖动它。selSpriteRange就是代表攻击范围的图片。然后我们还定义了一个moveableSprites数组,里面存放了我们将要在游戏层上显示的炮塔精灵。

 

接下来看看GameHUD实现文件中的init方法:

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

              CGSize winSize = [CCDirector sharedDirector].winSize;

        //画出游戏HUD的背景

        [CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGB565];
        background = [CCSprite spriteWithFile:@"hud.png"];
        background.anchorPoint = ccp(0,0);
        [self addChild:background];
        [CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_Default];

        //加载炮塔图片,并将它们绘制到游戏HUD层,你当然可以用四个完全不同的图片来代表不同的炮塔

        movableSprites = [[NSMutableArray alloc] init];
        NSArray *images = [NSArray arrayWithObjects:@"MachineGunTurret.png", @"MachineGunTurret.png", @"MachineGunTurret.png", @"MachineGunTurret.png", nil];
        for(int i = 0; i < images.count; ++i) {
            NSString *image = [images objectAtIndex:i];
            CCSprite *sprite = [CCSprite spriteWithFile:image];
            float offsetFraction = ((float)(i+1))/(images.count+1);
            sprite.position = ccp(winSize.width*offsetFraction, 35);
            [self addChild:sprite];
            [movableSprites addObject:sprite];
        }
              [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
         }
         return self;
}

上面两段注释的中文可以基本上解释我们做了些什么。程序首先载入了hud的背景图片,然后我们遍历了一个数组,将这些图像加载并存储为精灵,指定它们的位置,让它们彼此之间保持一定的间距,将其添加为游戏层的子节点,并存储在moveableSprites数组中,便于后续的查找。关于如何把4个图片精确的放在一行,我们还可以参考这里:Ray's tutorials found here about dragging and dropping images.

最后,我们需要通知CCTouchDispatcher,需要处理一些触碰事件。

 

好了,接下来我们来看看当玩家和游戏层互动的时候,我们该如何处理这些事件:

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint touchLocation = [self convertTouchToNodeSpace:touch];

         CCSprite * newSprite = nil;
    for (CCSprite *sprite in movableSprites) {
        if (CGRectContainsPoint(sprite.boundingBox, touchLocation)) {
                     DataModel *m = [DataModel getModel];
                     m._gestureRecognizer.enabled = NO;

                     selSpriteRange = [CCSprite spriteWithFile:@"Range.png"];
                     selSpriteRange.scale = 4;
                     [self addChild:selSpriteRange z:-1];
                     selSpriteRange.position = sprite.position;

            newSprite = [CCSprite spriteWithTexture:[sprite texture]]; //sprite;
                     newSprite.position = sprite.position;
                     selSprite = newSprite;
                     [self addChild:newSprite];

            break;
        }
    }
    return TRUE;
}

上面一大堆代码究竟做了些什么?还记得上面提过我们把代表炮塔图片的精灵存储在moveableSprites这个数组中吗?这里我们用一个循环来遍历这些精灵,使用CCRectContainsPoint来判断玩家的触摸点是否在炮塔图片范围内。如果是的话,我们会调用DataModel,通知它首先关闭”gestureRecognizer“,或许你还记得这个变量,它是指向”UIPanGestureRecognizer”的指针,这样做的意思是---当我们在拖动炮塔的时候,先别移动屏幕了。然后我们复制了正在拖动的炮塔,将其赋给selSprite。当然,在此之前,我们还添加了炮塔攻击范围的图片。这样的话当玩家在放置炮塔的时候知道可以打多远。

 

 

好了,那么当玩家的触摸点开始移动起来时又会发生什么?让我们来看看ccTouchMoved这个方法:

- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint touchLocation = [self convertTouchToNodeSpace:touch];

    CGPoint oldTouchLocation = [touch previousLocationInView:touch.view];
    oldTouchLocation = [[CCDirector sharedDirector] convertToGL:oldTouchLocation];
    oldTouchLocation = [self convertToNodeSpace:oldTouchLocation];

    CGPoint translation = ccpSub(touchLocation, oldTouchLocation);   

         if (selSprite) {
              CGPoint newPos = ccpAdd(selSprite.position, translation);
        selSprite.position = newPos;
              selSpriteRange.position = newPos;

              DataModel *m = [DataModel getModel];
              CGPoint touchLocationInGameLayer = [m._gameLayer convertTouchToNodeSpace:touch];

              BOOL isBuildable = [m._gameLayer canBuildOnTilePosition: touchLocationInGameLayer];
              if (isBuildable) {
                     selSprite.opacity = 200;
              } else {
                     selSprite.opacity = 50;
              }
         }
}

通过上面的代码,我们可以在屏幕上拖动“伪“炮塔,这样玩家就知道在哪里可以放下炮塔。更有趣的是,哥在这里添加了额外的代码(调用TutorialScene.m中canBuildOnTilePosition这个方法),从而允许程序检查瓦片的位置,判断玩家把炮塔所拖到的位置是否是可修建的:

 

- (BOOL) canBuildOnTilePosition:(CGPoint) pos
{
         CGPoint towerLoc = [self tileCoordForPosition: pos];

         int tileGid = [self.background tileGIDAt:towerLoc];
         NSDictionary *props = [self.tileMap propertiesForGID:tileGid];
         NSString *type = [props valueForKey:@"buildable"];

         if([type isEqualToString: @"1"]) {
              return YES;
         }

         return NO;
}

该方法会再次检查瓦片的属性,如果是“可修建的”(值为1),就返回YES,否则返回NO。如果该瓦片是不可修建的,我们将CCSprite精灵的透明度设置为50,否则,将其设置为100(完全显示)。当然,你完全可以用其它方法来向玩家显示该处是否可以修建,比如将精灵染成红色代表所在位置不可修建,而染成绿色或蓝色代表所在位置可以修建。这里就不需要多言了,感兴趣的朋友自己来试试吧。

 

到了现在,就只剩对触摸事件结束的处理了:

- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
         CGPoint touchLocation = [self convertTouchToNodeSpace:touch];
         DataModel *m = [DataModel getModel];

         if (selSprite) {
              CGRect backgroundRect = CGRectMake(background.position.x,
                                                                  background.position.y,
                                                                  background.contentSize.width,
                                                                  background.contentSize.height);

              if (!CGRectContainsPoint(backgroundRect, touchLocation)) {
                     CGPoint touchLocationInGameLayer = [m._gameLayer convertTouchToNodeSpace:touch];

                     [m._gameLayer addTower: touchLocationInGameLayer];
              }

              [self removeChild:selSprite cleanup:YES];
              selSprite = nil;
              [self removeChild:selSpriteRange cleanup:YES];
              selSpriteRange = nil;
         }

         m._gestureRecognizer.enabled = YES;
}

在放下炮塔之前,得让玩家有后悔的机会,也就是说在某些地方放下炮塔是可以取消修建的。默认的位置当然就是gameHud层本身。所以我们做了一个判断,看触摸点结束的位置是否在Hud背景图片的范围内。如果被确认在游戏层上,我们就调用addTower这个方法。如果不是,我们只需要清除这个炮塔,同时清除“伪”炮塔图片和代表其攻击范围的精灵。

 

看完这一段,可谓小功告成了!让合作的美工稍作修改,你会发现已经像那么回事了。那么下一章我们要做的就是,把这部分和上部分的内容整合在一起,再加上可以开火的炮塔!

 

小结一下,

本章我们创建了一个独立的game hud层;

学习了从一个层将炮塔拖曳到另一个层;

学习了瓦片地图的更多技巧!

 

我们还将学习的是:

如何让炮塔向敌人开火

怎样开启一轮轮的波次进攻,以及如何制作特殊的炮塔!

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

?>