In this tutorial, you’ll learn how to build a game like Cut the Rope with SpriteKit in Swift, complete with animations, sound effects and physics!

Update note: Brody Eller updated this tutorial for iOS 13, Xcode 11 and Swift 5. Tammy Coron wrote the original.

Cut The Rope is a popular physics-driven game where players feed a little monster named Om Nom by cutting ropes which suspend candies. Slice at just the right time and place and Om Nom gets a tasty treat.

With all due respect to Om Nom, the game’s true star is its simulated physics: Ropes swing, gravity pulls and candies tumble just as you’d expect in real life.

You can build a similar experience using the physics engine of SpriteKit, Apple’s 2D game framework. In this Cut the Rope with SpriteKit tutorial, you’ll do just that with a game called Snip The Vine.

Note: This tutorial assumes you have some experience with SpriteKit. If you’re new to SpriteKit, check out our SpriteKit Tutorial for Beginners

Getting Started

In Snip The Vine, you’ll feed cute little animals pineapples to a crocodile. To get started, download the project files by clicking the Download Materials button at the top or bottom of the tutorial. Open the project in Xcode for a quick look at how it’s structured.

You’ll find the project files in several folders. In this tutorial, you’ll work in Classes, which contains the primary code files. Feel free to explore the other folders, shown below:

You’ll be using values from Constants.swift throughout the tutorial, so take some time to get acquainted with that file before diving in.

Adding the Crocodile to the Scene

Be forewarned that this crocodile is quite snappy, please keep your fingers at a safe distance at all times! :]

The crocodile is represented by an SKSpriteNode . You’ll need to retain a reference to the crocodile for your game logic. You’ll also need to set up a physics body for the crocodile sprite, to detect and handle contacts with other bodies.

In GameScene.swift, add the following properties to the top of the class:

private var crocodile: SKSpriteNode! private var prize: SKSpriteNode!

These properties will store references to the crocodile and the prize (the pineapple).

Locate setUpCrocodile() inside GameScene.swift and add the following code:

crocodile = SKSpriteNode(imageNamed: ImageName.crocMouthClosed) crocodile.position = CGPoint(x: size.width * 0.75, y: size.height * 0.312) crocodile.zPosition = Layer.crocodile crocodile.physicsBody = SKPhysicsBody( texture: SKTexture(imageNamed: ImageName.crocMask), size: crocodile.size) crocodile.physicsBody?.categoryBitMask = PhysicsCategory.crocodile crocodile.physicsBody?.collisionBitMask = 0 crocodile.physicsBody?.contactTestBitMask = PhysicsCategory.prize crocodile.physicsBody?.isDynamic = false addChild(crocodile) animateCrocodile()

With this code, you create the crocodile node and set its position and zPosition .

The croc has an SKPhysicsBody , which means it can interact with other objects in the world. This will be useful later, when you want to detect when the pineapple lands in its mouth.

You don’t want the croc to get knocked over or fall off the bottom of the screen! To prevent that, you set isDynamic to false , which prevents physical forces from affecting it.

The categoryBitMask defines what physics category the body belongs to — PhysicsCategory.crocodile , in this case. You set collisionBitMask to 0 because you don’t want the crocodile to bounce off any other bodies. All you need to know is when a “prize” body makes contact with the crocodile, so you set the contactTestBitMask accordingly.

You might notice that the physics body for the crocodile initializes using an SKTexture object. You could simply re-use .crocMouthOpen for the body texture, but that image includes the croc’s whole body, whereas the mask texture just includes the crocodile’s head and mouth. A croc can’t eat a pineapple with its tail!

Now you’ll add a “waiting” animation to the crocodile. Find animateCrocodile() and add the following code:

let duration = Double.random(in: 2...4) let open = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthOpen)) let wait = SKAction.wait(forDuration: duration) let close = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthClosed)) let sequence = SKAction.sequence([wait, open, wait, close]) crocodile.run(.repeatForever(sequence))

Besides making the little crocodile very anxious, this code also creates a couple of actions that change the texture of the crocodile node so that it alternates between a closed mouth and an open mouth.

The SKAction.sequence(_:) constructor creates a sequence of actions from an array. In this case, the texture actions combine in sequence with a randomly-chosen delay period between two and four seconds.

The sequence action is wrapped in repeatForever(_:) , so it will repeat for the duration of the level. The crocodile node then runs it.

That’s it! Build and run and see this fierce reptile snap his jaws of death!

You’ve got scenery and you’ve got a croc — now you need cute little animals a pineapple.

Adding the Prize

Open GameScene.swift and locate setUpPrize() . Add the following:

prize = SKSpriteNode(imageNamed: ImageName.prize) prize.position = CGPoint(x: size.width * 0.5, y: size.height * 0.7) prize.zPosition = Layer.prize prize.physicsBody = SKPhysicsBody(circleOfRadius: prize.size.height / 2) prize.physicsBody?.categoryBitMask = PhysicsCategory.prize prize.physicsBody?.collisionBitMask = 0 prize.physicsBody?.density = 0.5 addChild(prize)

Similar to the crocodile, the pineapple node also uses a physics body. The big difference is that the pineapple should fall and bounce around, whereas the crocodile just sits there and waits impatiently. So you leave isDynamic set to its default value of true . You also decrease the density of the pineapple so it can swing more freely.

Working With Physics

Before you start dropping pineapples, it’s a good idea to configure the physics world. Locate setUpPhysics() inside of GameScene.swift and add the following three lines:

physicsWorld.contactDelegate = self physicsWorld.gravity = CGVector(dx: 0.0, dy: -9.8) physicsWorld.speed = 1.0

This sets up the physics world’s contactDelegate , gravity and speed . Gravity specifies the gravitational acceleration applied to physics bodies in the world, while speed regulates how fast the simulation executes. You want both properties set to their default values here.

You’ll notice you didn’t have to conform to SKPhysicsContactDelegate because there’s already an extension at the bottom of GameScene that implements it. Leave it alone for now; you’ll use it later.

Build and run again. You should see the pineapple sail past the crocodile and fall into the water.

Time to add the vines.

Adding Vines

SpriteKit physics bodies model rigid objects… but vines bend. To handle this, you’ll implement each vine as an array of segments with flexible joints, similar to a chain.

Each vine has three significant attributes:

anchorPoint: A CGPoint indicating where the end of the vine connects to the tree.

indicating where the end of the vine connects to the tree. length: An Int representing the number of segments in the vine.

representing the number of segments in the vine. name: A String used to identify which vine a given segment belongs to.

In this tutorial, the game has only one level. But in a real game, you’d want to be able to easily create new level layouts without writing a lot of code. A good way to do this is to specify your level data independently of your game logic, perhaps by storing it in a data file with a property list or JSON.

Since you’ll be loading your vine data from a file, the natural structure to represent it is an array of Codable values, which can be easily read from a property list using PlistDecoder . Each dictionary in the property list will represent one VineData instance, a struct that is already defined in the starter project.

In GameScene.swift, locate setUpVines() and add the following code:

let decoder = PropertyListDecoder() guard let dataFile = Bundle.main.url( forResource: GameConfiguration.vineDataFile, withExtension: nil), let data = try? Data(contentsOf: dataFile), let vines = try? decoder.decode([VineData].self, from: data) else { return }

Load the vine data from a property list file. Take a look at VineData.plist inside Resources/Data. You should see that the file contains an array of dictionaries, each containing a relAnchorPoint and length:

Next, add the following code to the method:

// 1 add vines for (i, vineData) in vines.enumerated() { let anchorPoint = CGPoint( x: vineData.relAnchorPoint.x * size.width, y: vineData.relAnchorPoint.y * size.height) let vine = VineNode( length: vineData.length, anchorPoint: anchorPoint, name: "\(i)") // 2 add to scene vine.addToScene(self) // 3 connect the other end of the vine to the prize vine.attachToPrize(prize) }

With this code, you:

For each vine, initialize a new VineNode . length specifies the number of segments in the vine. relAnchorPoint determines the anchor position of the vine relative to the size of the scene. Finally, attach the VineNode to the scene with addToScene(_:) . Then, attach it to the prize using attachToPrize(_:) .

Next, you’ll implement those methods in VineNode .

Defining the Vine Class

Open VineNode.swift. VineNode is a custom class that inherits from SKNode . It doesn’t have any visual appearance of its own, but instead acts as a container for a collection of SKSpriteNode s that represent the vine segments.

Add the following properties to the class definition:

private let length: Int private let anchorPoint: CGPoint private var vineSegments: [SKNode] = []

Some errors will appear because you haven’t initialized the length and anchorPoint properties. You’ve declared them as non-optional, but you haven’t assigned a value. Fix this by replacing the implementation of init(length:anchorPoint:name:) with the following:

self.length = length self.anchorPoint = anchorPoint super.init() self.name = name

Pretty straightforward… but for some reason, there are still errors. You might notice there’s a second initializer method, init(coder:) . You aren’t calling it anywhere, so what is it for?

Because SKNode implements NSCoding , it inherits the required initializer init(coder:) . That means you have to initialize your non-optional properties there as well, even though you aren’t using it.

Do that now. Replace the content of init(coder:) with:

length = aDecoder.decodeInteger(forKey: "length") anchorPoint = aDecoder.decodeCGPoint(forKey: "anchorPoint") super.init(coder: aDecoder)

Next, you need to implement addToScene(_:) . This is a complex method, so you’ll write it in stages. First, find addToScene(_:) and add the following:

// add vine to scene zPosition = Layer.vine scene.addChild(self)

You add the vine to the scene and set its zPosition . Next, add this block of code to same the method:

// create vine holder let vineHolder = SKSpriteNode(imageNamed: ImageName.vineHolder) vineHolder.position = anchorPoint vineHolder.zPosition = 1 addChild(vineHolder) vineHolder.physicsBody = SKPhysicsBody(circleOfRadius: vineHolder.size.width / 2) vineHolder.physicsBody?.isDynamic = false vineHolder.physicsBody?.categoryBitMask = PhysicsCategory.vineHolder vineHolder.physicsBody?.collisionBitMask = 0

This creates the vine holder, which is like a nail for the vine to hang from. As with the crocodile, this body is not dynamic and does not collide with other bodies.

The vine holder is circular, so use the SKPhysicsBody(circleOfRadius:) initializer. The position of the vine holder matches the anchorPoint that you specified when creating the VineNode .

Next, you’ll create the vine. Add the following code, again to the bottom of the same method:

// add each of the vine parts for i in 0..<length { let vineSegment = SKSpriteNode(imageNamed: ImageName.vineTexture) let offset = vineSegment.size.height * CGFloat(i + 1) vineSegment.position = CGPoint(x: anchorPoint.x, y: anchorPoint.y - offset) vineSegment.name = name vineSegments.append(vineSegment) addChild(vineSegment) vineSegment.physicsBody = SKPhysicsBody(rectangleOf: vineSegment.size) vineSegment.physicsBody?.categoryBitMask = PhysicsCategory.vine vineSegment.physicsBody?.collisionBitMask = PhysicsCategory.vineHolder }

This loop creates an array of vine segments that's equal in number to the length you specified when you created VineNode . Each segment is a sprite with its own physics body. The segments are rectangular, so you use SKPhysicsBody(rectangleOf:) to specify the shape of the physics body.

Unlike the vine holder, the vine nodes are dynamic – they can move around and are affected by gravity.

Build and run to see your progress.

Uh oh! The vine segments fall off the screen like chopped spaghetti!

Adding Joints to the Vines

The problem is that you haven't joined the vine segments together yet. To fix that, you need to add this final chunk of code to the bottom of addToScene(_:) :

// set up joint for vine holder let joint = SKPhysicsJointPin.joint( withBodyA: vineHolder.physicsBody!, bodyB: vineSegments[0].physicsBody!, anchor: CGPoint( x: vineHolder.frame.midX, y: vineHolder.frame.midY)) scene.physicsWorld.add(joint) // set up joints between vine parts for i in 1..<length { let nodeA = vineSegments[i - 1] let nodeB = vineSegments[i] let joint = SKPhysicsJointPin.joint( withBodyA: nodeA.physicsBody!, bodyB: nodeB.physicsBody!, anchor: CGPoint( x: nodeA.frame.midX, y: nodeA.frame.minY)) scene.physicsWorld.add(joint) }

This code sets up physical joints between the segments, connecting them together. The type of joint you've used is an SKPhysicsJointPin . This joint type behaves as if you'd hammered a pin through the two nodes, allowing them to pivot around the pin but not move closer or farther from one another.

Build and run again. Your vines should hang realistically from the trees.

The final step is to attach the vines to the pineapple. Still in VineNode.swift, scroll to attachToPrize(_:) . Add the following code:

// align last segment of vine with prize let lastNode = vineSegments.last! lastNode.position = CGPoint(x: prize.position.x, y: prize.position.y + prize.size.height * 0.1) // set up connecting joint let joint = SKPhysicsJointPin.joint(withBodyA: lastNode.physicsBody!, bodyB: prize.physicsBody!, anchor: lastNode.position) prize.scene?.physicsWorld.add(joint)

This code gets the last segment of the vine and positions it slightly above the center of the prize. You want to attach it here so the prize hangs down realistically. If it was dead-center, the prize would be evenly weighted and might spin on its axis. It also creates another pin joint to attach the vine segment to the prize.

Build and run. If all your joints and nodes are set up properly, you should see a screen similar to the one below:

Yay! A dangling pineapple – who the heck ties pineapples to trees? :]

Snipping the Vines

You probably noticed that you still can't snip those vines! You'll sort out that little problem next.

In this section, you’ll work with touch methods that will allow players to snip those hanging vines. Back in GameScene.swift, locate touchesMoved(_:with:) and add the following code:

for touch in touches { let startPoint = touch.location(in: self) let endPoint = touch.previousLocation(in: self) // check if vine cut scene?.physicsWorld.enumerateBodies( alongRayStart: startPoint, end: endPoint, using: { body, _, _, _ in self.checkIfVineCut(withBody: body) }) // produce some nice particles showMoveParticles(touchPosition: startPoint) }

This code works as follows: First, it gets the current and previous positions of each touch. Next, it loops through all of the bodes in the scene that lie between those two points, using the very handy enumerateBodies(alongRayStart:end:using:) method of SKScene . For each body encountered, it calls checkIfVineCut(withBody:) , which you'll write in a minute.

Finally, the code calls a method that creates an SKEmitterNode by loading it from Particle.sks, and adds it to the scene at the position of the user's touch. This results in a nice green smoke trail wherever you drag your finger. Pure eye candy!

Now, scroll down to checkIfVineCut(withBody:) and add this block of code to the method body:

let node = body.node! // if it has a name it must be a vine node if let name = node.name { // snip the vine node.removeFromParent() // fade out all nodes matching name enumerateChildNodes(withName: name, using: { node, _ in let fadeAway = SKAction.fadeOut(withDuration: 0.25) let removeNode = SKAction.removeFromParent() let sequence = SKAction.sequence([fadeAway, removeNode]) node.run(sequence) }) }

The code above first checks if the node that's connected to the physics body has a name. Remember, there are other nodes in the scene besides vine segments, and you certainly don't want to accidentally slice up the crocodile or the pineapple with a careless swing! But you only named the vine node segments, so if the node has a name, you can be certain that it's part of a vine.

Next, you remove the node from the scene. Removing a node also removes its physicsBody and destroys any joints connected to it. You've now officially snipped the vine!

Finally, you enumerate through all nodes in the scene whose name matches the name of the node that you swiped, using the scene's enumerateChildNodes(withName:using:) . The only nodes whose name should match are the other segments in the same vine, so you're just looping over the segments of whichever vine you sliced.

For each node, you create an SKAction that first fades out the node and then removes it from the scene. The effect is that after it's sliced, each vine will fade away.

Build and run. Try and snip those vines – you should now be able to swipe and cut all three vines and see that prize fall. Sweet pineapples! :]

Handling Contact Between Bodies

When you wrote setUpPhysics() , you specified that GameScene would act as the contactDelegate for the physicsWorld . You also configured the contactTestBitMask of the croc so that SpriteKit would notify when it intersects with the prize. That was excellent foresight!

Now, in GameScene.swift you need to implement didBegin(_:) of SKPhysicsContactDelegate , which will trigger whenever it detects an intersection between two appropriately-masked bodies. There's a stub for that method — scroll down to find it and add the following code:

if (contact.bodyA.node == crocodile && contact.bodyB.node == prize) || (contact.bodyA.node == prize && contact.bodyB.node == crocodile) { // shrink the pineapple away let shrink = SKAction.scale(to: 0, duration: 0.08) let removeNode = SKAction.removeFromParent() let sequence = SKAction.sequence([shrink, removeNode]) prize.run(sequence) }

This code checks if the two intersecting bodies belong to the crocodile and the prize. You don't know the nodes' order, so you check for both combinations. If the test passes, you trigger a simple animation sequence that shrinks the prize down to nothing and then removes it from the scene.

Animate the Crocodile's Chomp

You also want the crocodile to chomp down when it catches a pineapple. Inside the if statement where you just triggered the pineapple shrink animation, add the following extra line:

runNomNomAnimation(withDelay: 0.15)

Now locate runNomNomAnimation(withDelay:) and add this code:

crocodile.removeAllActions() let closeMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthClosed)) let wait = SKAction.wait(forDuration: delay) let openMouth = SKAction.setTexture(SKTexture(imageNamed: ImageName.crocMouthOpen)) let sequence = SKAction.sequence([closeMouth, wait, openMouth, wait, closeMouth]) crocodile.run(sequence)

The code above removes any animation currently running on the crocodile node using removeAllActions() . It then creates a new animation sequence that closes and opens the crocodile’s mouth and has the crocodile run this sequence.

This new animation will trigger when the prize lands in the croc's mouth, giving the impression that the crocodile is chewing it.

While you're at it, add the following lines below the call to enumerateChildNodes in checkIfVineCut(withBody:) :

crocodile.removeAllActions() crocodile.texture = SKTexture(imageNamed: ImageName.crocMouthOpen) animateCrocodile()

This will ensure that the croc's mouth is open when you snip a vine so there's a chance of the prize falling in the croc's mouth.

Build and run.

The happy croc will now chew up the pineapple if it lands in its mouth. But once that's happened, the game just hangs there. You're solve that issue next.

Resetting the Game

Next, you'll reset the game when the pineapple falls down or the croc eats the pineapple.

In GameScene.swift, find switchToNewGame(withTransition:) , and add the following code:

let delay = SKAction.wait(forDuration: 1) let sceneChange = SKAction.run { let scene = GameScene(size: self.size) self.view?.presentScene(scene, transition: transition) } run(.sequence([delay, sceneChange]))

The code above uses SKView ’s presentScene(_:transition:) to present the next scene.

In this case, the scene you transition to is a new instance of the same GameScene . You also pass in a transition effect using the SKTransition class. You specify the transition as an argument to the method so that you can use different transition effects depending on the outcome of the game.

Scroll back to didBegin(_:) , and inside the if statement, after the Prize Shrink and NomNom animations, add the following:

// transition to next level switchToNewGame(withTransition: .doorway(withDuration: 1.0))

This calls switchToNewGame(withTransition:) using the .doorway(withDuration:) initializer to create a doorway transition. This shows the next level with an effect like a door opening. Build and run to see the effect.

Pretty neat, huh?

Ending the Game

You might think that you need to add another physics body to the water so you can detect if the prize hits it, but that wouldn't help if the pineapple flies off the side of the screen.

A simpler, better approach is just to detect when the pineapple has moved below the bottom of the screen edge, then end the game.

SKScene provides update(_:) that's called once every frame. Find that method in GameScene.swift and add the following logic:

if prize.position.y <= 0 { switchToNewGame(withTransition: .fade(withDuration: 1.0)) }

The if statement checks if the prize's y coordinate is less than zero – that is, the bottom of the screen. If so, it calls switchToNewGame(withTransition:) to start the level again, this time using a .fade(withDuration:) .

Build and run.

You should see the scene fade out and transition to a new scene whenever the player wins or loses.

Adding Sound and Music

Now, you're going to add a nice jungle song from incompetech.com and some sound effects from freesound.org.

SpriteKit will handle the sound effects for you. But you'll use AVAudioPlayer to play the background music seamlessly between level transitions.

Adding the Background Music

To start adding the music, add another property to GameScene.swift:

private static var backgroundMusicPlayer: AVAudioPlayer!

This declares a type property so all instances of GameScene will be able to access the same backgroundMusicPlayer . Locate setUpAudio() and add the following code:

if GameScene.backgroundMusicPlayer == nil { let backgroundMusicURL = Bundle.main.url( forResource: SoundFile.backgroundMusic, withExtension: nil) do { let theme = try AVAudioPlayer(contentsOf: backgroundMusicURL!) GameScene.backgroundMusicPlayer = theme } catch { // couldn't load file :[ } GameScene.backgroundMusicPlayer.numberOfLoops = -1 }

The code above checks if the backgroundMusicPlayer exists yet. If not, it initializes a new AVAudioPlayer with the BackgroundMusic that you added to Constants.swift earlier. It then converts it to a URL and assigns it to the property. The numberOfLoops value is set to -1 , which indicates that the song should loop indefinitely.

Next, add this code to the bottom of setUpAudio() :

if !GameScene.backgroundMusicPlayer.isPlaying { GameScene.backgroundMusicPlayer.play() }

This starts the background music when the scene first loads. It will then play indefinitely until the app exits or another method calls stop() on the player.

You could just call play() without first checking if the player is playing, but this way the music won't skip or restart if it's already playing when the level begins.

Adding the Sound Effects

While you're here, you may as well set up all the sound effects that you'll use later. Unlike the music, you don't want to play the sound effects right away. Instead, you'll create some reusable SKAction s that will play the sounds later.

Go back up to the top of the GameScene class definition and add the following properties:

private var sliceSoundAction: SKAction! private var splashSoundAction: SKAction! private var nomNomSoundAction: SKAction!

Now go back to setUpAudio() and add the following lines to the bottom of the method:

sliceSoundAction = .playSoundFileNamed( SoundFile.slice, waitForCompletion: false) splashSoundAction = .playSoundFileNamed( SoundFile.splash, waitForCompletion: false) nomNomSoundAction = .playSoundFileNamed( SoundFile.nomNom, waitForCompletion: false)

This code initializes the sound actions using SKAction ’s playSoundFileNamed(_:waitForCompletion:) . Now, it's time to play the sound effects.

Scroll up to update(_:) and add the following code inside the if statement above the call to switchToNewGame(withTransition:) :

run(splashSoundAction)

That will play the sound of a splash when the pineapple lands in the water. Next, find didBegin(_:) and add the following code just below the runNomNomAnimation(withDelay:) line:

run(nomNomSoundAction)

That will play a chomping sound when the croc catches its prize. Finally, locate checkIfVineCut(withBody:) and add the following code at the bottom of the if let statement:

run(sliceSoundAction)

That will play a swiping sound whenever the player snips a vine.

Build and run to enjoy that crunchy sound when the croc eats the pineapple!

Getting Rid of an Awkward Sound Effect

Did you discover a bug? If you miss the croc, the splashing sound plays multiple times. This is because you trigger "level complete" logic repeatedly before the game transitions to the next scene. To correct this, add a new state property to the top of the class:

private var isLevelOver = false

Now modify update(_:) and didBegin(_:) by adding the following code at the top of each of them:

if isLevelOver { return }

Finally, inside the other if statements of the same methods, add some code to set the isLevelOver state to true:

isLevelOver = true

Now, as soon as the game detects that isLevelOver is set, either because the pineapple hit the ground or because the croc got its meal, it'll stop checking for the game win/lose scenarios. It won't keep repeatedly trying to play those sound effects.

Build and run. There are no awkward sound effects anymore!

Adding Some Difficulty

After playing a few rounds, the game seems a bit too easy. You'll quickly get to the point where you can feed the croc with a single well-timed slice through the three vines.

Make things trickier by using the value in Constants.swift, CanCutMultipleVinesAtOnce .

In GameScene.swift, add one last property at the top of the GameScene class definition:

private var didCutVine = false

Now locate checkIfVineCut(withBody:) and add the following if statement at the top:

if didCutVine && !GameConfiguration.canCutMultipleVinesAtOnce { return }

Add this code to the bottom of the same method, inside the if statement:

didCutVine = true

Just to keep things together, find the touchesBegan(_:with:) , and add the following line:

didCutVine = false

This way, you reset didCutVine whenever the user touches the screen.

Build and run again.

You should see that it's now only possible to snip one vine each time you swipe. To cut another, you have to lift your finger and then swipe again.

Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this Cut The Rope with SpriteKit tutorial.

Don't stop now! Try adding new levels, different vines and maybe even a score display and timer.

If you’d like to learn more about SpriteKit, be sure to check out our book, 2D Apple Games by Tutorials.

If you have any questions or comments, feel free to join in the discussion below!