Like Batman and Robin or Superman and Lois Lane, SpriteKit and Swift are an amazing combination:
- SpriteKit is one of the best ways to make games on iOS. It’s easy to learn, powerful, and is fully supported by Apple.
- Swift is an easy language to get started with, especially if you are a beginner to the iOS platform.
In this tutorial, you will learn how to create a simple 2D game using Apple’s 2D game framework, SpriteKit — using Swift!
You can either follow along with this SpriteKit tutorial, or just jump straight to the sample project at the end. And yes. There will be ninjas.
SpriteKit vs. Unity
The most popular alternative to SpriteKit at the moment is a game framework called Unity. Unity was originally developed as a 3D engine, but it has full built-in 2D support, too.
Before you get started, put some thought into whether SpriteKit or Unity is the best choice for your game.
Advantages of SpriteKit
- It’s built right into iOS. There is no need to download extra libraries or have external dependencies. You can also seamlessly use other iOS APIs like iAd, In-App Purchases, etc. without having to rely on extra plugins.
- It leverages your existing skills. If you already know Swift and iOS development, you can pick up SpriteKit extremely quickly.
- It’s written by Apple. This gives you some confidence that it will be well supported moving forward on all of Apple’s new products. For example, you can use the same SpriteKit code to make your game work on iOS, macOS, and tvOS without a hitch.
- It’s free. Maybe one of the best reasons for small indies! You get all of SpriteKit’s functionality at no cost. Unity does have a free version but it doesn’t have all of the features of the Pro version. You’ll need to upgrade if you want to avoid the Unity splash screen, for example.
Advantages of Unity
- Cross-platform. This is one of the big ones. If you use SpriteKit, you’re locked into the Apple ecosystem. With Unity, you can easily port your games to Android, Windows, and more.
- Visual scene designer. Unity makes it extremely easy to lay out your levels and test your game in realtime with the click of a button. SpriteKit does have a scene editor, but it is very basic compared to what Unity offers.
- Asset store. Unity comes with a built-in asset store where you can buy various components for your game. Some of these components can save you a good bit of development time!
- More powerful. In general, Unity just has more features and functionality than the SpriteKit/Scene Kit combination.
Which Should I Choose?
After this you may be thinking, “Well, which 2D framework should I choose?”
The answer depends on what your goals are:
- If you’re a complete beginner, or solely focused on the Apple ecosystem: Use SpriteKit — it’s built-in, easy to learn, and will get the job done.
- If you want to be cross-platform, or have a more complicated game: Use Unity — it’s more powerful and flexible.
If you think Unity is for you, check out our book Unity Games by Tutorials.
Otherwise, keep reading on to get started with this SpriteKit tutorial!
Getting Started
Start by downloading the project files at the top or bottom of this page. Open the starter project and navigate to GameViewController.swift. Add the following at the end of viewDidLoad()
:
let scene = GameScene(size: view.bounds.size)
let skView = view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.ignoresSiblingOrder = true
scene.scaleMode = .resizeFill
skView.presentScene(scene)
GameViewController
is a normal UIViewController
, except that its root view is an SKView
, which is a view that contains a SpriteKit scene.
Here, you’ve instructed viewDidLoad()
to create a new instance of the GameScene
on startup, with the same size as the view itself. Setting skView.showsFPS
to true means a frame rate indicator will be displayed.
That’s it for the initial setup; now it’s time to get something on the screen!
Adding a Sprite
Open GameScene.swift and add with the following to GameScene
:
// 1
let player = SKSpriteNode(imageNamed: "player")
override func didMove(to view: SKView) {
// 2
backgroundColor = SKColor.white
// 3
player.position = CGPoint(x: size.width * 0.1, y: size.height * 0.5)
// 4
addChild(player)
}
Here’s what this does, step by step:
- Here you declare a private constant for the player (i.e. the ninja), which is an example of a sprite. As you can see, creating a sprite is easy — simply pass in the name of the image to use.
- Setting the background color of a scene in SpriteKit is as simple as setting the backgroundColor property. Here you set it to white.
- You position the sprite to be 10% across horizontally, and centered vertically.
- To make the sprite appear on the scene, you must add it as a child of the scene.
Build and run, and voilà — ladies and gentlemen, the ninja has entered the building!
Moving Monsters
Next you want to add some monsters into your scene for your ninja to combat. To make things more interesting, you want the monsters to be moving — otherwise there wouldn’t be much of a challenge! You’ll create the monsters slightly off screen to the right and set up an action that tells them to move to the left.
Add the following methods to GameScene.swift, inside the class, before the closing curly brace:
func random() -> CGFloat {
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(min: CGFloat, max: CGFloat) -> CGFloat {
return random() * (max - min) + min
}
func addMonster() {
// Create sprite
let monster = SKSpriteNode(imageNamed: "monster")
// Determine where to spawn the monster along the Y axis
let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)
// Position the monster slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)
// Add the monster to the scene
addChild(monster)
// Determine speed of the monster
let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))
// Create the actions
let actionMove = SKAction.move(to: CGPoint(x: -monster.size.width/2, y: actualY),
duration: TimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
monster.run(SKAction.sequence([actionMove, actionMoveDone]))
}
The first part of addMonster()
should make sense based on what you’ve learned so far: you do some simple calculations to determine where you want to create the object, set the position of the object, and add it to the scene the same way you did for the player sprite.
The new element here is adding actions. SpriteKit provides a lot of extremely useful built-in actions that help you easily change the state of sprites over time, such as move actions, rotate actions, fade actions, animation actions, and more. Here you use three actions on the monster:
- SKAction.move(to:duration:): You use this action to make the object move off-screen to the left. You can specify how long the movement should take, and here you vary the duration randomly from 2-4 seconds.
- SKAction.removeFromParent(): SpriteKit comes with a helpful action that removes a node from its parent, effectively deleting it from the scene. Here you use this action to remove the monster from the scene when it is no longer visible. This is important because otherwise you would have an endless supply of monsters and would eventually consume all device resources.
- SKAction.sequence(_:): The sequence action allows you to chain together a sequence of actions that are performed in order, one at a time. This way, you can have the “move to” action performed first, and once it is complete, you perform the “remove from parent” action.
arc4random()
. This suffices for the simple random number generation needs in this game, but if you want more advanced functionality, check out the random number APIs in GameplayKit.One last thing before you move on. You need to actually call the method to create monsters! And to make things fun, let’s have monsters continuously spawning over time.
Simply add the following code to the end of didMove(to:)
:
run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run(addMonster),
SKAction.wait(forDuration: 1.0)
])
))
Here you run a sequence of actions to call a block of code, and then wait for 1 second. You repeat this sequence of actions endlessly.
That’s it! Build and run the project; now you should see monsters happily moving across the screen:
Shooting Projectiles
At this point, the ninja is just begging for some action — so it’s time to add shooting! There are many ways you could implement shooting, but for this game you’re going to make it so that when the user taps the screen, a projectile is shot from the player in the direction of the tap.
You’ll want to use a “move to” action to implement this, but in order to use this you have to do a little math. The “move to” action requires you to give a destination for the projectile, but you can’t just use the touch point because the touch point represents the direction to shoot relative to the player. You actually want to keep the projectile moving through the touch point until it goes off-screen.
Here’s a picture that illustrates the matter:
As you can see, you have a small triangle created by the x and y offset from the origin point to the touch point. You just need to make a big triangle with the same ratio — and you know you want one of the endpoints to be off the screen.
To run these calculations, it really helps if you have some basic vector math routines you can call (like methods to add and subtract vectors). However, SpriteKit doesn’t have any by default so you’ll have to write your own.
Luckily they are very easy to write thanks to the power of Swift operator overloading. Add these functions to the top of your file, right before the GameScene
class:
func +(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
func -(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
func *(point: CGPoint, scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x * scalar, y: point.y * scalar)
}
func /(point: CGPoint, scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x / scalar, y: point.y / scalar)
}
#if !(arch(x86_64) || arch(arm64))
func sqrt(a: CGFloat) -> CGFloat {
return CGFloat(sqrtf(Float(a)))
}
#endif
extension CGPoint {
func length() -> CGFloat {
return sqrt(x*x + y*y)
}
func normalized() -> CGPoint {
return self / length()
}
}
These are standard implementations of some vector math functions. If you’re confused about what’s going on here or are new to vector math, check out this quick vector math explanation.
Next, add a new method to the GameScene
class — again, before the closing curly brace:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// 1 - Choose one of the touches to work with
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
// 2 - Set up initial location of projectile
let projectile = SKSpriteNode(imageNamed: "projectile")
projectile.position = player.position
// 3 - Determine offset of location to projectile
let offset = touchLocation - projectile.position
// 4 - Bail out if you are shooting down or backwards
if offset.x < 0 { return }
// 5 - OK to add now - you've double checked position
addChild(projectile)
// 6 - Get the direction of where to shoot
let direction = offset.normalized()
// 7 - Make it shoot far enough to be guaranteed off screen
let shootAmount = direction * 1000
// 8 - Add the shoot amount to the current position
let realDest = shootAmount + projectile.position
// 9 - Create the actions
let actionMove = SKAction.move(to: realDest, duration: 2.0)
let actionMoveDone = SKAction.removeFromParent()
projectile.run(SKAction.sequence([actionMove, actionMoveDone]))
}
There's a lot going on here, so here's what it does, step by step.
- One of the cool things about SpriteKit is that it includes a category on UITouch with
location(in:)
andpreviousLocation(in:)
methods. These let you find the coordinate of a touch within an SKNode's coordinate system. In this case, you use it to find out where the touch is within the scene's coordinate system. - You then create a projectile and place it where the player is to start. Note you don't add it to the scene yet, because you have to do some sanity checking first - this game does not allow the ninja to shoot backwards.
- You then subtract the projectile's current position from the touch location to get a vector from the current position to the touch location.
- If the X value is less than 0, this means the player is trying to shoot backwards. This is not allowed in this game (real ninjas don't look back!), so just return.
- Otherwise, it's OK to add the projectile to the scene.
- Convert the offset into a unit vector (of length 1) by calling
normalized()
. This will make it easy to make a vector with a fixed length in the same direction, because 1 * length = length. - Multiply the unit vector in the direction you want to shoot in by 1000. Why 1000? It will definitely be long enough to go past the edge of the screen. :]
- Add the shoot amount to the current position to get where it should end up on the screen.
- Finally, create
move(to:,duration:)
andremoveFromParent()
actions like you did earlier for the monster.
Build and run. Now your ninja should be able to fire away at the oncoming hordes!
Collision Detection and Physics: Overview
So now you have shurikens flying everywhere, but what your ninja really wants to do is to lay some smack down. So, it's time to add some code to detect when your projectiles intersect your targets.
One of the nice things about SpriteKit is it comes with a physics engine built right in! Not only are physics engines great for simulating realistic movement, but they're also great for collision detection purposes.
You'll set up the game to use SpriteKit's physics engine to determine when monsters and projectiles collide. At a high level, here's what you're going to do:
- Set up the physics world. A physics world is the simulation space for running physics calculations. One is set up on the scene by default, and you might want to configure a few properties on it, like gravity.
- Create physics bodies for each sprite. In SpriteKit, you can associate a shape with each sprite for collision detection purposes, and set certain properties on it. This is called a physics body. Note that the physics body does not have to be the exact same shape as the sprite. Usually it's a simpler, approximate shape rather than pixel-perfect, since that's good enough for most games and performance.
- Set a category for each type of sprite. One of the properties you can set on a physics body is a category, which is a bitmask indicating the group or groups it belongs to. In this game, you're going to have two categories: one for projectiles and one for monsters. Then later when two physics bodies collide, you can easily tell what kind of sprite you're dealing with by looking at its category.
- Set a contact delegate. Remember that physics world from earlier? Well, you can set a contact delegate on it to be notified when two physics bodies collide. There you'll write some code to examine the categories of the objects, and if they're the monster and projectile, you'll make them go boom!
Now that you understand the battle plan, it's time to put it into action!
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!
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!
Where to Go From Here?
And that's a wrap! You can download the project files at the top or bottom of this page.
I hope you enjoyed learning about SpriteKit and are inspired to make your own game. If you have any questions or comments about this SpriteKit tutorial, please join the discussion below!
If you want to learn more about SpriteKit, you should check out our book: 2D Apple Games by Tutorials:
In this book we teach you everything you need to know to make great games for iOS, tvOS, watchOS and macOS: from physics, to tile maps, to particle systems, and even how to make your games "juicy" with polish and special effects!
The post SpriteKit Tutorial for Beginners appeared first on Ray Wenderlich.