您的位置:首页 > 移动开发 > Cocos引擎

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

2013-03-30 07:04 911 查看
如何使用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];

}

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

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







当然,这里采用的射击方式并非是最佳的,因为我们在连续分配炮弹,而在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];

}

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

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





敌军凶猛!

如果这些敌军坦克只是静坐修禅,当然最好不过!不过这样游戏也少了很多乐趣!这里将从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];

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





让敌军坦克动起来

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

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

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;

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



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐