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

Implementing the Physics Contact Delegate Methods

Open GameScene.h and modify the @interface line to look like the following:

@interface GameScene : SKScene <SKPhysicsContactDelegate>

This declares your scene as a delegate for the physics engine. The didBeginContact: method of SKPhysicsContactDelegate executes each time two physics bodies make contact, based on how you set your physics bodies' categoryBitMask and contactTestBitMask. You'll implement didBeginContact: in just a moment.

Much like taps, contact can happen at any time. Consequently, didBeginContact: can execute at any time. But in keeping with your discrete time ticks, you should only process contact during those ticks when update: is called. So, just like taps, you'll create a queue to store contacts until they can be processed via update:.

Switch back to GameScene.m and add the following new property to the class extension at the top:

@property (strong) NSMutableArray* contactQueue;

Now add the following code to the end of didMoveToView:, right after the self.userInteractionEnabled = YES; line:

self.contactQueue = [NSMutableArray array];
self.physicsWorld.contactDelegate = self;

This just initializes an empty contact queue and sets the scene as the contact delegate of the physics engine.

Next, add this method to the #pragma mark - Physics Contact Helpers section:

-(void)didBeginContact:(SKPhysicsContact *)contact {
    [self.contactQueue addObject:contact];
}

This method simply records the contact in your contact queue to handle later when update: executes.

Still in the same section, add the following method:

-(void)handleContact:(SKPhysicsContact*)contact {
    //1
    // Ensure you haven't already handled this contact and removed its nodes
    if (!contact.bodyA.node.parent || !contact.bodyB.node.parent) return;

    NSArray* nodeNames = @[contact.bodyA.node.name, contact.bodyB.node.name];
    if ([nodeNames containsObject:kShipName] && [nodeNames containsObject:kInvaderFiredBulletName]) {
        //2
        // Invader bullet hit a ship
        [self runAction:[SKAction playSoundFileNamed:@"ShipHit.wav" waitForCompletion:NO]];
        [contact.bodyA.node removeFromParent];
        [contact.bodyB.node removeFromParent];
    } else if ([nodeNames containsObject:kInvaderName] && [nodeNames containsObject:kShipFiredBulletName]) {
        //3
        // Ship bullet hit an invader
        [self runAction:[SKAction playSoundFileNamed:@"InvaderHit.wav" waitForCompletion:NO]];
        [contact.bodyA.node removeFromParent];
        [contact.bodyB.node removeFromParent];
    }
}

This code is relatively straightforward, and explained below:

  1. Don't allow the same contact twice.
  2. If an invader bullet hits your ship, remove your ship and the bullet from the scene and play a sound.
  3. If a ship bullet hits an invader, remove the invader and the bullet from the scene and play a different sound.

Add the following method to the #pragma mark - Scene Update Helpers section::

-(void)processContactsForUpdate:(NSTimeInterval)currentTime {
    for (SKPhysicsContact* contact in [self.contactQueue copy]) {
        [self handleContact:contact];
        [self.contactQueue removeObject:contact];
    }
}

The above just drains the contact queue, calling handleContact: for each contact in the queue.

Add the following line to the very top of update: to call your queue handler:

[self processContactsForUpdate:currentTime];

Build and run you app, and start firing at those invaders!

Exchange Fire

Now, when your ship's bullet hits an invader, the invader disappears from the scene and an explosion sound plays. In contrast, when an invader's bullet hits your ship, the code removes your ship from the scene and a different explosion sound plays.

Depending on your playing skill (or lack thereof!), you may have to run a few times to see both invaders and your ship get destroyed. Just hit Command R to run again.

Updating Your Heads Up Display (HUD)

Your game looks good, but it's lacking a certain something. There's not much dramatic tension to your game. What's the advantage of hitting an invader with your bullet if you don't get credit? What's the downside to being hit by an invader's bullet if there's no penalty?

You'll rectify this by awarding score points for hitting invaders with your ship's bullets, and by reducing your ship's health when it gets hit by an invader's bullet.

Add the following properties to the class extension:

@property NSUInteger score;
@property CGFloat shipHealth;

Your ship's health starts at 100% but you will store it as a number ranging from 0 to 1.

Add the following line to setupShip as the last line in the method:

self.shipHealth = 1.0f;

The above sets your ship's initial health.

Now, replace the following line in setupHud:

healthLabel.text = [NSString stringWithFormat:@"Health: %.1f%%", 100.0f];

With this:

healthLabel.text = [NSString stringWithFormat:@"Health: %.1f%%", self.shipHealth * 100.0f];

The new line sets the initial HUD text based on your ship's actual health value instead of a static value of 100.

Next, add the following two methods to the #pragma mark - HUD Helpers section:

-(void)adjustScoreBy:(NSUInteger)points {
    self.score += points;
    SKLabelNode* score = (SKLabelNode*)[self childNodeWithName:kScoreHudName];
    score.text = [NSString stringWithFormat:@"Score: %04u", self.score];
}

-(void)adjustShipHealthBy:(CGFloat)healthAdjustment {
    //1
    self.shipHealth = MAX(self.shipHealth + healthAdjustment, 0);

    SKLabelNode* health = (SKLabelNode*)[self childNodeWithName:kHealthHudName];
    health.text = [NSString stringWithFormat:@"Health: %.1f%%", self.shipHealth * 100];
}

These methods are fairly straightforward: update the score and the score label, and update the ship's health and the health label. //1 merely ensures that the ship's health doesn't go negative.

The final step is to call these methods at the right time during gameplay. Replace handleContact: with the following updated version:

-(void)handleContact:(SKPhysicsContact*)contact {
    // Ensure you haven't already handled this contact and removed its nodes
    if (!contact.bodyA.node.parent || !contact.bodyB.node.parent) return;

    NSArray* nodeNames = @[contact.bodyA.node.name, contact.bodyB.node.name];
    if ([nodeNames containsObject:kShipName] && [nodeNames containsObject:kInvaderFiredBulletName]) {
        // Invader bullet hit a ship
        [self runAction:[SKAction playSoundFileNamed:@"ShipHit.wav" waitForCompletion:NO]];
        //1
        [self adjustShipHealthBy:-0.334f];
        if (self.shipHealth <= 0.0f) {
            //2
            [contact.bodyA.node removeFromParent];
            [contact.bodyB.node removeFromParent];
        } else {
            //3
            SKNode* ship = [self childNodeWithName:kShipName];
            ship.alpha = self.shipHealth;
            if (contact.bodyA.node == ship) [contact.bodyB.node removeFromParent];
            else [contact.bodyA.node removeFromParent];
        }
    } else if ([nodeNames containsObject:kInvaderName] && [nodeNames containsObject:kShipFiredBulletName]) {
        // Ship bullet hit an invader
        [self runAction:[SKAction playSoundFileNamed:@"InvaderHit.wav" waitForCompletion:NO]];
        [contact.bodyA.node removeFromParent];
        [contact.bodyB.node removeFromParent];
        //4
        [self adjustScoreBy:100];
    }
}

Here's what's changed in the method:

  1. Adjust the ship's health when it gets hit by an invader's bullet.
  2. If the ship's health is zero, remove the ship and the invader's bullet from the scene.
  3. If the ship's health is greater than zero, only remove the invader's bullet from the scene. Dim the ship's sprite slightly to indicate damage.
  4. When an invader is hit, add 100 points to the score.

The above also explains why you store the ship's health as a value between 0 and 1, even though your health starts at 100. Since alpha values range from 0 to 1, you can use the ship's health value as the alpha value for for your ship to indicate progressive damage. That's pretty handy!

Build and run your game again; you should see the score change when your bullets hit an invader; as well, you should see your ship's health change when your ship is hit, as below:

Scores Updating