Update 05/10/2016: Updated for Xcode 7.3 by Ryan Ackermann. Original tutorial by Joel Shapiro and previously updated by Riccardo d’Antoni.
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>, withEvent event: UIEvent?) { if let touch = touches.first { if (touch.tapCount == 1) { tapQueue.append(1) } } } |
The touchesEnded()
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 makeBulletOfType(bulletType: BulletType) -> SKNode { var bullet: SKNode switch bulletType { case .ShipFired: bullet = SKSpriteNode(color: SKColor.greenColor(), size: kBulletSize) bullet.name = kShipFiredBulletName case .InvaderFired: bullet = SKSpriteNode(color: SKColor.magentaColor(), 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.moveTo(destination, duration: duration), SKAction.waitForDuration(3.0 / 60.0), SKAction.removeFromParent() ]) // 2 let soundAction = SKAction.playSoundFileNamed(soundName, waitForCompletion: true) // 3 bullet.runAction(SKAction.group([bulletAction, soundAction])) // 4 addChild(bullet) } func fireShipBullets() { let existingBullet = childNodeWithName(kShipFiredBulletName) // 1 if existingBullet == nil { if let ship = childNodeWithName(kShipName) { let bullet = makeBulletOfType(.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, toDestination: bulletDestination, withDuration: 1.0, andSoundFileName: "ShipBullet.wav") } } } |
Going through the code in fireBullet()
step-by-step, you do the following:
- 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. - 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.
- 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.
- 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()
:
- 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!
- Set the bullet’s position so that it comes out of the top of the ship.
- 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.
- Fire the bullet!
Your laser cannon is almost ready to fire!
Add the following to the “Scene Update Helpers” section:
func processUserTapsForUpdate(currentTime: CFTimeInterval) { // 1 for tapCount in tapQueue { if tapCount == 1 { // 2 fireShipBullets() } // 3 tapQueue.removeAtIndex(0) } } |
Let’s review the above code:
- Loop over your
tapQueue
. - 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.
- Remove the tap from the queue.
Note: processUserTapsForUpdate()
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()
:
processUserTapsForUpdate(currentTime) |
This invokes processUserTapsForUpdate()
during the update loop and processes any user taps.
Build your game, run, and fire away!
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 fireInvaderBulletsForUpdate(currentTime: CFTimeInterval) { let existingBullet = childNodeWithName(kInvaderFiredBulletName) // 1 if existingBullet == nil { var allInvaders = Array<SKNode>() // 2 enumerateChildNodesWithName(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 = makeBulletOfType(.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, toDestination: bulletDestination, withDuration: 2.0, andSoundFileName: "InvaderBullet.wav") } } } |
The central logic for the above method is as follows:
- Only fire a bullet if one’s not already on-screen.
- Collect all the invaders currently on-screen.
- Select an invader at random.
- Create a bullet and fire it from just below the selected invader.
- The bullet should travel straight down and move just off the bottom of the screen.
- Fire off the invader’s bullet.
Add the following line to the end of update()
:
fireInvaderBulletsForUpdate(currentTime) |
This invocation of fireInvaderBulletsForUpdate()
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:
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:
- Set the ship's category.
- Don't detect contact between the ship and other physics bodies.
- 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 makeInvaderOfType()
right before the return invader
line:
invader.physicsBody = SKPhysicsBody(rectangleOfSize: invader.frame.size) invader.physicsBody!.dynamic = 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 makeBulletOfType()
:
bullet.physicsBody = SKPhysicsBody(rectangleOfSize: bullet.frame.size) bullet.physicsBody!.dynamic = 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 makeBulletOfType()
:
bullet.physicsBody = SKPhysicsBody(rectangleOfSize: bullet.frame.size) bullet.physicsBody!.dynamic = 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.dynamic = true
. If not, Sprite Kit won't check for contact between these bullets and the static invaders as their definition is invader.physicsBody.dynamic = 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
.
Implementing the Physics Contact Delegate Methods
Still in GameScene.swift, modify the class
line to look like the following:
class GameScene: SKScene, SKPhysicsContactDelegate { |
This declares your scene as a delegate for the physics engine. The didBeginContact()
method of SKPhysicsContactDelegate
executes each time two physics bodies make contact, based on how you set your physics bodies' categoryBitMask
and contactTestBitMask
. You'll implement didBeginContact
in just a moment.
Much like taps, contact can happen at any time. Consequently, didBeginContact
can execute at any time. But in keeping with your discrete time ticks, you should only process contact during those ticks when update
is called. So, just like taps, you'll create a queue to store contacts until they can be processed via update
.
Add the following new property at the top of the class:
var contactQueue = [SKPhysicsContact]() |
Now add the following code to the end of didMoveToView()
:
physicsWorld.contactDelegate = self |
This just initializes an empty contact queue and sets the scene as the contact delegate of the physics engine.
Next, add this method below touchesEnded()
in the "Physics Contact Helpers" section:
func didBeginContact(contact: SKPhysicsContact) { contactQueue.append(contact) } |
This method simply records the contact in your contact queue to handle later when update()
executes.
Below didBeginContact()
, add the following method:
func handleContact(contact: SKPhysicsContact) { //1 // Ensure you haven't already handled this contact and removed its nodes if contact.bodyA.node?.parent == nil || contact.bodyB.node?.parent == nil { return } let nodeNames = [contact.bodyA.node!.name!, contact.bodyB.node!.name!] // 2 if nodeNames.contains(kShipName) && nodeNames.contains(kInvaderFiredBulletName) { // 3 // Invader bullet hit a ship self.runAction(SKAction.playSoundFileNamed("ShipHit.wav", waitForCompletion: false)) contact.bodyA.node!.removeFromParent() contact.bodyB.node!.removeFromParent() } else if nodeNames.contains(InvaderType.name) && nodeNames.contains(kShipFiredBulletName) { // 4 // Ship bullet hit an invader self.runAction(SKAction.playSoundFileNamed("InvaderHit.wav", waitForCompletion: false)) contact.bodyA.node!.removeFromParent() contact.bodyB.node!.removeFromParent() } } |
This code is relatively straightforward, and explained below:
- Don't allow the same contact twice.
- Check to see if an invader bullet hits your ship.
- If an invader bullet hits your ship, remove your ship and the bullet from the scene and play a sound.
- If a ship bullet hits an invader, remove the invader and the bullet from the scene and play a different sound.
Add the following method to the // Scene Update
section:
func processContactsForUpdate(currentTime: CFTimeInterval) { for contact in contactQueue { handleContact(contact) if let index = contactQueue.indexOf(contact) { contactQueue.removeAtIndex(index) } } } |
The above code just drains the contact queue, calling handleContact for each contact in the queue and then remove the contact
with the newly added method in the Array extension.
Add the following line to the very top of update()
to call your queue handler:
processContactsForUpdate(currentTime) |
Build and run you app, and start firing at those invaders!
Now, when your ship’s bullet hits an invader, the invader disappears from the scene and an explosion sound plays. In contrast, when an invader’s bullet hits your ship, the code removes your ship from the scene and a different explosion sound plays.
Depending on your playing skill (or lack thereof!), you may have to run a few times to see both invaders and your ship get destroyed.
Updating Your Heads Up Display (HUD)
Your game looks good, but it’s lacking a certain something. There’s not much dramatic tension to your game. What’s the advantage of hitting an invader with your bullet if you don’t get credit? What’s the downside to being hit by an invader’s bullet if there’s no penalty?
You’ll rectify this by awarding score points for hitting invaders with your ship’s bullets, and by reducing your ship’s health when it gets hit by an invader’s bullet.
Add the following properties to the top of the class:
var score: Int = 0 var shipHealth: Float = 1.0 |
Your ship’s health starts at 100% but you will store it as a number ranging from 0 to 1. The above sets your ship’s initial health.
Now, replace the following line in setupHud()
:
healthLabel.text = String(format: "Health: %.1f%%", 100.0) |
With this:
healthLabel.text = String(format: "Health: %.1f%%", shipHealth * 100.0) |
The new line sets the initial HUD text based on your ship’s actual health value instead of a static value of 100.
Next, add the following two methods below setupHud()
:
func adjustScoreBy(points: Int) { score += points if let score = childNodeWithName(kScoreHudName) as? SKLabelNode { score.text = String(format: "Score: %04u", self.score) } } func adjustShipHealthBy(healthAdjustment: Float) { // 1 shipHealth = max(shipHealth + healthAdjustment, 0) if let health = childNodeWithName(kHealthHudName) as? SKLabelNode { health.text = String(format: "Health: %.1f%%", self.shipHealth * 100) } } |
These methods are fairly straightforward: update the score and the score label, and update the ship’s health and the health label. //1
merely ensures that the ship’s health doesn’t go negative.
The final step is to call these methods at the right time during gameplay. Replace handleContact()
with the following updated version:
func handleContact(contact: SKPhysicsContact) { // Ensure you haven't already handled this contact and removed its nodes if contact.bodyA.node?.parent == nil || contact.bodyB.node?.parent == nil { return } let nodeNames = [contact.bodyA.node!.name!, contact.bodyB.node!.name!] if nodeNames.contains(kShipName) && nodeNames.contains(kInvaderFiredBulletName) { // Invader bullet hit a ship runAction(SKAction.playSoundFileNamed("ShipHit.wav", waitForCompletion: false)) // 1 adjustShipHealthBy(-0.334) if shipHealth <= 0.0 { // 2 contact.bodyA.node!.removeFromParent() contact.bodyB.node!.removeFromParent() } else { // 3 if let ship = self.childNodeWithName(kShipName) { ship.alpha = CGFloat(shipHealth) if contact.bodyA.node == ship { contact.bodyB.node!.removeFromParent() } else { contact.bodyA.node!.removeFromParent() } } } } else if nodeNames.contains(InvaderType.name) && nodeNames.contains(kShipFiredBulletName) { // Ship bullet hit an invader runAction(SKAction.playSoundFileNamed("InvaderHit.wav", waitForCompletion: false)) contact.bodyA.node!.removeFromParent() contact.bodyB.node!.removeFromParent() // 4 adjustScoreBy(100) } } |
Here’s what’s changed in the method:
- Adjust the ship’s health when it gets hit by an invader’s bullet.
- If the ship’s health is zero, remove the ship and the invader’s bullet from the scene.
- If the ship’s health is greater than zero, only remove the invader’s bullet from the scene. Dim the ship’s sprite slightly to indicate damage.
- When an invader is hit, add 100 points to the score.
The above also explains why you store the ship’s health as a value between 0 and 1, even though your health starts at 100. Since alpha values range from 0 to 1, you can use the ship’s health value as the alpha value for for your ship to indicate progressive damage. That’s pretty handy!
Build and run your game again; you should see the score change when your bullets hit an invader; as well, you should see your ship’s health change when your ship is hit, as below:
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 loadInvaderTexturesOfType(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 makeInvaderOfType(invaderType: InvaderType) -> SKNode { let invaderTextures = loadInvaderTexturesOfType(invaderType) // 2 let invader = SKSpriteNode(texture: invaderTextures[0]) invader.name = InvaderType.name // 3 invader.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(invaderTextures, timePerFrame: timePerMove))) // invaders' bitmasks setup invader.physicsBody = SKPhysicsBody(rectangleOfSize: invader.frame.size) invader.physicsBody!.dynamic = 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 handleContact()
:
func isGameOver() -> Bool { // 1 let invader = childNodeWithName(InvaderType.name) // 2 var invaderTooLow = false enumerateChildNodesWithName(InvaderType.name) { node, stop in if (Float(CGRectGetMinY(node.frame)) <= self.kMinInvaderBottomHeight) { invaderTooLow = true stop.memory = true } } // 3 let ship = childNodeWithName(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.doorsOpenHorizontalWithDuration(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 moveInvadersForUpdate()
:
func adjustInvaderMovementToTimePerMove(newTimerPerMove: CFTimeInterval) { // 1 if newTimerPerMove <= 0 { return } // 2 let ratio: CGFloat = CGFloat(timePerMove / newTimerPerMove) timePerMove = newTimerPerMove // 3 enumerateChildNodesWithName(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 withinmoveInvadersForUpdate
. 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.adjustInvaderMovementToTimePerMove(self.timePerMove * 0.8) stop.memory = true } case .Left: //4 if (CGRectGetMinX(node.frame) <= 1.0) { proposedMovementDirection = .DownThenRight // Add the following line self.adjustInvaderMovementToTimePerMove(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.
Where to Go From Here?
Here is the final project from this Sprite Kit tutorial.
I encourage you to experiment with your SKInvaders game. Play with it, tweak it and see what you can do! Breaking your code, fixing it, then finally seeing an awesome new feature come to life is one of the many thrills of game development.
If you’d like to learn more, you should check out our book 2D iOS Games by Tutorials. The book teaches you everything you need to know to make 2D iOS games, by making a series of mini-games like this one, from an action game to a puzzle game to a tower defense game.
Enjoy playing your new game; I look forward to hearing from you with comments below or questions in the forums!
The post How To Make a Game Like Space Invaders with Sprite Kit and Swift Tutorial: Part 2 appeared first on Ray Wenderlich.