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
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

Polishing Your Invader and Ship Images

You've been incredibly patient working with these less-than-menacing red, green, blue and magenta rectangles. Keeping the visuals simple has worked well because it allowed you to focus ruthlessly on getting your game logic correct.

Now you'll add some actual image sprites to make your game much more realistic — and more fun to play!

Replace makeInvaderOfType: with the following two methods:

-(NSArray*)loadInvaderTexturesOfType:(InvaderType)invaderType {
    NSString* prefix;
    switch (invaderType) {
        case InvaderTypeA:
            prefix = @"InvaderA";
            break;
        case InvaderTypeB:
            prefix = @"InvaderB";
            break;
        case InvaderTypeC:
        default:
            prefix = @"InvaderC";
            break;
    }
    //1
    return @[[SKTexture textureWithImageNamed:[NSString stringWithFormat:@"%@_00.png", prefix]],
             [SKTexture textureWithImageNamed:[NSString stringWithFormat:@"%@_01.png", prefix]]];
}

-(SKNode*)makeInvaderOfType:(InvaderType)invaderType {
    NSArray* invaderTextures = [self loadInvaderTexturesOfType:invaderType];
    //2
    SKSpriteNode* invader = [SKSpriteNode spriteNodeWithTexture:[invaderTextures firstObject]];
    invader.name = kInvaderName;
    //3
    [invader runAction:[SKAction repeatActionForever:[SKAction animateWithTextures:invaderTextures timePerFrame:self.timePerMove]]];

    invader.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:invader.frame.size];
    invader.physicsBody.dynamic = NO;
    invader.physicsBody.categoryBitMask = kInvaderCategory;
    invader.physicsBody.contactTestBitMask = 0x0;
    invader.physicsBody.collisionBitMask = 0x0;

    return invader;
}

Here's what the new code does:

  1. Loads a pair of sprite images — InvaderA_00.png and InvaderA_01.png — for each invader type and creates SKTexture objects from them.
  2. Uses the first such texture as the sprite's base image.
  3. Animates these two images in a continuous animation loop.

All of the images were included in the starter project and iOS knows how to find and load them, so there's nothing left to do here.

Build and run your app; you should see something similar to the screenshot below:

Invader sprite images

Looks pretty cool doesn't it? Next, you'll replace your blocky green ship with a much more retro and stylish looking version.

Replace makeShip with the following:

-(SKNode*)makeShip {
    //1
    SKSpriteNode* ship = [SKSpriteNode spriteNodeWithImageNamed:@"Ship.png"];
    ship.name = kShipName;
    //2
    ship.color = [UIColor greenColor];
    ship.colorBlendFactor = 1.0f;
    ship.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:ship.frame.size];
    ship.physicsBody.dynamic = YES;
    ship.physicsBody.affectedByGravity = NO;
    ship.physicsBody.mass = 0.02;
    ship.physicsBody.categoryBitMask = kShipCategory;
    ship.physicsBody.contactTestBitMask = 0x0;
    ship.physicsBody.collisionBitMask = kSceneEdgeCategory;

    return ship;
}

This code looks a bit different. Here's what's going on:

  1. Your ship sprite is now constructed from an image.
  2. Originally, the ship image is white, just like the invader images. But the code sets the sprite color to make the image green. Effectively this blends the green color with the sprite image.

Build and run your game; you should see your official-looking green ship appear as below:

Ship sprite image

Play your game for a while — what do you notice? Although you can blast happily away at the invaders, there's no clear victory or defeat. It's not much of a space war, is it?

Implementing the End Game

Think about how your game should end. What are the conditions that will lead to a game being over?

  • Your ship's health drops to zero.
  • You destroy all the invaders.
  • The invaders get too close to Earth.

You'll now add checks for each of the above conditions.

First, add the following constant to the #pragma mark - Custom Type Definitions section, underneath the definition for kShipSize::

#define kMinInvaderBottomHeight 2 * kShipSize.height

The above defines the height at which the invaders are considered to have invaded Earth.

Next, add the following import to the #import section at the top of the file:

#import "GameOverScene.h"

The above imports the header for a scene named GameOverScene which is already present in the starter project.

Next, add the following new property to the class extension:

@property BOOL gameEnding;

That sets everything up for the various game over scenarios.

Now, add the following two methods to the #pragma mark - Game End Helpers section:

-(BOOL)isGameOver {
    //1
    SKNode* invader = [self childNodeWithName:kInvaderName];

    //2
    __block BOOL invaderTooLow = NO;
    [self enumerateChildNodesWithName:kInvaderName usingBlock:^(SKNode *node, BOOL *stop) {
        if (CGRectGetMinY(node.frame) <= kMinInvaderBottomHeight) {
            invaderTooLow = YES;
            *stop = YES;
        }
    }];

    //3
    SKNode* ship = [self childNodeWithName:kShipName];

    //4
    return !invader || invaderTooLow || !ship;
}

-(void)endGame {
    //1
    if (!self.gameEnding) {
        self.gameEnding = YES;
        //2
        [self.motionManager stopAccelerometerUpdates];
        //3
        GameOverScene* gameOverScene = [[GameOverScene alloc] initWithSize:self.size];
        [self.view presentScene:gameOverScene transition:[SKTransition doorsOpenHorizontalWithDuration:1.0]];
    }
}

Here's what's happening in the first method, which checks to see if the game is over:

  1. Get all invaders that remain in the scene.
  2. Iterate through the invaders to check if any invaders are too low.
  3. Get a pointer to your ship: if the ship's health drops to zero, then the player is considered dead and the player ship will be removed from the scene. In this case, you'd get a nil value indicating that there is no player ship.
  4. Return whether your game is over or not. If there are no more invaders, or an invader is too low, or your ship is destroyed, then the game is over.

The second method actually ends the game and displays the game over scene. Here's what the code does:

  1. End your game only once. Otherwise, you'll try to display the game over scene multiple times and this would be a definite bug.
  2. Stop accelerometer updates.
  3. Show the GameOverScene. You can inspect GameOverScene.m for the details, but it's a basic scene with a simple "Game Over" message. The scene will start another game if you tap on it.

Add the following line as the first line of code in update::

    if ([self isGameOver]) [self endGame];

The above checks to see if the game is over every time the scene updates. If the game is over, then it displays the game over scene.

Build and run; blast away at the invaders until your game ends. Hopefully, you'll destroy all of the invaders before they destroy you! Once your game ends, you should see a screen similar to the following:

Game over

Tap the game over scene and you should be able to play again!

One Last Thing: Polish and Fidelity

It's a truism of game development that the last 20% of game development takes as long as the first 80%. When you're working on your next game, it's a good idea to start out iterating quickly with low-fidelity art assets (e.g. your colored squares) so you can quickly figure out if your game is fun to play.

If it's not fun to play with colored squares, it's not going to be fun to play with fancy art work, either! Nail down your gameplay and game logic first, then build out with fancy art assets and cool sound effects.

That being said, it's essential that you polish your game before releasing it to the App Store. The App Store is a crowded market and spit and polish will distinguish your app from the competition. Try to add little animations, storylines and a dash of cute factor that will delight your users. Also, consider being true to the game if you're remaking a classic.

If you're a fan of Space Invaders, you'll know that your remake is missing one important element. In the original game, the invaders march faster the closer they get to the bottom of the screen.

This was an artifact of the early CPU used to run the first Space Invaders game - the game loop ran faster and faster with fewer invaders because there was less work to do with each loop cycle. The game's programmer, Tomohiro Nishikado, decided to leave this behavior in the game as a challenging game mechanic.

You'll update your game to incorporate this game mechanic as well to please the retro gaming purists out there.

Add the following method to the #pragma mark - Invader Movement Helpers section:

-(void)adjustInvaderMovementToTimePerMove:(NSTimeInterval)newTimePerMove {
    //1
    if (newTimePerMove <= 0) return;

    //2
    double ratio = self.timePerMove / newTimePerMove;
    self.timePerMove = newTimePerMove;

    [self enumerateChildNodesWithName:kInvaderName usingBlock:^(SKNode *node, BOOL *stop) {
        //3
        node.speed = node.speed * ratio;
    }];
}

Let's examine this code:

  1. Ignore bogus values — a value less than or equal to zero would mean infinitely fast or reverse movement, which doesn't make sense.
  2. Set the scene's timePerMove to the given value. This will speed up the movement of invaders within moveInvadersForUpdate:. Record the ratio of the change so you can adjust the node's speed accordingly.
  3. Speed up the animation of invaders so that the animation cycles through its two frames more quickly. The ratio ensures that if the new time per move is 1/3 the old time per move, the new animation speed is 3 times the old animation speed. Setting the node's speed ensures that all of the node's actions run more quickly, including the action that animates between sprite frames.

Now, you need something to invoke this new method.

Modify determineInvaderMovementDirection as indicated by comments below:

...
case InvaderMovementDirectionDownThenLeft:
    proposedMovementDirection = InvaderMovementDirectionLeft;
    // Add the following line
    [self adjustInvaderMovementToTimePerMove:self.timePerMove * 0.8];
    *stop = YES;
    break;
case InvaderMovementDirectionDownThenRight:
    proposedMovementDirection = InvaderMovementDirectionRight;
    // Add the following line
    [self adjustInvaderMovementToTimePerMove:self.timePerMove * 0.8];
...

The new code simply reduces the time per move by 20% each time the invaders move down. This increases their speed by 25% (4/5 the move time means 5/4 the move speed).

Build and run your game, and watch the movement of the invaders; you should notice that those invaders move faster and faster as they get closer to the bottom of the screen:

Final Screen

This was a quick and easy code change that made your game that much more challenging and fun to play. If you're going to save the Earth from invading hordes, you might as well do it right! Spending time on seemingly minor tweaks like this is what makes the difference between a good game and a GREAT game.