SpriteKit Tutorial for Beginners

In this SpriteKit tutorial, you will learn how to create a simple 2D game using SpriteKit, Apple’s 2D game framework, while writing in Swift 4! By Brody Eller.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Collision Detection and Physics: Implementation

Start by adding this struct to the top of GameScene.swift:

struct PhysicsCategory {
  static let none      : UInt32 = 0
  static let all       : UInt32 = UInt32.max
  static let monster   : UInt32 = 0b1       // 1
  static let projectile: UInt32 = 0b10      // 2
}

This code sets up the constants for the physics categories you'll need in a bit — no pun intended! :]

Note: You may be wondering what the fancy syntax is here. The category on SpriteKit is just a single 32-bit integer, acting as a bitmask. This is a fancy way of saying each of the 32-bits in the integer represents a single category (and hence you can have 32 categories max). Here you're setting the first bit to indicate a monster, the next bit over to represent a projectile, and so on.

Next, create an extension at the end of GameScene.swift implementing the SKPhysicsContactDelegate protocol:

extension GameScene: SKPhysicsContactDelegate {

}

Then inside didMove(to:) add these lines after adding the player to the scene:

physicsWorld.gravity =.zero
physicsWorld.contactDelegate = self

This sets up the physics world to have no gravity, and sets the scene as the delegate to be notified when two physics bodies collide.

Inside addMonster(), add these lines right after creating the monster sprite:

monster.physicsBody = SKPhysicsBody(rectangleOf: monster.size) // 1
monster.physicsBody?.isDynamic = true // 2
monster.physicsBody?.categoryBitMask = PhysicsCategory.monster // 3
monster.physicsBody?.contactTestBitMask = PhysicsCategory.projectile // 4
monster.physicsBody?.collisionBitMask = PhysicsCategory.none // 5

Here's what this does:

  1. Create a physics body for the sprite. In this case, the body is defined as a rectangle of the same size as the sprite, since that's a decent approximation for the monster.
  2. Set the sprite to be dynamic. This means that the physics engine will not control the movement of the monster. You will through the code you've already written, using move actions.
  3. Set the category bit mask to be the monsterCategory you defined earlier.
  4. contactTestBitMask indicates what categories of objects this object should notify the contact listener when they intersect. You choose projectiles here.
  5. collisionBitMask indicates what categories of objects this object that the physics engine handle contact responses to (i.e. bounce off of). You don't want the monster and projectile to bounce off each other — it's OK for them to go right through each other in this game — so you set this to .none.

Next add some similar code to touchesEnded(_:with:), right after the line setting the projectile's position:

projectile.physicsBody = SKPhysicsBody(circleOfRadius: projectile.size.width/2)
projectile.physicsBody?.isDynamic = true
projectile.physicsBody?.categoryBitMask = PhysicsCategory.projectile
projectile.physicsBody?.contactTestBitMask = PhysicsCategory.monster
projectile.physicsBody?.collisionBitMask = PhysicsCategory.none
projectile.physicsBody?.usesPreciseCollisionDetection = true

As a test, see if you can understand each line here and what it does. If not, just refer back to the points explained above!

As a second test, see if you can spot two differences. Answer below!

[spoiler title="What Are the Differences?"]

  1. You're using a circle shaped body instead of a rectangle body. Since the projectile is a nice circle, this makes for a better match.
  2. You also set usesPreciseCollisionDetection to true. This is important to set for fast moving bodies like projectiles, because otherwise there is a chance that two fast moving bodies can pass through each other without a collision being detected.

[/spoiler]

Next, add a method that will be called when the projectile collides with the monster before the closing curly brace of GameScene. Nothing calls this automatically; you will be calling this later.

func projectileDidCollideWithMonster(projectile: SKSpriteNode, monster: SKSpriteNode) {
  print("Hit")
  projectile.removeFromParent()
  monster.removeFromParent()
}

All you do here is remove the projectile and monster from the scene when they collide. Pretty simple, eh?

Now it's time to implement the contact delegate method. Add the following new method to the extension you made earlier:

func didBegin(_ contact: SKPhysicsContact) {
  // 1
  var firstBody: SKPhysicsBody
  var secondBody: SKPhysicsBody
  if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
    firstBody = contact.bodyA
    secondBody = contact.bodyB
  } else {
    firstBody = contact.bodyB
    secondBody = contact.bodyA
  }
 
  // 2
  if ((firstBody.categoryBitMask & PhysicsCategory.monster != 0) &&
      (secondBody.categoryBitMask & PhysicsCategory.projectile != 0)) {
    if let monster = firstBody.node as? SKSpriteNode,
      let projectile = secondBody.node as? SKSpriteNode {
      projectileDidCollideWithMonster(projectile: projectile, monster: monster)
    }
  }
}

Since you set the scene as the physics world's contactDelegate earlier, this method will be called whenever two physics bodies collide and their contactTestBitMasks are set appropriately.

There are two parts to this method:

  1. This method passes you the two bodies that collide, but does not guarantee that they are passed in any particular order. So this bit of code just arranges them so they are sorted by their category bit masks so you can make some assumptions later.
  2. Here is the check to see if the two bodies that collided are the projectile and monster, and if so, the method you wrote earlier is called.

Build and run, and now when your projectiles intersect targets they should disappear!

SpriteKit Tutorial

Finishing Touches

You’re pretty close to having an extremely simple but workable game now. You just need to add some sound effects and music — what kind of game doesn’t have sound? — and some simple game logic.

The resources in the project for this tutorial already have some cool background music and an awesome pew-pew sound effect. You just need to play them!

To do this, add these line to the end of didMove(to:):

let backgroundMusic = SKAudioNode(fileNamed: "background-music-aac.caf")
backgroundMusic.autoplayLooped = true
addChild(backgroundMusic)

This uses SKAudioNode to play and loop the background music for your game.

As for the sound effect, add this line after the guard statement in touchesEnded(_:withEvent:):

run(SKAction.playSoundFileNamed("pew-pew-lei.caf", waitForCompletion: false))

Pretty handy, eh? You can play a sound effect with one line!

Build and run, and enjoy your groovy tunes!

Note: If you don't hear the background music, try running on a device instead of the simulator.

Game Over, Man!

Now, create a new scene that will serve as your “You Win” or “You Lose” indicator. Create a new file with the iOS\Source\Swift File template, name the file GameOverScene and click Create.

Add the following to GameOverScene.swift:

import SpriteKit

class GameOverScene: SKScene {
  init(size: CGSize, won:Bool) {
    super.init(size: size)
    
    // 1
    backgroundColor = SKColor.white
    
    // 2
    let message = won ? "You Won!" : "You Lose :["
    
    // 3
    let label = SKLabelNode(fontNamed: "Chalkduster")
    label.text = message
    label.fontSize = 40
    label.fontColor = SKColor.black
    label.position = CGPoint(x: size.width/2, y: size.height/2)
    addChild(label)
    
    // 4
    run(SKAction.sequence([
      SKAction.wait(forDuration: 3.0),
      SKAction.run() { [weak self] in
        // 5
        guard let `self` = self else { return }
        let reveal = SKTransition.flipHorizontal(withDuration: 0.5)
        let scene = GameScene(size: size)
        self.view?.presentScene(scene, transition:reveal)
      }
      ]))
   }
  
  // 6
  required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

There are six parts to point out here:

  1. Set the background color to white, same as you did for the main scene.
  2. Based on the won parameter, the message is set to either "You Won" or "You Lose".
  3. This is how you display a label of text on the screen with SpriteKit. As you can see, it's pretty easy. You just choose your font and set a few parameters.
  4. Finally, this sets up and runs a sequence of two actions. First it waits for 3 seconds, then it uses the run() action to run some arbitrary code.
  5. This is how you transition to a new scene in SpriteKit. You can pick from a variety of different animated transitions for how you want the scenes to display. Here you've chosen a flip transition that takes 0.5 seconds. Then you create the scene you want to display, and use presentScene(_:transition:) on self.view.
  6. If you override an initializer on a scene, you must implement the required init(coder:) initializer as well. However this initializer will never be called, so you just add a dummy implementation with a fatalError(_:) for now.

So far so good! Now you just need to set up your main scene to load the game over scene when appropriate.

Switch back to GameScene.swift, and inside addMonster(), replace monster.run(SKAction.sequence([actionMove, actionMoveDone])) with the following:

let loseAction = SKAction.run() { [weak self] in
  guard let `self` = self else { return }
  let reveal = SKTransition.flipHorizontal(withDuration: 0.5)
  let gameOverScene = GameOverScene(size: self.size, won: false)
  self.view?.presentScene(gameOverScene, transition: reveal)
}
monster.run(SKAction.sequence([actionMove, loseAction, actionMoveDone]))

This creates a new "lose action" that displays the game over scene when a monster goes off-screen. See if you understand each line here, if not refer to the explanation for the previous code block.

Now you should handle the win case too; don't be cruel to your players! :] Add a new property to the top of GameScene, right after the declaration of player:

var monstersDestroyed = 0

And add this to the bottom of projectileDidCollideWithMonster(projectile:monster:):

monstersDestroyed += 1
if monstersDestroyed > 30 {
  let reveal = SKTransition.flipHorizontal(withDuration: 0.5)
  let gameOverScene = GameOverScene(size: self.size, won: true)
  view?.presentScene(gameOverScene, transition: reveal)
}

Here you keep track of how many monsters the player destroys. If the player successfully destroys more than 30 monsters, the game ends and the player wins the game!

Build and run. You should now have win and lose conditions and see a game over scene when appropriate!

SpriteKit Tutorial

Brody Eller

Contributors

Brody Eller

Author

Felicity Johnson

Tech Editor

Essan Parto

Final Pass Editor

Richard Critz

Team Lead

Over 300 content creators. Join our team.