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.
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
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
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:
- 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.
- 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.
- Set the category bit mask to be the
monsterCategory
you defined earlier. -
contactTestBitMask
indicates what categories of objects this object should notify the contact listener when they intersect. You choose projectiles here. -
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?"]
- 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.
- 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 contactTestBitMask
s are set appropriately.
There are two parts to this method:
- 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.
- 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!
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!
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:
- Set the background color to white, same as you did for the main scene.
- Based on the
won
parameter, the message is set to either "You Won" or "You Lose". - 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.
- 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. - 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:)
onself.view
. - 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 afatalError(_:)
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!