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 2 of 5 of this article. Click here to view the first page.

Making Invaders Attack

Awesome, your ship can finally fire on those evil invaders! You’ll have them on the run soon enough.

But you’ve probably noticed that your bullets pass straight through invaders instead of blowing them up. That’s because your bullets aren’t yet smart enough to detect when they’ve hit an invader. You’re going to fix that in a moment.

First, you’ll make the invaders return fire by adding the code below to the Scene Update Helpers section:

-(void)fireInvaderBulletsForUpdate:(NSTimeInterval)currentTime {
    SKNode* existingBullet = [self childNodeWithName:kInvaderFiredBulletName];
    //1
    if (!existingBullet) {
        //2
        NSMutableArray* allInvaders = [NSMutableArray array];
        [self enumerateChildNodesWithName:kInvaderName usingBlock:^(SKNode *node, BOOL *stop) {
            [allInvaders addObject:node];
        }];

        if ([allInvaders count] > 0) {
            //3
            NSUInteger allInvadersIndex = arc4random_uniform([allInvaders count]);
            SKNode* invader = [allInvaders objectAtIndex:allInvadersIndex];
            //4
            SKNode* bullet = [self makeBulletOfType:InvaderFiredBulletType];
            bullet.position = CGPointMake(invader.position.x, invader.position.y - invader.frame.size.height/2 + bullet.frame.size.height / 2);
            //5
            CGPoint bulletDestination = CGPointMake(invader.position.x, - bullet.frame.size.height / 2);
            //6
            [self fireBullet:bullet toDestination:bulletDestination withDuration:2.0 soundFileName:@"InvaderBullet.wav"];
        }
    }
}

The central logic for the above method is as follows:

  1. Only fire a bullet if one’s not already on-screen.
  2. Collect all the invaders currently on-screen.
  3. Select an invader at random.
  4. Create a bullet and fire it from just below the selected invader.
  5. The bullet should travel straight down and move just off the bottom of the screen.
  6. Fire off the invader’s bullet.

Add the following line to the end of update::

[self fireInvaderBulletsForUpdate:currentTime];

This invocation of fireInvaderBulletsForUpdate: starts the invaders firing back at you.

Build, run, and you should see the invaders firing their purple bullets at your ship, as shown in the screenshot below:

Enemy Bullets

As a matter of game design, notice that the invaders’ bullets are purple while your ship’s bullets are green. This strong color contrast makes it easy to see the difference between bullets in the heat of battle. Also, you should hear a different sound when your ship fires versus when an invader fires. The use of different sounds is partly stylistic, to give your game rich audio and make it more immersive. But it’s also partly an accessibility issue since 7 – 10% of men and 0.5% – 1% women are color blind. The differentiation in sound effects will make your game more playable by those who are color blind.

Detecting When Bullets Hit Their Target

With all those bullets flying around on the screen it’s amazing that nothing blows up! That’s because your game has no hit detection. It needs to detect when your ship’s bullets hit an invader — and when an invader’s bullet hits your ship.

You could do this manually, comparing bullet/invader/ship positions at each update: invocation and checking for hits. But why not let Sprite Kit do the work for you?

Since you’re already using physics bodies, Sprite Kit’s physics engine can detect when one body hits another. For this, you’ll use contact detection — not collision detection. You’re not using physics to move bullets or invaders, so you’re not interested in the physical collisions between them. Contact detection merely detects when one physics body overlaps another in space; it doesn’t otherwise move or affect the bodies in contact.

Some games have many distinct types of physics bodies and are not interested in contact between all types of physics bodies. Sprite Kit will only check for contact between those categories of physics bodies that you tell it to check.

This is both a speed optimization and a correctness constraint, as some types of contact may not be desired. Controlling which physics bodies are checked for contact begins by defining category bitmasks.

Add the following code to the Custom Type Definitions section:

static const u_int32_t kInvaderCategory            = 0x1 << 0;
static const u_int32_t kShipFiredBulletCategory    = 0x1 << 1;
static const u_int32_t kShipCategory               = 0x1 << 2;
static const u_int32_t kSceneEdgeCategory          = 0x1 << 3;
static const u_int32_t kInvaderFiredBulletCategory = 0x1 << 4;

These strange-looking constants are bitmasks. A bitmask is basically a way of stuffing multiple on/off variables into a single 32-bit unsigned integer. A bitmask can have 32 distinct values when stored as a u_int32_t. Each of these five categories defines a type of physics body. Notice how the number to the right of the << operator is different in each case; that guarantees each bitmask is unique and distinguishable from the others.

Add the following code to createContent right before the [self setupInvaders]; line:

self.physicsBody.categoryBitMask = kSceneEdgeCategory;

This new code sets the category for the physics body of your scene.

Add the following code to makeShip right before the return ship; line to set up the categories for your ship:

//1
ship.physicsBody.categoryBitMask = kShipCategory;
//2
ship.physicsBody.contactTestBitMask = 0x0;
//3
ship.physicsBody.collisionBitMask = kSceneEdgeCategory;

Here's the breakdown of the above code:

  1. Set the ship's category.
  2. Don't detect contact between the ship and other physics bodies.
  3. Do detect collisions between the ship and the scene's outer edges.

Note:You didn't need to set the ship's collisionBitMask before because only your ship and the scene had physics bodies. The default collisionBitMask of "all" was sufficient in that case. Since you'll be adding physics bodies to invaders next, setting your ship's collisionBitMask precisely ensures that your ship will only collide with the sides of the scene and won't also collide with invaders.

While you're at it, you should set the category for the invaders since this will help detect collisions between your ship's bullets and the invaders.

Add the following to the end of makeInvaderOfType: right before the return invader; line:

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

This code gives your invader a physics body and identifies it as an invader using kInvaderCategory. It also indicates that you don't want invaders to contact or collide with other entities.

Your next step is to categorize bullets and set their contact and collision masks.

Add the following to the end of the first case statement in makeBulletOfType: right before the break:

bullet.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:bullet.frame.size];
bullet.physicsBody.dynamic = YES;
bullet.physicsBody.affectedByGravity = NO;
bullet.physicsBody.categoryBitMask = kShipFiredBulletCategory;
bullet.physicsBody.contactTestBitMask = kInvaderCategory;
bullet.physicsBody.collisionBitMask = 0x0;

The above code identifies ship-fired bullets as such and tells Sprite Kit to check for contact between ship-fired bullets and invaders, but that collisions should be ignored.

That takes care of the ship's bullets — now on to the invaders' bullets!

Add the following code block to the end of the second case statement in makeBulletOfType:, just before the break:

bullet.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:bullet.frame.size];
bullet.physicsBody.dynamic = YES;
bullet.physicsBody.affectedByGravity = NO;
bullet.physicsBody.categoryBitMask = kInvaderFiredBulletCategory;
bullet.physicsBody.contactTestBitMask = kShipCategory;
bullet.physicsBody.collisionBitMask = 0x0;

This code is similar to the previous block: it identifies invader-fired bullets as such and tells Sprite Kit to check for contact between invader-fired bullets and your ship, and again, ignores the collision aspect.

Note: In order for contact detection to work, the ship-fired bullets must be defined as dynamic by setting bullet.physicsBody.dynamic = YES. If not, Sprite Kit won't check for contact between these bullets and the static invaders as their definition is invader.physicsBody.dynamic = NO. Invaders are static because they aren't moved by the physics engine. Sprite Kit won't check for contact between two static bodies, so if you need to check for contact between two categories of physics bodies, at least one of the categories must have a dynamic physics body.

You may be wondering why the contactTestBitMask values are not symmetrical. For example, why are you setting an invader's contactTestBitMastk = 0x0 but a ship-fired bullet's contactTestBitMask = kInvaderCategory?

The reason is that when Sprite Kit checks for contact between any two physics bodies A and B, only one of the bodies needs to declare that it should test for contact with the other, not both. As long as either A declares that it can contact with B, or B declares that it can contact with A, contact will be detected. It's not necessary for both bodies to declare that they should test for contact with the other.

Setting the contactTestBitMask on only one type of body like you've done seems more manageable. You might prefer to set contactTestBitMask values on both types of bodies, and that's fine, as long as you're consistent in choosing one approach or the other.

With these changes, your game's physics engine will detect contact between ship-fired bullets and the invaders, and between invader-fired bullets and your ship. But how does the physics engine inform your game of these contacts?

The answer is to use SKPhysicsContactDelegate.