Cocos2D Tutorial for iOS: How To Make A Space Shooter iPhone Game

A Cocos2D tutorial for iOS that will teach you how to make a space shooter iPhone game. By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Adding Asteroids

The game is looking good so far, but where's the danger and excitement?! Let's spice things up by adding some wild asteroids to the scene!

The approach we're going to take is every so often, we'll create an asteroid offscreen to the right of the screen. Then we'll run a Cocos2D action to move it to the left of the screen.

We could simply create a new asteroid every time we needed to spawn, but allocating memory is a slow operation and it's best when you can avoid it. So we'll pre-allocate memory for a bunch of asteroids, and simply grab the next available asteroid when we need it.

OK, let's see what this looks like. Start by adding a few new instance variables inside the @interface in HelloWorldLayer.h:

CCArray *_asteroids;
int _nextAsteroid;
double _nextAsteroidSpawn;

Then make the following changes to HelloWorldLayer.m:

// Add to top of file
#define kNumAsteroids   15

// Add to bottom of init
_asteroids = [[CCArray alloc] initWithCapacity:kNumAsteroids];
for(int i = 0; i < kNumAsteroids; ++i) {
    CCSprite *asteroid = [CCSprite spriteWithSpriteFrameName:@"asteroid.png"];
    asteroid.visible = NO;
    [_batchNode addChild:asteroid];
    [_asteroids addObject:asteroid];
}

// Add new method, above update loop
- (float)randomValueBetween:(float)low andValue:(float)high {
    return (((float) arc4random() / 0xFFFFFFFFu) * (high - low)) + low;
}

// Add to bottom of update loop
double curTime = CACurrentMediaTime();
if (curTime > _nextAsteroidSpawn) {
    
    float randSecs = [self randomValueBetween:0.20 andValue:1.0];
    _nextAsteroidSpawn = randSecs + curTime;
    
    float randY = [self randomValueBetween:0.0 andValue:winSize.height];
    float randDuration = [self randomValueBetween:2.0 andValue:10.0];
    
    CCSprite *asteroid = [_asteroids objectAtIndex:_nextAsteroid];
    _nextAsteroid++;
    if (_nextAsteroid >= _asteroids.count) _nextAsteroid = 0;
    
    [asteroid stopAllActions];
    asteroid.position = ccp(winSize.width+asteroid.contentSize.width/2, randY);
    asteroid.visible = YES;
    [asteroid runAction:[CCSequence actions:
                         [CCMoveBy actionWithDuration:randDuration position:ccp(-winSize.width-asteroid.contentSize.width, 0)],
                         [CCCallFuncN actionWithTarget:self selector:@selector(setInvisible:)],
                         nil]];
    
}

// Add new method
- (void)setInvisible:(CCNode *)node {
    node.visible = NO;
}

Some things to point out about the above code:

  • CCArray is similar to NSArray, but optimized for speed. So it's good to use in Cocos2D when possible.
  • Notice that we add all 15 asteroids to the batch node as soon as the game starts, but set them all to invisible. If they're invisible we treat them as inactive.
  • We use an instance variable (_nextAsteroidSpawn) to tell us the time to spawn an asteroid next. We always check this in the update loop.
  • If you're new to Cocos2D actions, these are easy ways to get sprites to do things over time, such as move, scale, rotate, etc. Here we perform a sequence of two actions: move to the left a good bit, then call a method that will set the asteroid to invisible again.

Compile and run your code, and now you have some asteroids flying across the screen!

Adding asteroids to dodge

Shooting Lasers

I don't know about you, but the first thing I think of when I see asteroids is MUST SHOOT THEM!

So let's take care of this urge by adding the ability to fire lasers! This code will be similar to how we added asteroids, because we'll create an array of reusable laser beams and move them across the screen with actions.

The main difference is we'll be using touch handling to detect when to shoot them.

Start by adding a few new instance variables inside the @interface in HelloWorldLayer.h:

CCArray *_shipLasers;
int _nextShipLaser;

Then make the following changes to HelloWorldLayer.m:

// Add to top of file
#define kNumLasers      5

// Add to bottom of init
_shipLasers = [[CCArray alloc] initWithCapacity:kNumLasers];
for(int i = 0; i < kNumLasers; ++i) {
    CCSprite *shipLaser = [CCSprite spriteWithSpriteFrameName:@"laserbeam_blue.png"];
    shipLaser.visible = NO;
    [_batchNode addChild:shipLaser];
    [_shipLasers addObject:shipLaser];
}

self.isTouchEnabled = YES;

// Add new method
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        
    CGSize winSize = [CCDirector sharedDirector].winSize;
       
    CCSprite *shipLaser = [_shipLasers objectAtIndex:_nextShipLaser];
    _nextShipLaser++;
    if (_nextShipLaser >= _shipLasers.count) _nextShipLaser = 0;
    
    shipLaser.position = ccpAdd(_ship.position, ccp(shipLaser.contentSize.width/2, 0));
    shipLaser.visible = YES;
    [shipLaser stopAllActions];
    [shipLaser runAction:[CCSequence actions:
                          [CCMoveBy actionWithDuration:0.5 position:ccp(winSize.width, 0)],
                          [CCCallFuncN actionWithTarget:self selector:@selector(setInvisible:)],
                          nil]];
    
}

So this shows you how easy it is to receive touch events in Cocos2D - just set isTouchEnabled to YES, then you can implement ccTouchesBegan (and/or ccTouchesMoved, ccTouchesEnded, etc)!

Compile and run your code, and now you can go pew-pew!

Shooting lasers from ship

Basic Collision Detection

So far things look like a game, but don't act like a game, because nothing blows up!

And since I'm naughty by nature (and not 'cause I hate ya), it's time to add some violence into this game!

Start by adding the following to the @interface in HelloWorldLayer.h:

int _lives;

Then add the following to the bottom of the update loop:

for (CCSprite *asteroid in _asteroids) {        
    if (!asteroid.visible) continue;
    
    for (CCSprite *shipLaser in _shipLasers) {                        
        if (!shipLaser.visible) continue;
        
        if (CGRectIntersectsRect(shipLaser.boundingBox, asteroid.boundingBox)) {                
            shipLaser.visible = NO;
            asteroid.visible = NO;                
            continue;
        }
    }
    
    if (CGRectIntersectsRect(_ship.boundingBox, asteroid.boundingBox)) {
        asteroid.visible = NO;
        [_ship runAction:[CCBlink actionWithDuration:1.0 blinks:9]];            
        _lives--;
    }
}

This is a very basic method of collision detection that just checks the bounding box of the sprites to see if they collide. Note that this counts transparency, so isn't a perfect way of checking for collisions, but it's good enough for a simple game like this.

For more info on a better way to do collision detection in Cocos2D, check out the How To Use Box2D For Just Collision Detection tutorial.

Compile and run your code, and now things should blow up!

Win/Lose Detection

We're almost done - just need to add a way for the player to win or lose!

We'll make it so the player wins if he survives for 30 seconds, and loses if he gets hit 3 times.

Start by making a few changes to HelloWorldLayer.h:

// Add before @interface
typedef enum {
    kEndReasonWin,
    kEndReasonLose
} EndReason;

// Add inside @interface
double _gameOverTime;
bool _gameOver;

Then make the following changes to HelloWorldLayer.m:

// Add at end of init
_lives = 3;
double curTime = CACurrentMediaTime();
_gameOverTime = curTime + 30.0;

// Add at end of update loop
if (_lives <= 0) {
    [_ship stopAllActions];
    _ship.visible = FALSE;
    [self endScene:kEndReasonLose];
} else if (curTime >= _gameOverTime) {
    [self endScene:kEndReasonWin];
}

// Add new methods above update
- (void)restartTapped:(id)sender {
    [[CCDirector sharedDirector] replaceScene:[CCTransitionZoomFlipX transitionWithDuration:0.5 scene:[HelloWorldLayer scene]]];   
}

- (void)endScene:(EndReason)endReason {
    
    if (_gameOver) return;
    _gameOver = true;
    
    CGSize winSize = [CCDirector sharedDirector].winSize;
    
    NSString *message;
    if (endReason == kEndReasonWin) {
        message = @"You win!";
    } else if (endReason == kEndReasonLose) {
        message = @"You lose!";
    }
    
    CCLabelBMFont *label;
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        label = [CCLabelBMFont labelWithString:message fntFile:@"Arial-hd.fnt"];
    } else {
        label = [CCLabelBMFont labelWithString:message fntFile:@"Arial.fnt"];
    }
    label.scale = 0.1;
    label.position = ccp(winSize.width/2, winSize.height * 0.6);
    [self addChild:label];
    
    CCLabelBMFont *restartLabel;
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        restartLabel = [CCLabelBMFont labelWithString:@"Restart" fntFile:@"Arial-hd.fnt"];    
    } else {
        restartLabel = [CCLabelBMFont labelWithString:@"Restart" fntFile:@"Arial.fnt"];    
    }
    
    CCMenuItemLabel *restartItem = [CCMenuItemLabel itemWithLabel:restartLabel target:self selector:@selector(restartTapped:)];
    restartItem.scale = 0.1;
    restartItem.position = ccp(winSize.width/2, winSize.height * 0.4);
    
    CCMenu *menu = [CCMenu menuWithItems:restartItem, nil];
    menu.position = CGPointZero;
    [self addChild:menu];
    
    [restartItem runAction:[CCScaleTo actionWithDuration:0.5 scale:1.0]];
    [label runAction:[CCScaleTo actionWithDuration:0.5 scale:1.0]];
    
}

Don't worry if you don't understand how the endScene method works - that's some code I've used for a bunch of games for a quick win/lose menu in the past.

The important part is just that you understand the rest of the code - every update loop, you just check to see if the player has won or lost, and call that method if so.

Compile and run the code, and see if you can lose!

Adding game over detection