Harder Monsters and More Levels: How To Make A Simple iPhone Game with Cocos2D 2.X Part 3

Note from Ray: You guys voted for me to update this classic beginning Cocos2D tutorial series from Cocos2D 1.X to Cocos2D 2.X in the weekly tutorial vote, so your wish is my command! :] This tutorial series is now fully up-to-date for Cocos2D 2.X, Xcode 4.5, and has a ton of improvements such as Retina […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share

Contents

Hide contents

Harder Monsters and More Levels: How To Make A Simple iPhone Game with Cocos2D 2.X Part 3

10 mins

Watch out for the green guy!

Watch out for the green guy!

Note from Ray: You guys voted for me to update this classic beginning Cocos2D tutorial series from Cocos2D 1.X to Cocos2D 2.X in the weekly tutorial vote, so your wish is my command! :]

This tutorial series is now fully up-to-date for Cocos2D 2.X, Xcode 4.5, and has a ton of improvements such as Retina display and iPhone 4″ screen support. Here’s the pre Cocos2D 1.X version if you need it!

So far, the game you’ve been making in How To Make A Simple iPhone Game with Cocos2D 2.X tutorial series is pretty cool. You have a rotating turret, monsters to shoot, and uber sound effects.

But our turret has it too easy. The monsters only take one shot, and there’s just one level! He’s not even warming up yet.

In this tutorial, you will extend our project so that you make different types of monsters of varying difficulty, and implement multiple levels into the game.

Tougher Monsters

For fun, let’s create two types of monsters: a weak and fast monster, and a strong and slow monster. To help the player distinguish between the two, download these new resources for this tutorial, and add them to your project. It includes a new monster image and a cool explosion sound effect.

Now let’s make your Monster class. There are many ways to model your Monster class, but you’re going to do the simplest thing, which is to make our Monster class a subclass of CCSprite. You’re also going to create two subclasses of Monster: one for our weak and fast monster, and one for our strong and slow monster.

Note: Subclassing CCSprite like this works fine for simple games like this, but the more complicated your games get the more likely you are to run into maintainability issues long term when you do this. If your game is going to be complex, you might want to look into component based architecture instead.

Stay tuned – we will have a tutorial on this site on the subject soon! :]

Create a new file with the iOS\cocos2d v2.x\CCNode class template, make it a subclass of CCSprite, and name it Monster.m.

Then replace Monster.h with the following:

#import "cocos2d.h"

@interface Monster : CCSprite

@property (nonatomic, assign) int hp;
@property (nonatomic, assign) int minMoveDuration;
@property (nonatomic, assign) int maxMoveDuration;

- (id)initWithFile:(NSString *)file hp:(int)hp minMoveDuration:(int)minMoveDuration maxMoveDuration:(int)maxMoveDuration;

@end

@interface WeakAndFastMonster : Monster
@end

@interface StrongAndSlowMonster : Monster
@end

Pretty straightforward here: you just derive Monster from CCSprite and add a few variables for tracking monster state, and then derive two subclasses of Monster for two different types of monsters.

Now open up Monster.m and add in the implementation:

#import "Monster.h"

@implementation Monster

- (id)initWithFile:(NSString *)file hp:(int)hp minMoveDuration:(int)minMoveDuration maxMoveDuration:(int)maxMoveDuration {
    if ((self = [super initWithFile:file])) {
        self.hp = hp;
        self.minMoveDuration = minMoveDuration;
        self.maxMoveDuration = maxMoveDuration;
    }
    return self;
}

@end

@implementation WeakAndFastMonster

- (id)init {
    if ((self = [super initWithFile:@"monster.png" hp:1 minMoveDuration:3 maxMoveDuration:5])) {
    }
    return self;
}

@end

@implementation StrongAndSlowMonster

- (id)init {
    if ((self = [super initWithFile:@"monster2.png" hp:3 minMoveDuration:6 maxMoveDuration:12])) {
    }
    return self;
}

@end

This just defines the base class initializer, and the initializer for each of the subclasses, which set the default values for each type of monster.

Now let’s integrate our new Monster class into the rest of the code! First add the import to your new file to the top of HelloWorldLayer.m:

#import "Monster.h"

Then let’s modify the addMonster method to construct instances of our new class rather than creating the sprite directly. Replace the spriteWithFile line with the following:

//CCSprite * monster = [CCSprite spriteWithFile:@"monster.png"];
Monster * monster = nil;
if (arc4random() % 2 == 0) {
    monster = [[[WeakAndFastMonster alloc] init] autorelease];
} else {
    monster = [[[StrongAndSlowMonster alloc] init] autorelease];
}

This will give a 50% chance to spawn each type of monster. Also, since you’ve moved the speed of the monsters into the classes, modify the min/max duration lines as follows:

int minDuration = monster.minMoveDuration; //2.0;
int maxDuration = monster.maxMoveDuration; //4.0;

Finally, a couple mods to the updateMethod. First modify the code that loops through the monsters as follows:

BOOL monsterHit = FALSE;
NSMutableArray *monstersToDelete = [[NSMutableArray alloc] init];
for (Monster *monster in _monsters) {
    
    if (CGRectIntersectsRect(projectile.boundingBox, monster.boundingBox)) {
        monsterHit = TRUE;
        monster.hp --;
        if (monster.hp <= 0) {
            [monstersToDelete addObject:monster];
        }
        break;
    }
}

So basically, instead of instantly killing the monster, you subtract an HP and only destroy it if it's 0 or lower. Also, note that you break out of the loop if the projectile hits a monster, which means the projectile can only hit one monster per shot.

Finally, modify the projectilesToDelete test as follows:

if (monsterHit) {
    [projectilesToDelete addObject:projectile];
    [[SimpleAudioEngine sharedEngine] playEffect:@"explosion.caf"];
}

Build and run the code, and if all goes well you should see two different types of monsters running across the screen - which makes our turret's life a bit more challenging!

Multiple type of monsters in the game!

Multiple Levels

In order to implement multiple levels, you need to create a class that keeps track all of the information that differentiates one level from another. For this simple game, you'll just keep track of three things: the level number, how many seconds in between spawning enemies (harder levels will spawn monsters faster), and the background color (so you can easily differentiate levels).

To do this, create a new class with the iOS\Cocoa Touch\Objective-C class template. Name the class Level and make it a subclass of NSObject.

Then replace Level.h with the following:

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface Level : NSObject

@property (nonatomic, assign) int levelNum;
@property (nonatomic, assign) float secsPerSpawn;
@property (nonatomic, assign) ccColor4B backgroundColor;

- (id)initWithLevelNum:(int)levelNum secsPerSpawn:(float)secsPerSpawn backgroundColor:(ccColor4B)backgroundColor;

@end

And Level.m with the following:

#import "Level.h"

@implementation Level

- (id)initWithLevelNum:(int)levelNum secsPerSpawn:(float)secsPerSpawn backgroundColor:(ccColor4B)backgroundColor {
    if ((self = [super init])) {
        self.levelNum = levelNum;
        self.secsPerSpawn = secsPerSpawn;
        self.backgroundColor = backgroundColor;
    }
    return self;
}

@end

As you can see, this is a very simple plain-old object that just keeps track of these three pieces of information.

Next, you need to create a class that keeps track of all of the levels, as well as which level you are currently on. To do this, create a new class with the iOS\Cocoa Touch\Objective-C class template. Name the class LevelManager and make it a subclass of NSObject.

Then replace LevelManager.h with the following:

#import <Foundation/Foundation.h>
#import "Level.h"

@interface LevelManager : NSObject

+ (LevelManager *)sharedInstance;
- (Level *)curLevel;
- (void)nextLevel;
- (void)reset;

@end

And replace LevelManager.m with the following:

#import "LevelManager.h"

@implementation LevelManager {
    NSArray * _levels;
    int _curLevelIdx;
}

+ (LevelManager *)sharedInstance {
    static dispatch_once_t once;
    static LevelManager * sharedInstance; dispatch_once(&once, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

- (id)init {
    if ((self = [super init])) {
        _curLevelIdx = 0;
        Level * level1 = [[[Level alloc] initWithLevelNum:1 secsPerSpawn:2 backgroundColor:ccc4(255, 255, 255, 255)] autorelease];
        Level * level2 = [[[Level alloc] initWithLevelNum:2 secsPerSpawn:1 backgroundColor:ccc4(100, 150, 20, 255)] autorelease];
        _levels = [@[level1, level2] retain];
    }
    return self;
}

- (Level *)curLevel {
    if (_curLevelIdx >= _levels.count) {
        return nil;
    }
    return _levels[_curLevelIdx];
}

- (void)nextLevel {
    _curLevelIdx++;
}

- (void)reset {
    _curLevelIdx = 0;
}

- (void)dealloc {
    [_levels release];
    _levels = nil;
    [super dealloc];
}

@end

This is also a very simple class that keeps an array of the levels, as well as an index to the current level. It also contains a few helper methods to get the current level, advance to the next level, or reset back to the beginning.

Note that this class is a singleton, created/accessed through the static sharedInstance method.

Almost done - just need to refactor your code a bit to use this! Open HelloWorldLayer.m and make the following changes:

// Add to top of file
#import "LevelManager.h"

// In init, replace the call to super initWithColor with this:
if ((self = [super initWithColor:[LevelManager sharedInstance].curLevel.backgroundColor])) {

// In init, replace the line that sets up the gameLogic: selector with this:
[self schedule:@selector(gameLogic:) interval:[LevelManager sharedInstance].curLevel.secsPerSpawn];

Notice you are just reading the values from the current Level class rather than having them be the same hardcoded values for each level.

Finally open GameOverLayer.m and replace initWithWon: with this:

- (id)initWithWon:(BOOL)won {
    if ((self = [super initWithColor:ccc4(255, 255, 255, 255)])) {
        
        NSString * message;
        if (won) {
            [[LevelManager sharedInstance] nextLevel];
            Level * curLevel = [[LevelManager sharedInstance] curLevel];
            if (curLevel) {
                message = [NSString stringWithFormat:@"Get ready for level %d!", curLevel.levelNum];
            } else {
                message = @"You Won!";
                [[LevelManager sharedInstance] reset];
            }
        } else {
            message = @"You Lose :[";
            [[LevelManager sharedInstance] reset];
        }

        CGSize winSize = [[CCDirector sharedDirector] winSize];
        CCLabelTTF * label = [CCLabelTTF labelWithString:message fontName:@"Arial" fontSize:16];
        label.color = ccc3(0,0,0);
        label.position = ccp(winSize.width/2, winSize.height/2);
        [self addChild:label];
        
        [self runAction:
         [CCSequence actions:
          [CCDelayTime actionWithDuration:3],
          [CCCallBlockN actionWithBlock:^(CCNode *node) {
             [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]];
        }],
          nil]];
    }
    return self;
}

Notice this moves to the next level or resets the game if appropriate, and displays a "get ready" string instead of the win/lose message if the player is advancing to the next level.

That's it! Build and run, and see if you have what it takes to beat the game! :]

Multiple levels in the Cocos2D game

Contributors

Over 300 content creators. Join our team.