如何使用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;
编译运行游戏,现在就可以尽情的坦克大战了!
原文链接: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;
编译运行游戏,现在就可以尽情的坦克大战了!
相关文章推荐
- 如何使用cocos2dx 制作一个多向滚屏坦克类射击游戏-第二部分
- 如何使用cocos2d制作一个多向滚屏射击游戏-第一部分
- (译)如何使用cocos2d和box2d来制作一个Breakout游戏:第二部分(完)
- 如何使用cocos2dx 制作一个多向滚屏坦克类射击游戏-第一部分
- (译)如何使用cocos2d制作一个打地鼠的游戏:(第二部分。完)
- 如何使用cocos2d-x3.0和物理引擎来制作一个Breakout游戏:第二部分
- (译)如何使用cocos2d和box2d来制作一个Breakout游戏:第二部分(完)
- (译)如何使用cocos2d制作一个塔防游戏:第二部分
- (译)如何使用cocos2d制作一个塔防游戏:第二部分
- 15. 如何使用cocos2d制作一个打地鼠的游戏:(第二部分。完)
- (译)如何使用cocos2d制作一个滑动图片游戏教程:第二部分(完)
- 8.如何使用cocos2d和box2d来制作一个Breakout游戏:第二部分
- (译)如何使用cocos2d制作一个打地鼠的游戏:(第二部分。完)
- 如何使用cocos2d-x来制作一个塔防游戏:第二部分
- 如何使用cocos2d-x3.0制作一个打地鼠的游戏:第二部分
- 11. 碰撞检测和收集物品:如何使用cocos2d制作基于tiled地图的游戏:第二部分
- 如何使用Cocos2d-x 3.0制作基于tilemap的游戏:第二部分
- 如何使用cocos2d-x3.0制作一个滑动图片游戏:第二部分
- 如何使用cocos2d-x来制作一个塔防游戏:第三部分
- (译)如何使用cocos2d制作一个塔防游戏:第四部分(完)