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

Update 10/11/16: This tutorial has been updated for Xcode 8 and Swift 3.

Welcome back to our 2-part Sprite Kit tutorial that teaches you how to make a game like Space Invaders!

In the first part, you created the foundation of the game. So far, you’ve added the invaders, your ship, and a Heads Up Display (HUD) to your game. You also coded the logic to make the invaders move automatically and to make your ship move as you tilted your device.

In this second and final part, you’ll add the ability for your ship and the aliens to fire on each other and blow each other up! You’ll also polish your game by adding sound effects and realistic images to replace the colored rectangles that currently serve as place holders for the invaders and your ship.

This tutorial picks up where the first part left off. If you don’t have the project already, you can download the example project where you left things off.

All right, it’s time to blow up some invaders!

Making Your Ship Fire its Laser Cannon

You will be detecting taps in the scene’s touchesEnded() method. The question is what should you do inside that method?

Taps can happen at any point during the gameplay. Contrast that with the way your scene changes: — at discrete intervals from within the update() method. So how can you save up taps detected at any time in touchesEnded() and process them later in update() when it’s invoked by the Sprite Kit game loop?

The answer is a queue! You’re going to use a simple Array to store your taps in a FIFO (First In First Out) queue.

Add the following property at the top of GameScene.swift in order to initializes the tap queue to an empty array:

var tapQueue = [Int]()

Next find the // User Tap Helpers comment and add the following method inside:

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  if let touch = touches.first {
    if (touch.tapCount == 1) {
      tapQueue.append(1)
    }
  }
}

The touchesEnded(_:with:) method itself is fairly simple. It just adds an entry to the queue. You don’t need a custom class to store the tap in the queue since all you need to know is that a tap occurred. Therefore, you use the integer 1 as a mnemonic for single tap.

Since you know that the invaders will also eventually fire bullets at your ship, add the following enum to the top of the class:

enum BulletType {
  case shipFired
  case invaderFired
}

You’re going to use BulletType to share the same bullet code for both invaders and your ship. It appears that you and the invaders shop at the same ammunition stores! :]

Next, add the following properties:

let kShipFiredBulletName = "shipFiredBullet"
let kInvaderFiredBulletName = "invaderFiredBullet"
let kBulletSize = CGSize(width:4, height: 8)

Now, add the following method at the end of the “Scene Setup and Content Creation” section:

func makeBullet(ofType bulletType: BulletType) -> SKNode {
  var bullet: SKNode
  
  switch bulletType {
  case .shipFired:
    bullet = SKSpriteNode(color: SKColor.green, size: kBulletSize)
    bullet.name = kShipFiredBulletName
  case .invaderFired:
    bullet = SKSpriteNode(color: SKColor.magenta, size: kBulletSize)
    bullet.name = kInvaderFiredBulletName
    break
  }
  
  return bullet
}

This method is relatively straightforward: it simply creates a rectangular colored sprite to represent a bullet and sets the name of the bullet so you can find it later in your scene.

Now, add the following methods inside the “Bullet Helpers” section:

func fireBullet(bullet: SKNode, toDestination destination: CGPoint, withDuration duration: CFTimeInterval, andSoundFileName soundName: String) {
  // 1
  let bulletAction = SKAction.sequence([
    SKAction.move(to: destination, duration: duration),
    SKAction.wait(forDuration: 3.0 / 60.0),
    SKAction.removeFromParent()
    ])
  
  // 2
  let soundAction = SKAction.playSoundFileNamed(soundName, waitForCompletion: true)
  
  // 3
  bullet.run(SKAction.group([bulletAction, soundAction]))
  
  // 4
  addChild(bullet)
}

func fireShipBullets() {
  let existingBullet = childNode(withName: kShipFiredBulletName)
  
  // 1
  if existingBullet == nil {
    if let ship = childNode(withName: kShipName) {
      let bullet = makeBullet(ofType: .shipFired)
      // 2
      bullet.position = CGPoint(
        x: ship.position.x,
        y: ship.position.y + ship.frame.size.height - bullet.frame.size.height / 2
      )
      // 3
      let bulletDestination = CGPoint(
        x: ship.position.x,
        y: frame.size.height + bullet.frame.size.height / 2
      )
      // 4
      fireBullet(
        bullet: bullet,
        toDestination: bulletDestination,
        withDuration: 1.0,
        andSoundFileName: "ShipBullet.wav"
      )
    }
  }
}

Going through the code in fireBullet(bullet:toDestination:withDuration:andSoundFileName:) step-by-step, you do the following:

  1. Create an SKAction that moves the bullet to the desired destination and then removes it from the scene. This sequence executes the individual actions consecutively — the next action only takes place after the previous action has completed. Hence the bullet is removed from the scene only after it has been moved.
  2. Play the desired sound to signal that the bullet was fired. All sounds are included in the starter project and iOS knows how to find and load them.
  3. Move the bullet and play the sound at the same time by putting them in the same group. A group runs its actions in parallel, not sequentially.
  4. Fire the bullet by adding it to the scene. This makes it appear onscreen and starts the actions.

Here’s what you do in fireShipBullets():

  1. Only fire a bullet if there isn’t one currently on-screen. It’s a laser cannon, not a laser machine gun — it takes time to reload!
  2. Set the bullet’s position so that it comes out of the top of the ship.
  3. Set the bullet’s destination to be just off the top of the screen. Since the x coordinate is the same as that of the bullet’s position, the bullet will fly straight up.
  4. Fire the bullet!

Your laser cannon is almost ready to fire!

Add the following to the “Scene Update Helpers” section:

func processUserTaps(forUpdate currentTime: CFTimeInterval) {
  // 1
  for tapCount in tapQueue {
    if tapCount == 1 {
      // 2
      fireShipBullets()
    }
    // 3
    tapQueue.remove(at: 0)
  }
}

Let’s review the above code:

  1. Loop over your tapQueue.
  2. If the queue entry is a single-tap, handle it. As the developer, you clearly know that you only handle single taps for now, but it’s best to be defensive against the possibility of double-taps (or other actions) later.
  3. Remove the tap from the queue.

Note: processUserTaps(forUpdate:) completely consumes the queue of taps at each invocation. Combined with the fact that fireShipBullets() will not fire another bullet if one is already onscreen, this emptying of the queue means that extra or rapid-fire taps will be ignored. Only the first tap needed to fire a bullet will matter.

Finally, add the following code as the first line in update():

processUserTaps(forUpdate: currentTime)

This invokes processUserTaps(forUpdate:) during the update loop and processes any user taps.

Build your game, run, and fire away!

space_invaders_first_shot