关闭

手把手教你开发一款IOS飞行射击游戏(七)

标签: cocos2dIOS游戏
185人阅读 评论(0) 收藏 举报
分类:

这一篇,我们将完成GameLayer的制作。

在上一篇中,我们完成了敌人飞船的添加,但是运行的时候我们发现,子弹击中目标后直接穿过目标,并没有出现我们想要的效果。那么下面我们就开始为飞船制作生命条,并且完善子弹的效果。

首先我们创建类HealthBarComponent,代表飞船的生命条,HealthBarComponent继承自CCSpriteHealthBarComponent.h定义如下:

#import "cocos2d.h"

 

@interface HealthBarComponent : CCSprite{

    float orginalScale;

}

 

- (void)reset;

- (void)updateBar;

- (void)hide;

 

@end

 

HealthBarComponent.m的代码:

#import "HealthBarComponent.h"

#import "Entity.h"

 

@implementation HealthBarComponent

 

- (id)init{

    if (self = [superinitWithSpriteFrameName:@"healthBar.png"]) {

       self.visible = NO;

       self.anchorPoint = CGPointZero;

    }

    

    return self;

}   //init

 

- (void)reset{

    Entity* parent =(Entity*)self.parent;

    self.scaleX =parent.width/(float)self.contentSize.width;

    orginalScale = self.scaleX;

    self.position =ccp(parent.position.x, parent.position.y+parent.height);

    self.visible = YES;

}   //reset

 

- (void)updateBar{

    Entity* parent =(Entity*)self.parent;

    if (parent.visible == YES) {

       self.position = ccp(parent.position.x, parent.position.y+parent.height);

        self.scaleX= parent.healthPercentage*orginalScale;

    }else{

       self.visible = NO;

    }

}   //update

 

- (void)hide{

    self.visible = NO;

}

 

@end

 

我们用下面的图片初始化healthBar

healthBar.png

我们将这张图片也组装到dark-travel-elements中(TexturePacker

然后重新替换4publish生成的文件。

接着我们实现了resetupdateBar两个方法,从名字我们也能够看出,healthBar作为一个组件被添加给Entity,所以它的位置是跟Entity保持相对的,这也是我们在程序中希望看到的,飞船的生命条随着飞船移动而移动。接着,编译器提示我们healthPercentage未找到。我们在Entity中添加下面这个属性:

@property (readonly) float healthPercentage;

 

然后实现这个属性的get方法:

- (float)healthPercentage{

    return health/(float)healthRecord;

}   //healthPercentage

 

然后我们修改Entity的定义,为其添加一个本地变量:

@interface Entity : CCNode{

    …

    HealthBarComponent* healthBar;

}

接着,我们分别在AirCraftEnemyShipinit方法的最后(看一下HealthBarComponentreset函数我们可以发现,需要在parent对象,也就是EntityentitySpriteposition初始化完成之后,才可以调用HealthBarComponentreset方法)添加初始化healthBar的语句:

healthBar = [HealthBarComponent node];

[self addChild:healthBar];

[healthBar reset];

初始化好之后,我们在Entity中添加方法声明:

- (void)updateHealthBar;

然后实现这个方法为一个空方法。

接着我们在AirCraftEnemyShip类中重载这个方法:

- (void)updateHealthBar{

    [healthBar updateBar];

}

修改EnemyShipshowUp方法,添加updateHealthBar的调用:

- (void)showUp:(ccTime)delta{

    if (self.position.x >=[CommonUtility utility].screenWidth - [entitySprite contentSize].width-50) {

        [selfmoveByX:-(300*delta) andY:0];

        [selfupdateHealthBar];

    }else{

        [selfunschedule:_cmd];

       nextShootTime = 0;

        [selfscheduleUpdate];

    }

}

EnemyShipmoveWithLength:andAreaLimitation:movedTime:方法末尾添加[self updateHealthBar];

最后,在UserInteractionLayerupdate方法中,在[airCraft moveByX:velocity.x * deltaandY:velocity.y * delta];这行语句下面添加:

[airCraft updateHealthBar];

编译,运行,效果如下:

手把手教你开发一款IOS飞行射击游戏(七) - Daniel - KHome

 

下面我们继续制作飞船被击中的效果:

Entity类中添加方法声明:

- (void)gotHit:(float) damage;

实现如下:

- (void)gotHit:(float) damage{

    if (damage > 0) {

        health -=damage;

       entitySprite.color = ccRED;

        [selfschedule:@selector(recoverFromDamage) interval:0.03f];

    }

    [healthBar updateBar];

}   //gotHit

然后添加recoverFromDamage方法:

- (void)recoverFromDamage{

    entitySprite.color = ccWHITE;

    [self unschedule:_cmd];

}   //recoverFromDamage

这里解释一下这两个方法,gotHit方法分别在飞船和敌人的update方法中调用(通过检测飞船与子弹的边界重叠计算伤害,我们已经实现了那个方法),当遇到的伤害大于0时,修改Entity的生命值,将精灵纹理的底色变为红色(这里的效果是实现亮度的叠加,有兴趣的童鞋可以查一下资料,熟悉PS的童鞋应该懂我),这样给玩家一种飞船被击中的感觉,然后更新healthBar,但是飞船不能一直变红,所以我们用了一个技巧,调用schedule,执行的方法是recoverFromDamage,这里@selector的意思就是返回后面那个名字对应的方法指针,interval定义了这个方法被调用的循环周期。再看一下recoverFromDamage这个方法,由于我们只需要调用一次这个方法将Entity的纹理底色置成白色,所以我们在recoverFromDamage中调用[selfunschedule:_cmd];将这个schedule取消掉,这里_cmd代表当前的这个方法的指针,也就是recoverFromDamage自己。

接着,我们在AirCraft中重写gotHit方法:

- (void)gotHit:(float) damage{

    [super gotHit:damage];

    if (health <= 0) {

       entitySprite.visible = NO;

        [selfunscheduleAllSelectors];

        [healthBarhide];

        health =healthRecord;

        [selfexplode];

    }

}

添加判断,当生命值小于0的时候,炸掉飞船。

AirCraftinit方法最后添加[self scheduleUpdate];,重载update方法:

- (void)update:(ccTime)delta{

    [self gotHit:[[BulletCacheManagersharedBulletCacheManager] getBulletHitDamageWithinArea:[entitySpriteboundingBox] isPlayerBullet:NO]];

}

这个方法调用我们之前的子弹碰撞算法计算当前飞船所在区域的敌人子弹伤害,然后用gotHit方法更新飞船的状态(healthBar)。

同样,EnemyShip中我们也重写gotHit方法:

- (void)gotHit:(float) damage{

    [super gotHit:damage];

    if (health <= 0) {

       entitySprite.visible = NO;

        [selfunscheduleAllSelectors];

        health =healthRecord;

        [healthBarhide];

        [selfexplode];

    }

}

update方法中添加:

[self gotHit:[[BulletCacheManagersharedBulletCacheManager] getBulletHitDamageWithinArea:[entitySpriteboundingBox] isPlayerBullet:YES]];

现在运行一下,我们发现我们的飞船可以击中敌人也可以被敌人击中了,并且生命条会随之减少,当生命值小于等于0时,飞船就看不到了。

 

我们继续完善这款游戏,为游戏添加音效。游戏中我们希望在我们的飞船射击的时候发出子弹发射的声音,在敌人和我们的飞船爆炸的时候发出爆炸声。

Cocos2d提供了两个音效播放类供我们使用,分别是CDAudioManagerSimpleAudioEngine,区别大家可以google一下。这里我们使用SimpleAudioEngine。首先我们需要找到两个音效,一个用作射击音效,一个用作爆炸音效,这里特别推荐一个mac下的音效生成软件cfxr,可以用来制作常用的音效,制作过程非常方便,可以随机生成各类音效,也可以自己调整参数创建自己想要的效果。或者我们可以从网上下载两个音效,假设分别叫shoot-sound.mp3explode-sound.wav(之所以叫这两个名字是因为我下载的就是这个名字T_T),我们把这两个音效文件加入Resources中。

接着我们在GameLayerinitCaches方法中预加载这两个音效:

[[SimpleAudioEngine sharedEngine]preloadEffect:@"explode-sound.wav"];

[[SimpleAudioEngine sharedEngine]preloadEffect:@"shoot-sound.mp3"];

然后我们在AirCraftshootBulletAtGunLevel方法中添加播放音效的代码:

[[SimpleAudioEngine sharedEngine]playEffect:@"shoot-sound.mp3"];

Entityexplode方法中添加爆炸音效:

[[SimpleAudioEngine sharedEngine]playEffect:@"explode-sound.wav"];

完成后编译,运行,现在就能听到射击和爆炸的音效了。

 

我们继续完善我们的游戏,这回我们使用Cocos2d的粒子效果来为添加飞船的爆炸效果。关于粒子效果(ParticleEffect)大家可以查阅相关的资料了解一下,这里我们介绍一个粒子效果的实用工具:ParticleDesigner,使用ParticleDesigner我们可以通过直观的拖拽来实时地观察粒子效果的最终显示效果,ParticleDesigner界面如下:

手把手教你开发一款IOS飞行射击游戏(七) - Daniel - KHome

右侧是设置各种参数的面板,左侧是对应右侧参数的实际效果模拟。我们按照上面图片设置好参数后,播放可以看到一个爆炸的效果,我们将这个效果保存成一个explosion.plist文件(文件类型选择cocos2d(plist)),将这个文件添加到工程Resources中,然后在Entityexplode方法中添加下面的语句:

CCParticleSystem* system = [CCParticleSystemQuadparticleWithFile:@"explosion.plist"];

system.positionType = kCCPositionTypeFree;

system.autoRemoveOnFinish = YES;

system.position = self.position;

[[GameLayer sharedGameLayer] addChild:system];

这段代码用我们之前生成的plist文件初始化一个CCParticleSystem,然后设置positionTypekCCPositionTypeFree(粒子不会随着发射器的位置变化而变化,这个属性对于我们没有影响),autoRemoveOnFinish置为YES,这样这个爆炸效果执行完会立即消失,然后将这个粒子效果的位置设置为飞船所在位置,将粒子效果添加到GameLayer中。

编译,运行,飞船爆炸的时候会有很炫的爆炸效果了。粒子效果能够实现丰富的效果,有兴趣的话可以自己调整参数来试试,另外ParticleDesigner也提供了丰富的素材库,点击右上角的“SharedEmitters”就能看到了。

那么我们接下去做什么呢,既然是个游戏,那么玩家一定不希望一上来就出现一堆敌人,连同BOSS一起都出场,一定要有一个从简到难得过程,一关一关过才有趣味性。我们在GameLayer类的声明中加入这几个变量:

int enemyShipTotal;

int currentWave;

int baseEnemyCount;

int wave;

在初始化方法中对这几个变量赋初始值:

currentWave = 1;

baseEnemyCount = 0;

wave = 1;

然后我们删掉addEnemies方法的声明和调用,在GameLayer中添加下面几个方法:

- (void)showWaveCount{

    CCLabelTTF* waveLB =(CCLabelTTF*)[self getChildByTag:WaveLabelTag];

    [waveLB setString:[NSStringstringWithFormat:@"Wave %i", wave]];

    waveLB.visible = YES;

    [selfschedule:@selector(hideWaveCount) interval:2];

}

 

- (void)hideWaveCount{

    CCLabelTTF* waveLB =(CCLabelTTF*)[self getChildByTag:WaveLabelTag];

    waveLB.visible = NO;

    [self unschedule:_cmd];

}

 

- (void)showWave:(int) waveIndex{

    [self showWaveCount];

    wave++;

    int tagBase = 0;

    EnemyShip* bossShip;

    switch (waveIndex) {

        case 6:

           bossShip = [[EnemyShipCacheManager sharedEnemyShipCacheManager]getEnemyShip:BOSS];

           [self addChild:bossShip z:-1 tag:EnemyShipStartTag];

           [bossShip reset];

           enemyShipTotal = 1;

           [self addEnemyShip:BigDaddy enemyCount:(2+2*baseEnemyCount) baseTag:tagBase];

           tagBase += 2+2*baseEnemyCount;

           [self addEnemyShip:PowerMaker enemyCount:(2+2*baseEnemyCount) baseTag:tagBase];

           enemyShipTotal += 4+4*baseEnemyCount;

           tagBase = enemyShipTotal;

        case 5:

           [self addEnemyShip:BigDaddy enemyCount:(1+baseEnemyCount) baseTag:tagBase];

           tagBase += 1+baseEnemyCount;

           [self addEnemyShip:LittleWorm enemyCount:(2+2*baseEnemyCount) baseTag:tagBase];

           enemyShipTotal += 3+3*baseEnemyCount;

           tagBase = enemyShipTotal;

        case 4:

           [self addEnemyShip:PowerMaker enemyCount:(1+baseEnemyCount) baseTag:tagBase];

           tagBase += 1+baseEnemyCount;

           [self addEnemyShip:SpeedKiller enemyCount:(2+2*baseEnemyCount)baseTag:tagBase];

           enemyShipTotal += 3+3*baseEnemyCount;

           tagBase = enemyShipTotal;

        case 3:

           [self addEnemyShip:BigDaddy enemyCount:(1+baseEnemyCount) baseTag:tagBase];

           tagBase += 1+baseEnemyCount;

           [self addEnemyShip:PowerMaker enemyCount:(1+baseEnemyCount) baseTag:tagBase];

           enemyShipTotal += 2+2*baseEnemyCount;

           tagBase = enemyShipTotal;

        case 2:

           [self addEnemyShip:LittleWorm enemyCount:(2+2*baseEnemyCount) baseTag:tagBase];

           enemyShipTotal += 2+2*baseEnemyCount;

           tagBase = enemyShipTotal;

        case 1:

           [self addEnemyShip:SpeedKiller enemyCount:(2+2*baseEnemyCount)baseTag:tagBase];

           enemyShipTotal += 2+2*baseEnemyCount;

           tagBase = enemyShipTotal;

           break;

           

        default:

           currentWave = 0;

           baseEnemyCount++;

           [self showNextWave];

           break;

    }

}

 

- (void)addEnemyShip:(EnemyShipTypes) enemyType

         enemyCount:(int) enemyCount

            baseTag:(int) baseTag{

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

        EnemyShip*enemyShip = [[EnemyShipCacheManager sharedEnemyShipCacheManager]getEnemyShip:enemyType];

        [selfaddChild:enemyShip z:-1 tag:EnemyShipStartTag+baseTag+i];

        [enemyShipreset];

    }

}

 

- (void)showNextWave{

    currentWave = currentWave+1;

    [self showWave:currentWave];

}

 

showWaveCounthideWaveCount用到了我们之前在gotHit中用到的技巧,在屏幕中间显示当前的关卡数,然后利用scheduleunschedule2秒后将添加的当前关卡数Label隐藏掉。showWave方法根据当前的wave来生成敌人添加到场景中,我们重构之后抽出了addEnemyShip:enemyCount:baseTag:这个方法,这个方法用于向场景中添加指定数量的某种敌人。这里我们解释一下showWave这里为什么要使用tag,我们思考一下,如何判断需要进入到下一关了?一种很容易想到的方法是,添加一个变量记录每一关的剩余敌人数量,每当一个敌人爆炸之后,这个剩余敌人数量的变量减一,当这个变量为0时,wave++,然后再调用showWave显示下一波敌人。这里有一个问题,如何在一个敌人爆炸之后修改GameLayer中的那个记录变量?显然每个Entity是独立于GameLayer的,如果通过这个变量将EntityGameLayer耦合到一起,显然是不合理的。所以我们定义了一个EnemyShipStartTag,从这个EnemyShipStartTag起,一直到EnemyShipStartTag+ enemyShipTotal,这些tag对应的元素都是敌人的飞船,我们只需要在每次update的时候判断是不是所有这些tag对应的敌人飞船都不可见了(也就是爆炸掉了),如果所有这些敌人都不可见了,说明下一波敌人应该出现了。还有一个baseEnemyCount,这个属性是一个难度基数,我们看一下switch语句,发现其实只有6关,从第7关开始,都是在原来的关卡上增加敌人的数量来进行难度增加的。

我们在GameLayer的初始化方法中添加关卡数的Label

CCLabelTTF* waveLabel = [CCLabelTTFlabelWithString:@"Wave 1" fontName:@"Arial" fontSize:80];

waveLabel.visible = NO;

[self addChild:waveLabel z:100 tag:WaveLabelTag];

waveLabel.position = [CommonUtilityutility].screenCenter;

waveLabel.color = ccWHITE;

 

我们修改一下GameLayerupdate方法,添加判断下一关的循环逻辑:

BOOL needShowNextWave = YES;

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

        EnemyShip*enemyShip = (EnemyShip*)[self getChildByTag:EnemyShipStartTag+i];

        if(enemyShip != nil) {

           if (enemyShip.visible == YES) {

               needShowNextWave = NO;

           }else{

               [self removeChildByTag:EnemyShipStartTag+i];

           }

        }

    }

    if (needShowNextWave) {

        [selfshowNextWave];

    }

编译,运行,我们完成了关卡的制作。

 

现在剩下最后一部分了,分数的制作。首先我们继续在GameLayer类声明中添加两个变量:

   int score;

int highScore;

 

接着我们在GameLayer的初始化函数中添加记录的Label

CCLabelTTF* scoreLB = [CCLabelTTFlabelWithString:@"Score : 0" fontName:@"Arial"fontSize:15];

        [selfaddChild:scoreLB z:100 tag:ScoreLabelTag];

       scoreLB.anchorPoint = CGPointZero;

       scoreLB.position = ccp(10, [CommonUtility utility].screenHeight-26);

       scoreLB.color = ccWHITE;

        

        highScore =[[[NSUserDefaults standardUserDefaults] objectForKey:@"highScore"]intValue];

        CCLabelTTF*highScoreLB = [CCLabelTTF labelWithString:[NSStringstringWithFormat:@"High Score : %i", highScore]fontName:@"Arial" fontSize:15];

       highScoreLB.anchorPoint = CGPointZero;

       highScoreLB.position = ccp(scoreLB.position.x+150, [CommonUtilityutility].screenHeight-26);

       highScoreLB.color = ccYELLOW;

        [selfaddChild:highScoreLB z:100 tag:HighScoreLabelTag];

这里我们使用NSUserDefaults standardUserDefaults来记录和读取玩家的highScore,关于NSUserDefaults和其他几种数据存储方式,可以参考链接:

http://blog.csdn.net/leikezhu1981/article/details/7108959

添加完成后运行一下,可以看到左上方出现了白色和黄色的当前记录和最高纪录。

接着在GameLayer中添加更新当前分数和最高分数的方法:

- (void)updateScore{

    CCLabelTTF* scoreLB =(CCLabelTTF*)[self getChildByTag:ScoreLabelTag];

    [scoreLB setString:[NSStringstringWithFormat:@"Score : %i", score]];

    if (score > highScore) {

        highScore =score;

        CCLabelTTF*highScoreLB = (CCLabelTTF*)[self getChildByTag:HighScoreLabelTag];

       [highScoreLB setString:[NSString stringWithFormat:@"High Score : %i",highScore]];

    }

}

 

- (void)updateHighScore{

    [[NSUserDefaultsstandardUserDefaults] setObject:[NSNumber numberWithInt:highScore] forKey:@"highScore"];

}

最后,在GameLayerupdate方法中添加如下语句:

if (self.defaultAirCraft.visible == NO) {

    [self updateHighScore];

}

接着修改update方法中下面的代码(更新score并添加updateScore的调用):

BOOL needShowNextWave = YES;

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

        EnemyShip*enemyShip = (EnemyShip*)[self getChildByTag:EnemyShipStartTag+i];

        if(enemyShip != nil) {

           if (enemyShip.visible == YES) {

               needShowNextWave = NO;

           }else{

               [self removeChildByTag:EnemyShipStartTag+i]; 

               score += enemyShip.scoreWorth;

               [self updateScore];

           }

        }

    }

到此,我们GameLayer就已经制作完成了,运行结果如下:

手把手教你开发一款IOS飞行射击游戏(七) - Daniel - KHome

下一篇里,我们会介绍切换场景的相关知识。

85
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场