如何使用cocos2d制作一个多向滚屏射击游戏-第二部分

本文指导如何使用Cocos2d2.0开发坦克大战游戏,加入炮火发射、敌军坦克与游戏胜负机制,包括触摸控制、碰撞检测与游戏结束条件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如何使用cocos2d 2.0开发类似坦克大战的游戏(且支持ARC)2

 

原文链接:http://www.raywenderlich.com/6888/how-to-make-a-multi-directional-scrolling-shooter-part-2

 

示例项目链接:http://www.raywenderlich.com/downloads/Tanks2.zip

 

文中的代码根据个人习惯稍有修改。

 

在第一部分的内容中,我们创建了一个全新的支持ARC的Cocos2D 2.0项目,将瓦片地图添加到游戏中,并添加了一个坦克,可以使用加速计来进行操控。

 

在这部分(也是最后一部分)的内容中,我们将让坦克可以发射炮弹,同时会添加敌军坦克,添加游戏的赢/输机制,等等。

接下来的内容将从第一部分已完成的项目开始,可以从这里(http://www.raywenderlich.com/downloads/Tanks1.zip)直接下载。

 

准备好了,还是来制作游戏吧。

 

炮火连天

 

现在我们的坦克已经可以四处移动了,但还不能开火!坦克要开火跟女生要买新衣服一样天经地义,所以必须得尽快解决这个问题:)

 

当然,由于之前使用加速计来控制坦克的移动,这里可以直接使用触摸的方式让坦克开火。不过,为了让游戏变得更有趣一点,我们不仅要在玩家触摸的时候发货,还可以让坦克连续开火!

 

在Xcode中切换到Tank.h,在其中做出以下修改:

//在@interface部分添加以下代码:

CGPoint shootVector;

double timeSinceLastShot;

CCSprite *turret;

 

//在@interface之后添加以下代码:

 

@property(assign) BOOL shooting;

 

-(void)shootToward:(CGPoint)position;

-(void)shootNow;

 

在上面的代码中,我们添加了一个实例变量shootVector用于保存射击的方向,变量timeSinceLastShot用于保存从上次射击到现在所经过的时间。还添加了一个turret变量来保存添加到坦克顶部的新精灵对象-坦克的炮塔!

 

切换到Tank.m,并对代码做出以下调整:

在文件的顶部添加:

#import "SimpleAudioEngine.h"

在@implementation之后添加以下代码:

@synthesize shooting;

在initWithLayer:theType:theHp方法中添加以下代码:

 

NSString *turretName = [NSStringstringWithFormat:@"tank%d_turret.png",type];

turret = [CCSpritespriteWithSpriteFrameName:turretName];

turret.anchorPoint = ccp(0.5,0.25);

turret.position = ccp(self.contentSize.width/2,self.contentSize.height/2);

      [selfaddChild:turret];

 

在以上代码中,我们创建了一个新的精灵对象代表坦克炮塔,并将其添加为坦克的子节点。这样当我们移动坦克精灵的时候,炮塔也会随之移动。

 

请注意放置炮塔的方式:

首先将锚点的位置设置在靠近炮塔的基座。为什么这样做呢?因为锚点的位置就是旋转的中心点,而我们想要让炮塔沿着基座旋转,就必须将锚点设置在靠近基座。

接着我们将炮塔精灵对象的位置设置在坦克的中心。由于炮塔精灵是坦克的子节点,其位置是相对坦克的左下角的。这样我们就把锚点(炮塔的基座)连接在坦克的中心点上。

 

接下来在文件的底部添加一个新的方法:

 

-(void)shootToward:(CGPoint)position{

 

CGPoint offset = ccpSub(targetPosition, self.position);

float MIN_OFFSET = 10;

if(ccpLength(offset) < MIN_OFFSET) return;

 

shootVector = ccpNormalize(offset);

}

当玩家触摸屏幕的时候,就会调用该方法。这里需要检查触摸点到目标位置的距离不小于10个点(如果太近,则很难判断射击的方向)。接着我们将向量规范化(也即把向量的长度设置为1),从而得到一个射击的方向向量,并将其保存在shootVector变量中,以便后续使用。

 

接下来添加实际射击的方法如下:

 

-(void)shootNow{

 

//1

CGFloat angle = ccpToAngle(shootVector);

turret.rotation = (-1 * CC_RADIANS_TO_DEGREES(angle)) +90;

 

//2

float mapMax = MAX([layer tileMapWidth],[layer tileMapHeight]);

CGPoint actualVector = ccpMult(shootVector, mapMax);

 

//3

float POINTS_PER_SECOND = 300;

float duration = mapMax /POINTS_PER_SECOND;

 

//4

NSString *shootSound = [NSStringstringWithFormat:@"tank%dShoot.wav",type];

  [[SimpleAudioEnginesharedEngine]playEffect:shootSound];

 

//5

NSString *bulletName = [NSStringstringWithFormat:@"tank%d_bullet.png",type];

CCSprite *bullet = [CCSpritespriteWithSpriteFrameName:bulletName];

  bullet.tag = type;

  bullet.position = ccpAdd(self.position, ccpMult(shootVector, turret.contentSize.height));

CCMoveBy *move = [CCMoveByactionWithDuration:duration position:actualVector];

CCCallBlockN *call = [CCCallBlockNactionWithBlock:^(CCNode *node) {

    [node removeFromParentAndCleanup:YES];

  }];

  [bullet runAction:[CCSequenceactions:move,call, nil]];

  [layer.batchNodeaddChild:bullet];

 

}

让我们来解释下其中的代码(按照注释顺序):

1.首先我们将炮塔选中到面朝射击的方向。这里使用一个简单的辅助函数ccpToAngle,可以将向量转换成以弧度为单位的向量角度。急着将其转换成Cocos2D中使用的角度,然后乘以——1,因为在Cocos2D中使用顺时针旋转。同时需要加上90,因为炮塔的美术素材是朝上的(而不是朝右)。

2.接着我们计算出炮弹要射击的距离。这里我们获得瓦片地图宽度或高度的最大值,并乘以射击的方向向量;

3.再接下来我们需要计算出炮弹要达到指定地点所需的时间。这一点很简单,只需使用向量长度(瓦片地图宽度或高度中更大的数值)除以每秒的运动速度即可

4.添加音效

5.最后我们创建了一个新的炮弹精灵,并让其执行某个动作(在动作完成后消失),并将其添加到层的精灵表单中。

 

 

接下来对Tank.m做出以下修改:

 

添加以下新的方法:

-(BOOL)shouldShoot{

if(!self.shooting) returnNO;

 

double SECS_BETWEEN_SHOTS = 0.25;

if(timeSinceLastShot> SECS_BETWEEN_SHOTS){

 

timeSinceLastShot = 0;

returnYES;

  }else{

returnNO;

  }

}

 

-(void)updateShoot:(ccTime)dt{

 

timeSinceLastShot += dt;

if([selfshouldShoot]){

    [selfshootNow];

 

}

然后修改update方法如下:

 

- (void)update:(ccTime)dt {   

  [selfupdateMove:dt]; 

  [selfupdateShoot:dt];

}

 

 

通过以上代码,可以让坦克连续射击。每一次更新我们都会调用updateShoot方法。如果从上次射击到现在的时间超过了0.25秒,则调用shootNow方法。

 

好了,Tank.m已经完成。在Xcode中切换到HelloWorldLayer.m,并使用以下内容替代ccTouchesBegan和ccTouchesMoved:

 

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

 

UITouch * touch = [touches anyObject];

CGPoint mapLocation = [tileMapconvertTouchToNodeSpace:touch];

 

self.tank.shooting = YES;

  [self.tankshootToward:mapLocation];

 

//    self.tank.moving = YES;

//    [self.tank moveToward:mapLocation];

 

 

}

 

- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {

 

UITouch * touch = [touches anyObject];

CGPoint mapLocation = [tileMapconvertTouchToNodeSpace:touch];

 

//    self.tank.moving = YES;

//    [self.tank moveToward:mapLocation];

self.tank.shooting = YES;

  [self.tankshootToward:mapLocation];

 

 

}

 

 

通过以上的方法,我们使用触碰来进行射击,而非移动坦克。

 

编译运行游戏,可以触摸屏幕连续射击了!

 如何使用cocos2d <wbr>2.0开发类似坦克大战的游戏(且支持ARC)2



当然,这里采用的射击方式并非是最佳的,因为我们在连续分配炮弹,而在ios中这样是非常耗费内存的。一个更好的方式是预先分配一个炮弹数组,并在需要发射炮弹时重用之前的旧炮弹。

 

添加敌军坦克

 

任何一个坦克对战游戏都需要有敌军坦克,在Xcode中打开HelloWorldLayer.h,然后创建一个数组用于保存敌军坦克:

NSMutableArray *enemyTanks;

 

然后打开HelloWorldLayer.m,并在init方法的地步添加以下代码,以产生一些敌军坦克:

 

enemyTanks = [NSMutableArrayarray];

int NUM_ENEMY_TANKS = 50;

for(int i= 0; i< NUM_ENEMY_TANKS; ++i){

 

Tank *enemy = [[Tankalloc]initWithLayer:selftype:2hp:2];

CGPoint randSpot;

BOOL inWall = YES;

 

while(inWall){

          randSpot.x = CCRANDOM_0_1() *[selftileMapWidth];

          randSpot.y = CCRANDOM_0_1() *[selftileMapHeight];

          inWall = [selfisWallAtPosition:randSpot]; 

        }

        enemy.position = randSpot;

        [batchNodeaddChild:enemy];

        [enemyTanksaddObject:enemy];

      }

 

以上代码不难理解。我们在一些随机点创建了一批坦克(只要不是在水中)。

 

编译运行,可以看到敌军坦克遍布地图!为了方便坦克英雄识别,这里将敌军坦克都标识为红色!

如何使用cocos2d <wbr>2.0开发类似坦克大战的游戏(且支持ARC)2

 

 

敌军凶猛!

 

如果这些敌军坦克只是静坐修禅,当然最好不过!不过这样游戏也少了很多乐趣!这里将从Tank类派生一个子类RandomTank,并覆盖其中的一些方法。

 

在Xcode中使用iOS\Cocoa Touch\Objective-C class模板创建一个新的文件,将其命名为RandomTank,并将subclass of设置为Tank。打开RandomTank.h,并使用以下的代码替代其中的内容:

 

#import "Tank.h"

 

@interface RandomTank : Tank{

 

double timeForNextShot;

}

 

@end

 

这里添加了一个实例变量,用于记录到下一次设计前要等候多少秒。

 

切换到RandomTank.m,并使用以下代码替代其中的内容:

 

#import "RandomTank.h"

#import "HelloWorldLayer.h"

 

@implementation RandomTank

 

-(id)initWithLayer:(HelloWorldLayer *)theLayer type:(int)theType hp:(int)theHp{

 

if((self = [superinitWithLayer:theLayer type:theType hp:theHp])){

    [selfschedule:@selector(move:)interval:0.5];

  }

returnself;

}

 

-(BOOL)shouldShoot{

 

if(ccpDistance(self.position, layer.tank.position) >600) returnNO;

 

if(timeSinceLastShot>timeForNextShot){

timeSinceLastShot = 0;

timeForNextShot = (CCRANDOM_0_1() * 3)+1;

    [self shootToward:layer.tank.position];

returnYES;

  }else {

returnNO;

  }

 

}

 

-(void)calcNextMove{

//TODO

}

 

-(void)move:(ccTime)dt{

if(self.moving&&arc4random()% 3 !=0) return;

  [selfcalcNextMove];

}

 

@end

 

 

以上定时了一个移动方法,每半秒调用一次。

当进行射击时,我们会首先确保敌军坦克离坦克英雄足够近,否则如果敌军坦克在很远的地方就开炮,会让游戏难度大大提升。

接下来我们计算出下一次射击的随机时间,大概在1-4秒之间。如果达到该时间,会更新坦克的目标,并继续。

 

在HelloWorldLayer.m中添加以下代码:

#import "RandomTank.h"

 

然后在init方法中修改创建正常坦克的代码,如下:

 

RandomTank *enemy = [[RandomTankalloc]initWithLayer:selftype:2hp:2];

 

编译运行游戏,当坦克英雄距离敌军坦克一定距离的时候,敌军就会开炮了!

 如何使用cocos2d <wbr>2.0开发类似坦克大战的游戏(且支持ARC)2

 

让敌军坦克动起来

 

现在虽然敌军坦克已经开始射击了,但还需要让它们四处动一动。

 

为了让游戏尽可能简化,这里采取的策略是:

1.选择一个临近的随机点

2.确保该路径上没有障碍物,如果是,则让坦克朝该点移动

3.如果不是,则返回第一步

 

这里唯一需要考虑的是第2步!指定起始点和终点的坐标,我们如何走过坦克需要移动的瓦片,并确保不会遇上障碍物?

 

幸运的是,这个问题已被解决了,可参考James McNeil的博客(http://playtechs.blogspot.com/2007/03/raytracing-on-grid.html),这里我们直接使用他给出的方。

 

切换到RandomTank.m,并使用以下代码替代calcNextMove方法:

// From http://playtechs.blogspot.com/2007/03/raytracing-on-grid.html

-(BOOL)clearPathFromTileCoord:(CGPoint)start toTileCoord:(CGPoint)end{

int dx = abs(end.x - start.x);

int dy = abs(end.y - start.y);

int x = start.x;

int y = start.y;

int n = 1 + dx +dy;

int x_inc = (end.x>start.x) ? 1: -1;

int y_inc = (end.y>start.y) ? 1: -1;

int error = dx - dy;

  dx *=2;

  dy *=2;

 

for(;n>0; --n){

if ([layer isWallAtTileCoord:ccp(x,y)]) returnFALSE;

 

if(error >0){

      x += x_inc;

      error -= dy;

    }

else{

      y += y_inc;

      error += dx;

    }

 

  }

returnYES;

 

}

 

-(void)calcNextMove{

 

BOOL moveOK = NO;

CGPoint start = [layer tileCoordForPosition:self.position];

CGPoint end;

 

while (!moveOK){

 

    end = start;

    end.x += CCRANDOM_MINUS1_1() *((arc4random() % 10) +3);

    end.y += CCRANDOM_MINUS1_1() *((arc4random() % 10) +3);

 

    moveOK = [selfclearPathFromTileCoord:start toTileCoord:end];

 

  }

 

 

 

CGPoint moveToward = [layer positionForTileCoord:end];

 

self.moving = YES;

  [selfmoveToward:moveToward];

 

}

不要担心上面第一个方法的工作原理(如果感兴趣可以仔细看看那篇博客),只需要知道它可以检查在起点和终点之间是否存在障碍,如果是则返回FALSE。

 

而在calcNextMove方法中,我们使用了上面的算法。

编译运行,可以看到敌军坦克开始动起来!

 

碰撞,爆炸和出口

 

现在我们有敌人可以打,有炮弹可以发射,还需要的就是刺激的爆炸效果,还有就是让坦克英雄取得胜利的出口!

 

在HelloWorldLayer.h中对代码做出以下修改:

 

在@interface之前添加一个枚举变量:

 

typedefenum {

 

  kEndReasonWin,

  kEndReasonLose

 

}EndReason;

 

在@interface中添加以下几个实例变量;

 

CCParticleSystemQuad  *explosion;

CCParticleSystemQuad *explosion2;

BOOL gameOver;

CCSprite *exit;

 

接下来切换到HelloWorldLayer.m,并在init方法的底部添加以下代码:

 

explosion = [CCParticleSystemQuadparticleWithFile:@"explosion.plist"];

      [explosionstopSystem];

      [tileMapaddChild:explosionz:1];

 

explosion2 = [CCParticleSystemQuadparticleWithFile:@"explosion2.plist"];

      [explosion2stopSystem];

      [tileMapaddChild:explosion2z:1];

 

exit = [CCSpritespriteWithSpriteFrameName:@"exit.png"];

CGPoint exitTileCoord = ccp(98,98);

CGPoint exitTilePos = [selfpositionForTileCoord:exitTileCoord];

exit.position = exitTilePos;

      [batchNodeaddChild:exit];

 

self.scale  = 0.5;

 

在以上代码中,我们使用Cocos2D内置的粒子系统创建了两种不同类型的爆炸效果,并将其添加为瓦片地图的子节点,但首先需要先将其关闭。当需要使用的时候,会把它们移动到需要的地方,并使用resetSystem来启动。

 

接着我们在地图的右下角添加了一个出口。一旦坦克到达这一点,玩家就赢得了战斗!

 

注意到这里把层的比例设置为0.5,因为我们希望可以看到地图的更多内容。

 

现在在update方法的前面添加这些新的方法:

 

-(void)restartTapped:(id)sender{

  [[CCDirectorsharedDirector]replaceScene:[CCTransitionFlipXtransitionWithDuration:0.5scene:[HelloWorldLayerscene]]];

 

}

 

-(void)endScene:(EndReason)endReason{

 

if(gameOver) return;

gameOver = true;

 

CGSize winSize = [CCDirectorsharedDirector].winSize;

 

  NSString *message;

if(endReason == kEndReasonWin){

    message = @"You Win!";

  }elseif(endReason == kEndReasonLose){

    message = @"You Lose!";

  }

 

  CCLabelBMFont *label;

if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad){

 

    label = [CCLabelBMFontlabelWithString:message fntFile:@"TanksFont.fnt"];

  }else{

    label = [CCLabelBMFontlabelWithString:message fntFile:@"TanksFont.fnt"];

  }

 

  label.scale = 0.1;

  label.position = ccp(winSize.width/2,winSize.height *0.7);

  [selfaddChild:label];

 

  CCLabelBMFont *restartLabel;

if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad){

    restartLabel = [CCLabelBMFontlabelWithString:@"Restart"fntFile:@"Tanksfont.fnt"];

 

  }else{

    restartLabel = [CCLabelBMFontlabelWithString:@"Restart"fntFile:@"Tanksfont.fnt"];

  }

 

CCMenuItemLabel *restartItem = [CCMenuItemLabelitemWithLabel:restartLabel target:selfselector:@selector(restartTapped:)];

  restartItem.scale = 0.1;

  restartItem.position = ccp(winSize.width/2,winSize.height *0.3);

 

CCMenu *menu = [CCMenumenuWithItems:restartItem, nil];

  menu.position = CGPointZero;

  [selfaddChild:menu];

 

  [restartItem runAction:[CCScaleToactionWithDuration:0.5scale:4.0]];

  [label runAction:[CCScaleToactionWithDuration:0.5scale:4.0]];

 

}

 

以上方法我曾在多个原型游戏中使用。如果看过系列的其它博文应该知道,其作用就是重新启动游戏,这里就不再解释这些了。如果觉得看不太明白,可以先从系列的开始看起。

 

接下来在update方法的开始添加以下代码:

 

// 1

if(_gameOver)return;

 

// 2

if(CGRectIntersectsRect(_exit.boundingBox, _tank.boundingBox)){

[self endScene:kEndReasonWin];

}

 

// 3

NSMutableArray* childrenToRemove =[NSMutableArray array];

// 4

for(CCSprite * sprite in self.batchNode.children){

// 5

if(sprite.tag !=0){// bullet    

// 6      

if([self isWallAtPosition:sprite.position]){

[childrenToRemove addObject:sprite];

continue;

}

// 7

if(sprite.tag ==1){// hero bullet

for(int j = _enemyTanks.count -1; j >=0; j--){

                Tank *enemy =[_enemyTanks objectAtIndex:j];

if(CGRectIntersectsRect(sprite.boundingBox, enemy.boundingBox)){

 

[childrenToRemove addObject:sprite];

                    enemy.hp--;

if(enemy.hp <=0){

[[SimpleAudioEngine sharedEngine] playEffect:@"explode3.wav"];

                        _explosion.position = enemy.position;

[_explosion resetSystem];

[_enemyTanks removeObject:enemy];

[childrenToRemove addObject:enemy];

}else{

[[SimpleAudioEngine sharedEngine] playEffect:@"explode2.wav"];

}

}

}

}

// 8

if(sprite.tag ==2){// enemy bullet               

if(CGRectIntersectsRect(sprite.boundingBox, self.tank.boundingBox)){

[childrenToRemove addObject:sprite];

                self.tank.hp--;

 

if(self.tank.hp <=0){

[[SimpleAudioEngine sharedEngine]playEffect:@"explode2.wav"];                       

                    _explosion.position = self.tank.position;

[_explosion resetSystem];

[self endScene:kEndReasonLose];

}else{

                    _explosion2.position = self.tank.position;

[_explosion2 resetSystem];

[[SimpleAudioEngine sharedEngine] playEffect:@"explode1.wav"];                       

}

}

}

}

}

for(CCSprite * child in childrenToRemove){

[child removeFromParentAndCleanup:YES];

}

 

以上就是碰撞检查和游戏机制,这里稍微解释下:

 

1.开始记录游戏的状态,游戏是否结束。如果游戏已结束则无需做任何事。

2.如果坦克碰到出口,则玩家赢得胜利!

3.开始碰撞检测,有时候有的精灵在碰撞后需要从屏幕中删除(例如,当炮弹碰到坦克或障碍的时候,会被删除)。

4.对炮弹精灵设置标记,从而可以轻松将其识别

5.如果在炮弹的位置有障碍,则移除炮弹。

6.如果炮弹是由坦克英雄发射的,则检查它是否击中了敌军坦克,如果是,则将敌军坦克的HP减少(如果HP<=0则将其销毁)。同时还播放一个音效,以及激活一个爆炸的粒子系统。

7.与之类似,如果是敌军坦克发射的炮弹,则检查是否击中了坦克英雄,并进行相应的操作。当玩家的HP达到0时游戏以失败告终。

 

最后一步,在accelerometer:didAccelerate,ccTouchesBegan和ccTouchesMoved方法的前面添加以下代码:

if(gameOver) return;

 

编译运行游戏,现在就可以尽情的坦克大战了!

 

 

 

如何使用cocos2d <wbr>2.0开发类似坦克大战的游戏(且支持ARC)2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值