How To Make a Game Like Space Invaders with SpriteKit and Swift: Part 2

Learn how to make a game like Space Invaders using Apple’s built-in 2D game framework: Sprite Kit! By Ryan Ackermann.

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

Still in GameScene.swift, modify the class line to look like the following:

class GameScene: SKScene, SKPhysicsContactDelegate {

This declares your scene as a delegate for the physics engine. The didBegin(_:) 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, didBegin(_:) 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.

Add the following new property at the top of the class:

var contactQueue = [SKPhysicsContact]()

Now add the following code to the end of didMoveToView():

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 below touchesEnded() in the "Physics Contact Helpers" section:

func didBegin(_ contact: SKPhysicsContact) {
  contactQueue.append(contact)
}

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

Below didBegin(_:), add the following method:

func handle(_ contact: SKPhysicsContact) {
  //1
  // Ensure you haven't already handled this contact and removed its nodes
  if contact.bodyA.node?.parent == nil || contact.bodyB.node?.parent == nil {
    return
  }
  
  let nodeNames = [contact.bodyA.node!.name!, contact.bodyB.node!.name!]
  
  // 2
  if nodeNames.contains(kShipName) && nodeNames.contains(kInvaderFiredBulletName) {
    
    // 3
    // Invader bullet hit a ship
    run(SKAction.playSoundFileNamed("ShipHit.wav", waitForCompletion: false))
    
    contact.bodyA.node!.removeFromParent()
    contact.bodyB.node!.removeFromParent()
    
    
  } else if nodeNames.contains(InvaderType.name) && nodeNames.contains(kShipFiredBulletName) {
    
    // 4
    // Ship bullet hit an invader
    run(SKAction.playSoundFileNamed("InvaderHit.wav", waitForCompletion: false))
    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. Check to see if an invader bullet hits your ship.
  3. If an invader bullet hits your ship, remove your ship and the bullet from the scene and play a sound.
  4. 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 // Scene Update section:

func processContacts(forUpdate currentTime: CFTimeInterval) {
  for contact in contactQueue {
    handle(contact)
    
    if let index = contactQueue.index(of: contact) {
      contactQueue.remove(at: index)
    }
  }
}

The above code just drains the contact queue, calling handle(_:) for each contact in the queue and then remove the contact with the newly added method in the Array extension.

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

processContacts(forUpdate: currentTime)

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

space_invaders_fighting_back

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.

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 top of the class:

var score: Int = 0
var shipHealth: Float = 1.0

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

Now, replace the following line in setupHud():

healthLabel.text = String(format: "Health: %.1f%%", 100.0)

With this:

healthLabel.text = String(format: "Health: %.1f%%", shipHealth * 100.0)

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 below setupHud():

func adjustScore(by points: Int) {
  score += points
  
  if let score = childNode(withName: kScoreHudName) as? SKLabelNode {
    score.text = String(format: "Score: %04u", self.score)
  }
}

func adjustShipHealth(by healthAdjustment: Float) {
  // 1
  shipHealth = max(shipHealth + healthAdjustment, 0)
  
  if let health = childNode(withName: kHealthHudName) as? SKLabelNode {
    health.text = String(format: "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 handle(_:) with the following updated version:

func handle(_ contact: SKPhysicsContact) {
  // Ensure you haven't already handled this contact and removed its nodes
  if contact.bodyA.node?.parent == nil || contact.bodyB.node?.parent == nil {
    return
  }
  
  let nodeNames = [contact.bodyA.node!.name!, contact.bodyB.node!.name!]
  
  if nodeNames.contains(kShipName) && nodeNames.contains(kInvaderFiredBulletName) {
    // Invader bullet hit a ship
    run(SKAction.playSoundFileNamed("ShipHit.wav", waitForCompletion: false))
    
    // 1
    adjustShipHealth(by: -0.334)
    
    if shipHealth <= 0.0 {
      // 2
      contact.bodyA.node!.removeFromParent()
      contact.bodyB.node!.removeFromParent()
    } else {
      // 3
      if let ship = childNode(withName: kShipName) {
        ship.alpha = CGFloat(shipHealth)
        
        if contact.bodyA.node == ship {
          contact.bodyB.node!.removeFromParent()
          
        } else {
          contact.bodyA.node!.removeFromParent()
        }
      }
    }
    
  } else if nodeNames.contains(InvaderType.name) && nodeNames.contains(kShipFiredBulletName) {
    // Ship bullet hit an invader
    run(SKAction.playSoundFileNamed("InvaderHit.wav", waitForCompletion: false))
    contact.bodyA.node!.removeFromParent()
    contact.bodyB.node!.removeFromParent()
    
    // 4
    adjustScore(by: 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:

space_invaders_healthy