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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make a Game Like Space Invaders with SpriteKit and Swift: Part 2
35 mins
- Making Your Ship Fire its Laser Cannon
- Making Invaders Attack
- Detecting When Bullets Hit Their Target
- Implementing the Physics Contact Delegate Methods
- Updating Your Heads Up Display (HUD)
- Polishing Your Invader and Ship Images
- Implementing the End Game
- One Last Thing: Polish and Fidelity
- Where to Go From Here?
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:
- Loads a pair of sprite images — InvaderA_00.png and InvaderA_01.png — for each invader type and creates
SKTexture
objects from them. - Uses the first such texture as the sprite's base image.
- 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:
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:
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:
- Get a random invader in the scene (if one exists) - you'll use this later.
- Iterate through the invaders to check if any invaders are too low.
- 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. - 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:
- End your game only once. Otherwise, you'll try to display the game over scene multiple times and this would be a definite bug.
- Stop accelerometer updates.
- Show the
GameOverScene
. You can inspectGameOverScene.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:
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:
- Ignore bogus values — a value less than or equal to zero would mean infinitely fast or reverse movement, which doesn't make sense.
- Set the scene's
timePerMove
to the given value. This will speed up the movement of invaders withinmoveInvaders(forUpdate:)
. Record the ratio of the change so you can adjust the node's speed accordingly. - 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:
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.