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 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 section:

func fireInvaderBullets(forUpdate currentTime: CFTimeInterval) {
  let existingBullet = childNode(withName: kInvaderFiredBulletName)
  
  // 1
  if existingBullet == nil {
    var allInvaders = [SKNode]()
    
    // 2
    enumerateChildNodes(withName: InvaderType.name) { node, stop in
      allInvaders.append(node)
    }
    
    if allInvaders.count > 0 {
      // 3
      let allInvadersIndex = Int(arc4random_uniform(UInt32(allInvaders.count)))
      
      let invader = allInvaders[allInvadersIndex]
      
      // 4
      let bullet = makeBullet(ofType: .invaderFired)
      bullet.position = CGPoint(
        x: invader.position.x,
        y: invader.position.y - invader.frame.size.height / 2 + bullet.frame.size.height / 2
      )
      
      // 5
      let bulletDestination = CGPoint(x: invader.position.x, y: -(bullet.frame.size.height / 2))
      
      // 6
      fireBullet(
        bullet: bullet, 
        toDestination: bulletDestination, 
        withDuration: 2.0,
        andSoundFileName: "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():

fireInvaderBullets(forUpdate: currentTime)

This invocation of fireInvaderBullets(forUpdate:) 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:

space_invaders_firing_back

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.

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.

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.

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 new properties to GameScene:

let kInvaderCategory: UInt32 = 0x1 << 0
let kShipFiredBulletCategory: UInt32 = 0x1 << 1
let kShipCategory: UInt32 = 0x1 << 2
let kSceneEdgeCategory: UInt32 = 0x1 << 3
let kInvaderFiredBulletCategory: UInt32 = 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 UInt32. 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 after the line that creates the physics body:

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.

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 makeInvader(ofType:) right before the return invader line:

invader.physicsBody = SKPhysicsBody(rectangleOf: invader.frame.size)
invader.physicsBody!.isDynamic = false
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 inside the case .shipFired clause of makeBullet(ofType:):

bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.frame.size)
bullet.physicsBody!.isDynamic = true
bullet.physicsBody!.affectedByGravity = false
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 inside the case .invaderFired clause of makeBullet(ofType:):

bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.frame.size)
bullet.physicsBody!.isDynamic = true
bullet.physicsBody!.affectedByGravity = false
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.isDynamic = true. If not, Sprite Kit won't check for contact between these bullets and the static invaders as their definition is invader.physicsBody.isDynamic = false.

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.