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

Polishing Your Invader and Ship Images

You've been incredibly patient working with these less-than-menacing red, green, blue and magenta rectangles. Keeping the visuals simple has worked well because it allowed you to focus ruthlessly on getting your game logic correct.

Now you'll add some actual image sprites to make your game much more realistic — and more fun to play!

Replace makeInvaderOfType() with the following two methods:

func loadInvaderTextures(ofType invaderType: InvaderType) -> [SKTexture] {
  
  var prefix: String
  
  switch(invaderType) {
  case .a:
    prefix = "InvaderA"
  case .b:
    prefix = "InvaderB"
  case .c:
    prefix = "InvaderC"
  }
  
  // 1
  return [SKTexture(imageNamed: String(format: "%@_00.png", prefix)),
          SKTexture(imageNamed: String(format: "%@_01.png", prefix))]
}

func makeInvader(ofType invaderType: InvaderType) -> SKNode {
  let invaderTextures = loadInvaderTextures(ofType: invaderType)
  
  // 2
  let invader = SKSpriteNode(texture: invaderTextures[0])
  invader.name = InvaderType.name
  
  // 3
  invader.run(SKAction.repeatForever(SKAction.animate(with: invaderTextures, timePerFrame: timePerMove)))
  
  // invaders' bitmasks setup
  invader.physicsBody = SKPhysicsBody(rectangleOf: invader.frame.size)
  invader.physicsBody!.isDynamic = false
  invader.physicsBody!.categoryBitMask = kInvaderCategory
  invader.physicsBody!.contactTestBitMask = 0x0
  invader.physicsBody!.collisionBitMask = 0x0
  
  return invader
}

Here's what the new code does:

  1. Loads a pair of sprite images — InvaderA_00.png and InvaderA_01.png — for each invader type and creates SKTexture objects from them.
  2. Uses the first such texture as the sprite's base image.
  3. Animates these two images in a continuous animation loop.

All of the images were included in the starter project and iOS knows how to find and load them, so there's nothing left to do here.

Build and run your app; you should see something similar to the screenshot below:

space_invaders_pretty_enemies

Looks pretty cool doesn't it? Next, you'll replace your blocky green ship with a much more retro and stylish looking version.

Replace this piece of code inside makeShip():

let ship = SKSpriteNode(color: SKColor.greenColor(), size: kShipSize)

With the following:

let ship = SKSpriteNode(imageNamed: "Ship.png")

Your ship sprite is now constructed from an image.

Build and run your game; you should see your official-looking ship appear as below:

space_invaders_pretty_player

Play your game for a while — what do you notice? Although you can blast happily away at the invaders, there's no clear victory or defeat. It's not much of a space war, is it?

Implementing the End Game

Think about how your game should end. What are the conditions that will lead to a game being over?

  • Your ship's health drops to zero.
  • You destroy all the invaders.
  • The invaders get too close to Earth.

You'll now add checks for each of the above conditions.

First, add the following new properties to the top of the class:

let kMinInvaderBottomHeight: Float = 32.0
var gameEnding: Bool = false

The above defines the height at which the invaders are considered to have invaded Earth, and a flag that indicates whether the game is over or not.

Now, add the following two methods below handle(_:):

func isGameOver() -> Bool {
  // 1
  let invader = childNode(withName: InvaderType.name)
  
  // 2
  var invaderTooLow = false
  
  enumerateChildNodes(withName: InvaderType.name) { node, stop in
    
    if (Float(node.frame.minY) <= self.kMinInvaderBottomHeight)   {
      invaderTooLow = true
      stop.pointee = true
    }
  }
  
  // 3
  let ship = childNode(withName: kShipName)
  
  // 4
  return invader == nil || invaderTooLow || ship == nil
}

func endGame() {
  // 1
  if !gameEnding {
    
    gameEnding = true
    
    // 2
    motionManager.stopAccelerometerUpdates()
    
    // 3
    let gameOverScene: GameOverScene = GameOverScene(size: size)
    
    view?.presentScene(gameOverScene, transition: SKTransition.doorsOpenHorizontal(withDuration: 1.0))
  }
}

Here's what's happening in the first method, which checks to see if the game is over:

  1. Get a random invader in the scene (if one exists) - you'll use this later.
  2. Iterate through the invaders to check if any invaders are too low.
  3. Get a pointer to your ship: if the ship's health drops to zero, then the player is considered dead and the player ship will be removed from the scene. In this case, you'd get a nil value indicating that there is no player ship.
  4. Return whether your game is over or not. If there are no more invaders, or an invader is too low, or your ship is destroyed, then the game is over.

The second method actually ends the game and displays the game over scene. Here's what the code does:

  1. End your game only once. Otherwise, you'll try to display the game over scene multiple times and this would be a definite bug.
  2. Stop accelerometer updates.
  3. Show the GameOverScene. You can inspect GameOverScene.swift for the details, but it's a basic scene with a simple "Game Over" message. The scene will start another game if you tap on it.

Add the following line as the first line of code in update():

if isGameOver() {
  endGame()
}

The above checks to see if the game is over every time the scene updates. If the game is over, then it displays the game over scene.

Build and run; blast away at the invaders until your game ends. Hopefully, you'll destroy all of the invaders before they destroy you! Once your game ends, you should see a screen similar to the following:

space_invaders_game_over

Tap the game over scene and you should be able to play again!

One Last Thing: Polish and Fidelity

If it's not fun to play with colored squares, it's not going to be fun to play with fancy art work, either! Nail down your gameplay and game logic first, then build out with fancy art assets and cool sound effects.

That being said, it's essential that you polish your game before releasing it to the App Store. The App Store is a crowded market and spit and polish will distinguish your app from the competition. Try to add little animations, storylines and a dash of cute factor that will delight your users. Also, consider being true to the game if you're remaking a classic.

If you're a fan of Space Invaders, you'll know that your remake is missing one important element. In the original game, the invaders march faster the closer they get to the bottom of the screen.

You'll update your game to incorporate this game mechanic as well to please the retro gaming purists out there.

Convert the instance constant let timePerMove: CFTimeInterval = 1.0 to variable var:

var timePerMove: CFTimeInterval = 1.0

Then add the following method below moveInvaders(forUpdate:):

func adjustInvaderMovement(to timePerMove: CFTimeInterval) {
  // 1
  if self.timePerMove <= 0 {
    return
  }
  
  // 2
  let ratio: CGFloat = CGFloat(self.timePerMove / timePerMove)
  self.timePerMove = timePerMove
  
  // 3
  enumerateChildNodes(withName: InvaderType.name) { node, stop in
    node.speed = node.speed * ratio
  }
}

Let's examine this code:

  1. Ignore bogus values — a value less than or equal to zero would mean infinitely fast or reverse movement, which doesn't make sense.
  2. Set the scene's timePerMove to the given value. This will speed up the movement of invaders within moveInvaders(forUpdate:). Record the ratio of the change so you can adjust the node's speed accordingly.
  3. Speed up the animation of invaders so that the animation cycles through its two frames more quickly. The ratio ensures that if the new time per move is 1/3 the old time per move, the new animation speed is 3 times the old animation speed. Setting the node's speed ensures that all of the node's actions run more quickly, including the action that animates between sprite frames.

Now, you need something to invoke this new method.

Modify determineInvaderMovementDirection() as indicated by comments below:

case .Right:
  //3
  if (CGRectGetMaxX(node.frame) >= node.scene!.size.width - 1.0) {
    proposedMovementDirection = .DownThenLeft
    
    // Add the following line
    self.adjustInvaderMovement(to: self.timePerMove * 0.8)
    
    stop.memory = true
  }
case .Left:
  //4
  if (CGRectGetMinX(node.frame) <= 1.0) {
    proposedMovementDirection = .DownThenRight
    
    // Add the following line
    self.adjustInvaderMovement(to: self.timePerMove * 0.8)
    
    stop.memory = true
  }

The new code simply reduces the time per move by 20% each time the invaders move down. This increases their speed by 25% (4/5 the move time means 5/4 the move speed).

Build and run your game, and watch the movement of the invaders; you should notice that those invaders move faster and faster as they get closer to the bottom of the screen:

space_invaders_end

This was a quick and easy code change that made your game that much more challenging and fun to play. If you're going to save the Earth from invading hordes, you might as well do it right! Spending time on seemingly minor tweaks like this is what makes the difference between a good game and a GREAT game.