How To Make a Game Like Space Invaders with Sprite Kit Tutorial: Part 2

Learn how to make a game like Space Invaders in this 2-part Sprite Kit tutorial! By .

Leave a rating/review
Save for later
Share

Note from Ray: This is a brand new Sprite Kit tutorial released as part of the iOS 7 Feast. Enjoy!

Welcome back to our 2-part Sprite Kit tutorial that teaches you how to make a game like Space Invaders!

In the first part, you created the foundation of the game. So far, you’ve added the invaders, your ship, and a Heads Up Display (HUD) to your game. You also coded the logic to make the invaders move automatically and to make your ship move as you tilted your device.

In this second and final part, you’ll add the ability for your ship and the aliens to fire on each other and blow each other up! You’ll also polish your game by adding sound effects and realistic images to replace the colored rectangles that currently serve as place holders for the invaders and your ship.

This tutorial picks up where the first part left off. If you don’t have the project already, you can download the example project where we left things off.

All right, it’s time to blow up some invaders!

Making Your Ship Fire its Laser Cannon

First, think about how you want your scene to fire the ship’s laser cannon. You decided to use single-taps to fire the cannon. But how should you detect these single taps?

You have two obvious choices:

  1. UITapGestureRecognizer – Create one and attach it to your scene’s view.
  2. touchesEnded:withEvent: – Your scene is a subclass of UIResponder; therefore, you could use this to detect touches directly.

The second approach is the best choice in this situation: touchesEnded:withEvent:. The first approach gets a bit tricky when you need to detect and handle touches differently in the various scene nodes of your game, since you can only specify a single callback selector for the UITapGestureRecognizer on the scene’s view. The extra work to get this working properly just isn’t worth it in this simple case.

Since all SKNode nodes (including SKScene) can handle touches directly via touchesEnded:withEvent:, the second choice is a much more natural approach to handling node-specific touches — and will pay off when you develop games with more complex tap handling.

Now that you’re going to detect user taps in your scene’s touchesEnded:withEvent: method, what should you do inside that method?

Taps can happen at any point during the gameplay. Contrast that with the way your scene changes: — at discrete intervals from within the update: method. So how can you save up taps detected at any time in touchesEnded:withEvent: and process them later in update: when it’s invoked by the Sprite Kit game loop?

The answer is a queue! You’re going to use a simple NSMutableArray to store your taps in a FIFO (First In First Out) queue.

Add the following property to the class extension in GameScene.m:

@property (strong) NSMutableArray* tapQueue;

Now add the following lines to didMoveToView: right after the [self.motionManager startAccelerometerUpdates]; line:

self.tapQueue = [NSMutableArray array];
self.userInteractionEnabled = YES;

The above code initializes the tap queue to an empty array and ensures that user interactions are enabled for the scene so it can receive tap events.

Now, add the following code right after #pragma mark - User Tap Helpers:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // Intentional no-op
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    // Intentional no-op
}

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    // Intentional no-op
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch* touch = [touches anyObject];
    if (touch.tapCount == 1) [self.tapQueue addObject:@1];
}

The first three methods are just empty stubs; they’re added because Apple suggests doing so when you override touchesEnded:withEvent: without calling super.

The touchesEnded:withEvent: method itself is fairly simple. It just adds an entry to the queue. You don’t need a custom class to store the tap in the queue since all you need to know is that a tap occurred. Therefore, you can use any old object. Here, you use the integer 1 as a mnemonic for single tap (@1 is the new object-literal syntax that converts the literal 1 into an NSNumber object).

Since you know that the invaders will also eventually fire bullets at your ship, add the following code to the Custom Type Definitions section at the top of the file:

typedef enum BulletType {
    ShipFiredBulletType,
    InvaderFiredBulletType
} BulletType;

You’re going to use BulletType to share the same bullet code for both invaders and your ship. It appears that you and the invaders shop at the same ammunition stores! :]

Next, add the following code just below #define kHealthHudName @"healthHud":

#define kShipFiredBulletName @"shipFiredBullet"
#define kInvaderFiredBulletName @"invaderFiredBullet"
#define kBulletSize CGSizeMake(4, 8)

Now, add the following method to the Scene Setup and Content Creation section:

-(SKNode*)makeBulletOfType:(BulletType)bulletType {
    SKNode* bullet;

    switch (bulletType) {
        case ShipFiredBulletType:
            bullet = [SKSpriteNode spriteNodeWithColor:[SKColor greenColor] size:kBulletSize];
            bullet.name = kShipFiredBulletName;
            break;
        case InvaderFiredBulletType:
            bullet = [SKSpriteNode spriteNodeWithColor:[SKColor magentaColor] size:kBulletSize];
            bullet.name = kInvaderFiredBulletName;
            break;
        default:
            bullet = nil;
            break;
    }

    return bullet;
}

This method is relatively straightforward: it simply creates a rectangular colored sprite to represent a bullet and sets the name of the bullet so you can find it later in your scene.

Now, add the following methods to the #pragma mark - Bullet Helpers section:

-(void)fireBullet:(SKNode*)bullet toDestination:(CGPoint)destination withDuration:(NSTimeInterval)duration soundFileName:(NSString*)soundFileName {
    //1
    SKAction* bulletAction = [SKAction sequence:@[[SKAction moveTo:destination duration:duration],
                                                  [SKAction waitForDuration:3.0/60.0],
                                                  [SKAction removeFromParent]]];
    //2
    SKAction* soundAction  = [SKAction playSoundFileNamed:soundFileName waitForCompletion:YES];
    //3
    [bullet runAction:[SKAction group:@[bulletAction, soundAction]]];
    //4
    [self addChild:bullet];
}

-(void)fireShipBullets {
    SKNode* existingBullet = [self childNodeWithName:kShipFiredBulletName];
    //1
    if (!existingBullet) {
        SKNode* ship = [self childNodeWithName:kShipName];
        SKNode* bullet = [self makeBulletOfType:ShipFiredBulletType];
        //2
        bullet.position = CGPointMake(ship.position.x, ship.position.y + ship.frame.size.height - bullet.frame.size.height / 2);
        //3
        CGPoint bulletDestination = CGPointMake(ship.position.x, self.frame.size.height + bullet.frame.size.height / 2);
        //4
        [self fireBullet:bullet toDestination:bulletDestination withDuration:1.0 soundFileName:@"ShipBullet.wav"];
    }
}

Going through the code in fireBullet:toDestination:withDuration:soundFileName: step-by-step, you do the following:

  1. Create an SKAction that moves the bullet to the desired destination and then removes it from the scene. This sequence executes the individual actions consecutively — the next action only takes place after the previous action has completed. Hence the bullet is removed from the scene only after it has been moved.
  2. Play the desired sound to signal that the bullet was fired. All sounds are included in the starter project and iOS knows how to find and load them.
  3. Move the bullet and play the sound at the same time by putting them in the same group. A group runs its actions in parallel, not sequentially.
  4. Fire the bullet by adding it to the scene. This makes it appear onscreen and starts the actions.

Here’s what you do in fireShipBullets:

  1. Only fire a bullet if there isn’t one currently on-screen. It’s a laser cannon, not a laser machine gun — it takes time to reload!
  2. Set the bullet’s position so that it comes out of the top of the ship.
  3. Set the bullet’s destination to be just off the top of the screen. Since the x coordinate is the same as that of the bullet’s position, the bullet will fly straight up.
  4. Fire the bullet!

The decision in //1 to only allow one ship bullet on-screen at the same time is a gameplay decision, not a technical necessity. If your ship can fire thousands of bullets per minute, Space Invaders would be too easy. Part of the fun of your game is choosing your shots wisely and timing them to collide with invaders.

Your laser cannon is almost ready to fire!

Add the following to the Scene Update Helpers section:

-(void)processUserTapsForUpdate:(NSTimeInterval)currentTime {
    //1
    for (NSNumber* tapCount in [self.tapQueue copy]) {
        if ([tapCount unsignedIntegerValue] == 1) {
            //2
            [self fireShipBullets];
        }
        //3
        [self.tapQueue removeObject:tapCount];
    }
}

Let’s review the above code:

  1. Loop over a copy of your tapQueue; it must be a copy because it’s possible that you’ll modify the original tapQueue while this code is running, and modifying an array while looping over it is a big no-no.
  2. If the queue entry is a single-tap, handle it. As the developer, you clearly know that you only handle single taps for now, but it’s best to be defensive against the possibility of double-taps (or other actions) later.
  3. Remove the tap from the queue.

Note: processUserTapsForUpdate: completely consumes the queue of taps at each invocation. Combined with the fact that fireShipBullets will not fire another bullet if one is already onscreen, this emptying of the queue means that extra or rapid-fire taps will be ignored. Only the first tap needed to fire a bullet will matter.

Note: processUserTapsForUpdate: completely consumes the queue of taps at each invocation. Combined with the fact that fireShipBullets will not fire another bullet if one is already onscreen, this emptying of the queue means that extra or rapid-fire taps will be ignored. Only the first tap needed to fire a bullet will matter.

Finally, add the following code as the first line in update::

[self processUserTapsForUpdate:currentTime];

This invokes processUserTapsForUpdate: during the update loop and processes any user taps.

Build your game, run, and tap away!

Player Bullets