How To Make a Game Like Space Invaders with SpriteKit and Swift: Part 1
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 1
30 mins
Adding the Heads Up Display (HUD)
It wouldn't be much fun to play Space Invaders if you didn't keep score, would it? You're going to add a heads-up display (or HUD) to your game. As a star pilot defending Earth, your performance is being monitored by your commanding officers. They're interested in both your "kills" (score) and "battle readiness" (health).
Add the following constants at the top of GameScene.swift, just below kShipName
:
let kScoreHudName = "scoreHud"
let kHealthHudName = "healthHud"
Now, add your HUD by inserting the following method right after makeShip()
:
func setupHud() {
// 1
let scoreLabel = SKLabelNode(fontNamed: "Courier")
scoreLabel.name = kScoreHudName
scoreLabel.fontSize = 25
// 2
scoreLabel.fontColor = SKColor.green
scoreLabel.text = String(format: "Score: %04u", 0)
// 3
scoreLabel.position = CGPoint(
x: frame.size.width / 2,
y: size.height - (40 + scoreLabel.frame.size.height/2)
)
addChild(scoreLabel)
// 4
let healthLabel = SKLabelNode(fontNamed: "Courier")
healthLabel.name = kHealthHudName
healthLabel.fontSize = 25
// 5
healthLabel.fontColor = SKColor.red
healthLabel.text = String(format: "Health: %.1f%%", 100.0)
// 6
healthLabel.position = CGPoint(
x: frame.size.width / 2,
y: size.height - (80 + healthLabel.frame.size.height/2)
)
addChild(healthLabel)
}
This is boilerplate code for creating and adding text labels to a scene. The relevant bits are as follows:
- Give the score label a name so you can find it later when you need to update the displayed score.
- Color the score label green.
- Position the score label.
- Give the health label a name so you can reference it later when you need to update the displayed health.
- Color the health label red; the red and green indicators are common colors for these indicators in games, and they're easy to differentiate in the middle of furious gameplay.
- Position the health below the score label.
Add the following line below setupShip()
in createContent()
to call the setup method for your HUD:
setupHud()
Build and run your app; you should see the HUD in all of its red and green glory on your screen as shown below:
Invaders? Check. Ship? Check. HUD? Check. Now all you need is a little dynamic action to tie it all together!
Adding Motion to the Invaders
To render your game onto the screen, Sprite Kit uses a game loop which searches endlessly for state changes that require on-screen elements to be updated. The game loop does several things, but you'll be interested in the mechanisms that update your scene. You do this by overriding the update()
method, which you'll find as a stub in your GameScene.swift file.
When your game is running smoothly and renders 60 frames-per-second (iOS devices are hardware-locked to a max of 60 fps), update()
will be called 60 times per second. This is where you modify the state of your scene, such as altering scores, removing dead invader sprites, or moving your ship around...
You'll use update()
to make your invaders move across and down the screen. Each time Sprite Kit invokes update()
, it's asking you "Did your scene change?", "Did your scene change?"... It's your job to answer that question — and you'll write some code to do just that.
Insert the following code at the top of GameScene.swift, just above the definition of the InvaderType
enum:
enum InvaderMovementDirection {
case right
case left
case downThenRight
case downThenLeft
case none
}
Invaders move in a fixed pattern: right, right, down, left, left, down, right, right, ... so you'll use the InvaderMovementDirection
type to track the invaders' progress through this pattern. For example, InvaderMovementDirection.right
means the invaders are in the right, right portion of their pattern.
Next, insert the following properties just below the existing property for contentCreated
:
// 1
var invaderMovementDirection: InvaderMovementDirection = .right
// 2
var timeOfLastMove: CFTimeInterval = 0.0
// 3
let timePerMove: CFTimeInterval = 1.0
This setup code initializes invader movement as follows:
- Invaders begin by moving to the right.
- Invaders haven't moved yet, so set the time to zero.
- Invaders take 1 second for each move. Each step left, right or down takes 1 second.
Now, you're ready to make the invaders move. Add the following code just below // Scene Update
:
func moveInvaders(forUpdate currentTime: CFTimeInterval) {
// 1
if (currentTime - timeOfLastMove < timePerMove) {
return
}
// 2
enumerateChildNodes(withName: InvaderType.name) { node, stop in
switch self.invaderMovementDirection {
case .right:
node.position = CGPoint(x: node.position.x + 10, y: node.position.y)
case .left:
node.position = CGPoint(x: node.position.x - 10, y: node.position.y)
case .downThenLeft, .downThenRight:
node.position = CGPoint(x: node.position.x, y: node.position.y - 10)
case .none:
break
}
// 3
self.timeOfLastMove = currentTime
}
}
Here's a breakdown of the code above, comment by comment:
- If it's not yet time to move, then exit the method.
moveInvaders(forUpdate:)
is invoked 60 times per second, but you don't want the invaders to move that often since the movement would be too fast for a normal person to see. - Recall that your scene holds all of the invaders as child nodes; you added them to the scene using
addChild()
insetupInvaders()
identifying each invader by its name property. InvokingenumerateChildNodes(withName:using:)
only loops over the invaders because they're namedkInvaderName
; this makes the loop skip your ship and the HUDs. The guts of the block moves the invaders 10 pixels either right, left or down depending on the value ofinvaderMovementDirection
. - Record that you just moved the invaders, so that the next time this method is invoked (1/60th of a second from now), the invaders won't move again till the set time period of one second has elapsed.
To make your invaders move, add the following to update()
:
moveInvaders(forUpdate: currentTime)
Build and run your app; you should see your invaders slowly walk their way to the right:
Hmmm, what happened? Why did the invaders disappear? Maybe the invaders aren't as menacing as you thought!
The invaders don't yet know that they need to move down and change their direction once they hit the side of the playing field. Guess you'll need to help those invaders find their way!
Controlling the Invaders' Direction
Adding the following code just after // Invader Movement Helpers
:
func determineInvaderMovementDirection() {
// 1
var proposedMovementDirection: InvaderMovementDirection = invaderMovementDirection
// 2
enumerateChildNodes(withName: InvaderType.name) { node, stop in
switch self.invaderMovementDirection {
case .right:
//3
if (node.frame.maxX >= node.scene!.size.width - 1.0) {
proposedMovementDirection = .downThenLeft
stop.pointee = true
}
case .left:
//4
if (node.frame.minX <= 1.0) {
proposedMovementDirection = .downThenRight
stop.pointee = true
}
case .downThenLeft:
proposedMovementDirection = .left
stop.pointee = true
case .downThenRight:
proposedMovementDirection = .right
stop.pointee = true
default:
break
}
}
//7
if (proposedMovementDirection != invaderMovementDirection) {
invaderMovementDirection = proposedMovementDirection
}
}
Here's what's going on in the above code:
- Here you keep a reference to the current
invaderMovementDirection
so that you can modify it in//2
. - Loop over all the invaders in the scene and invoke the block with the invader as an argument.
- If the invader's right edge is within 1 point of the right edge of the scene, it's about to move offscreen. Set
proposedMovementDirection
so that the invaders move down then left. You compare the invader's frame (the frame that contains its content in the scene's coordinate system) with the scene width. Since the scene has ananchorPoint
of (0, 0) by default, and is scaled to fill its parent view, this comparison ensures you're testing against the view's edges. - If the invader's left edge is within 1 point of the left edge of the scene, it's about to move offscreen. Set
proposedMovementDirection
so that invaders move down then right. - If invaders are moving down then left, they've already moved down at this point, so they should now move left. How this works will become more obvious when you integrate
determineInvaderMovementDirection
withmoveInvadersForUpdate()
. - If the invaders are moving down then right, they've already moved down at this point, so they should now move right.
- If the proposed invader movement direction is different than the current invader movement direction, update the current direction to the proposed direction.
Add the following code within moveInvaders(forUpdate:)
, immediately after the conditional check of timeOfLastMove
:
determineInvaderMovementDirection()
Why is it important that you add the invocation of determineInvaderMovementDirection()
only after the check on timeOfLastMove
? That's because you want the invader movement direction to change only when the invaders are actually moving. Invaders only move when the check on timeOfLastMove
passes — i.e., the conditional expression is true.
What would happen if you added the new line of code above as the very first line of code in moveInvaders(forUpdate:)
? If you did that, then there would be two bugs:
- You'd be trying to update the movement direction way too often -- 60 times per second -- when you know it can only change at most once per second.
- The invaders would never move down, as the state transition from
downThenLeft
toleft
would occur without an invader movement in between. The next invocation ofmoveInvaders(forUpdate:)
that passed the check ontimeOfLastMove
would be executed withleft
and would keep moving the invaders left, skipping the down move. A similar bug would exist fordownThenRight
andright
.
Build and run your app; you'll see the invaders moving as expected across and down the screen:
Note: You might have noticed that the invaders' movement is jerky. That's a consequence of your code only moving invaders once per second — and moving them a decent distance at that. But the movement in the original game was jerky, so keeping this feature helps your game seem more authentic.
Note: You might have noticed that the invaders' movement is jerky. That's a consequence of your code only moving invaders once per second — and moving them a decent distance at that. But the movement in the original game was jerky, so keeping this feature helps your game seem more authentic.