Update note: This Sprite Kit tutorial was updated for iOS 8 and Swift by Nick Lockwood. Original post by Tutorial Team member Tammy Coron.
Prior to iOS 7, if you wanted to make a game, you either had to know a bit of arcane OpenGL magic, or rely on a third party library to do the heavy lifting for you. Once you had your graphics engine in place, you’d often need to add additional libraries for physics, sound effects, etc. This could result in additional dependencies as well as additional language requirements (such as C++).
With the introduction of Sprite Kit in iOS 7, Apple changed all of this. Developers have access to an integrated 2D graphics and physics engine, all exposed via a friendly Cocoa API. Now, developers can focus on making a game, instead of trying to assemble an engine from raw materials.
In this tutorial, you will use Sprite Kit to make a game similar to Cut the Rope, the award-winning, physics-based puzzle game. You’ll learn how to:
- Add sprites to scenes
- Create animation sequences
- Add music and sound effects
- Work with Sprite Kit’s physics engine!
By the end of this tutorial, you’ll be well on your way to using Sprite Kit to create your own game projects from scratch.
This is not an entry-level tutorial; if classes such as SKNode
or SKAction
are entirely new to you, then check out our Sprite Kit Tutorial for Beginners before continuing. That tutorial will get you quickly up to speed so you can start feeding pineapples to crocodiles.
Wait. What?
Read on.
Note: Make sure you’re using the latest version of Xcode 6.1 before continuing! Due to several incompatible changes in Swift, you’ll get compiler errors if you use Xcode 6.0!
On Your Marks… Get Set…
In this tutorial, you’ll be creating a game called Cut the Verlet. This game is modelled after Cut the Rope which tasks you to cut a rope holding a candy so it will drop into the mouth of a cute (but hungry, and impatient) creature. This game was first demonstrated in a previous tutorial using Objective-C and Cocos2D, but in this tutorial, you’ll be using Sprite Kit and Swift.
So what is a Verlet? Verlet is not actually a what but a who – Loup Verlet is a French physicist responsible for inventing Verlet integration, a mathematical method for modelling particle trajectories in motion, and also a great tool for simulating other physical interactions, such as ropes and solid structures. The original Cocos2D version of this tutorial used a Verlet integrator, but for the Sprite Kit version you’ll be using the built-in physics engine instead of a Verlet integrator (we’re keeping the name though).
If you’re interested in finding out more about modelling ropes using Verlet integrators, Gustavo Ambrozio (the author of the Cocos2D version of this tutorial) provides an excellent overview of verlets and how they are applied to the game. It’s not required reading for this tutorial, however.
To get started, first download the starter project for this tutorial. Extract the project to a convenient location on your hard drive and then open it in Xcode for a quick look at how it’s structured.
The project files are split into six folders, as shown below:
- Classes contains the primary code files, including the main view controller, the scene and the rope node. You will be adding to these classes throughout this tutorial.
- Supporting Files contains the project’s main storyboard and info.plist. You won’t need to touch these.
- Resources/Sounds contains the project’s sound effects and music files.
- Resources/Particles contains the files you need to add particles to the scene.
- Resources/Data contains data files that define the level.
- Images.xcassets contains all of the image assets for this project (this is technically a folder, although Xcode displays it differently).
For this tutorial, you’ll be working only with the files located in the Classes folder.
It’s time to begin!
Constant Vigilance
A constant is a property whose value never changes once set. In Swift, you’re encouraged to use constant whenever possible by using the let
keyword to declare your local and class properties. But we’re not talking about those kind of constants here, we’re talking about global constants.
Global constants help to make your code more readable and maintainable by avoiding repetition of hard-coded strings or magic numbers throughout your codebase. In this project, you’ll define some global constants for things like sprite image names, sound files, the z-order or zPosition
of your sprites and the category defined for each sprite (which is important for collision detection).
Open Constants.swift and add the following code below the //MARK: constants
line:
let BackgroundImage = "Background" let GroundImage = "Ground" let WaterImage = "Water" let RopeTextureImage = "RopeTexture" let RopeHolderImage = "RopeHolder" let CrocMouthClosedImage = "CrocMouthClosed" let CrocMouthOpenImage = "CrocMouthOpen" let CrocMaskImage = "CrocMask" let PrizeImage = "Pineapple" let PrizeMaskImage = "PineappleMask" let BackgroundMusicSound = "CheeZeeJungle.caf" let SliceSound = "Slice.caf" let SplashSound = "Splash.caf" let NomNomSound = "NomNom.caf" let RopeDataFile = "RopeData.plist" |
The code above declares a group of String
constants using the let
keyword to indicate that their values cannot be changed later. Because these constants are declared outside of any class, they are globally available anywhere in the program.
Beneath those, add the following:
struct Layer { static let Background: CGFloat = 0 static let Crocodile: CGFloat = 1 static let Rope: CGFloat = 1 static let Prize: CGFloat = 2 static let Foreground: CGFloat = 3 } struct Category { static let Crocodile: UInt32 = 1 static let RopeHolder: UInt32 = 2 static let Rope: UInt32 = 4 static let Prize: UInt32 = 8 } |
This code declares two structs, Layer
and Category
, each containing a bunch of static CGFloat
and UInt32
properties respectively. You’ll use these to specify the zPosition
and collision category of a sprite when you add it to the scene (more about this later).
toRaw()
, which makes them unwieldy to use for the purpose intended here. Structs containing static properties are a better way to create groups of numeric constants in SwiftFinally, under //MARK: game configuration
, add the following:
let PrizeIsDynamicsOnStart = true let CanCutMultipleRopesAtOnce = false |
In addition to avoiding “magic” values, constants also allow you to easily make certain parts of the game configurable. It’s not always obvious which gameplay decisions will make a game more fun, but it’s usually too late to go back and recode everything later if you realize you made the wrong call. Constants give you a simple way to flip a switch to swap between different approaches and see which works best in practice.
This last pair of constants will be used to tweak the gameplay later.
if
statements to enable and disable code at runtime.Now that you’ve got your constants in place, you can begin adding nodes to your scene, starting with the background and foreground scenery.
Setting the Scene
The starter project provides stub versions of the GameScene and RopeNode classes. You’ll be fleshing out those stubbed methods with the code to get the game up and running. The first step is to initialize the SKScene
and add the background and foreground scenery.
Open GameScene.swift and add the following to setUpScenery()
:
let background = SKSpriteNode(imageNamed: BackgroundImage) background.anchorPoint = CGPointMake(0, 1) background.position = CGPointMake(0, size.height) background.zPosition = Layer.Background background.size = CGSize(width: self.view!.bounds.size.width, height:self.view!.bounds.size.height) addChild(background) let water = SKSpriteNode(imageNamed: WaterImage) water.anchorPoint = CGPointMake(0, 0) water.position = CGPointMake(0, size.height - background.size.height) water.zPosition = Layer.Foreground water.size = CGSize(width: self.view!.bounds.size.width, height: self.view!.bounds.size.height * 0.2139) addChild(water) |
The setUpScenery()
method is called from didMoveToView()
. In this method, you create a couple of SKSpriteNode
s and initialize them using SKSpriteNode(imageNamed:)
. Because you now have to deal with multiple screen sizes for @2x resolutions (iPhone 5/5s and iPhone 6), the size of these background images needs to be made explicit (not inferred) and made relative to the window screen. There will be a little bit of scaling because of this, but this is OK for a background image.
You’ve changed the anchorPoint
of the background from the default value of (0.5, 0.5) to (0, 1). This means that it is positioned relative to the top-left corner of the sprite image, instead of its center, which is a bit easier to work with when positioning relative to the scene’s coordinate space. For the water, you’ve set the anchorPoint
to (0, 0), which makes it easier to align with the bottom of the background image.
The anchorPoint
property uses the unit coordinate system, where (0,0) represents the bottom-left corner of the sprite image, and (1,1) represents the top-right corner. Because it’s always measured from 0 to 1, these coordinates are independent of the image dimensions or aspect ratio.
You also set the sprites’ position
s within the scene and zPosition
s, which controls the layer order.
Recall that in Constants.swift, you specified some values for use with the sprite’s zPosition
. You use two of them in the code above: Layer.Background
and Layer.Foreground
. SKSpriteNode
inherits the zPosition
property from SKNode
. These values ensure that the background will always be drawn behind all the other sprites in the scene, and the foreground will always be drawn in front, regardless of the order in which you add the sprites.
Build and run your project. If you did everything right, you should see the following screen:
It’s a lonely jungle out there. Time to bring out the croc!
In a While, Crocodile
Just like the background scenery, the crocodile is represented by an SKSpriteNode
. There are a couple of important differences though: Firstly, you’ll need to retain a reference to the crocodile so you can refer back to it later in your game logic, and secondly, you’ll need to set up a physics body for the crocodile sprite in order to detect and handle collisions.
Still 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 main actors in the scene: the crocodile and the prize (pineapple). You’ve defined them as private, because they won’t be accessed outside of the scene itself, and it’s always good practice to keep your class data private unless there’s a good reason why it needs to be directly visible to other classes.
The type for these properties has been defined as SKSpriteNode!
. The “!” means that these are implicitly unwrapped optionals, which tells Swift that they don’t need to be initialized right away, but that we confident that they won’t be nil when we try to access them (and we better be confident – the app will crash if we get it wrong!).
let
keyword, but due to the design of the SKScene
class, we won’t be setting these values until the didMoveToView(view:)
method is called (after the scene has been initialized), which isn’t permitted for constants.Locate the setUpCrocodile()
method inside of GameScene.swift and add the following block of code:
crocodile = SKSpriteNode(imageNamed: CrocMouthClosedImage) crocodile.position = CGPointMake(size.width * 0.75, size.height * 0.312) crocodile.zPosition = Layer.Crocodile crocodile.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: CrocMaskImage), size: crocodile.size) crocodile.physicsBody?.categoryBitMask = Category.Crocodile crocodile.physicsBody?.collisionBitMask = 0 crocodile.physicsBody?.contactTestBitMask = Category.Prize crocodile.physicsBody?.dynamic = false addChild(crocodile) animateCrocodile() |
This code uses two of the constants you set up earlier: CrocMouthClosedImage
and Layer.Crocodile
. It also sets the position
of the crocodile node relative to the scene bounds.
Just as before, you set the zPosition
to place the crocodile node on top of the background. By default, Sprite Kit will layer nodes based on the order in which they’re added to their parent, but you can control the ordering yourself by providing a different zPosition
.
Unlike the background scenery, the croc has an SKPhysicsBody
, which means it can interact physically with other objects in the world. This will be useful later for detecting 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 though, so you’ve set dynamic = false
which prevents it from being affected by physical forces.
The categoryBitMask
, collisionBitMask
and contactTestBitMask
values will be used later for detecting collisions. The category bitmask defines a collision group for the sprite, and the contact bitmask defines which other groups this sprite can collide with. The collision bitmask is used for modelling realistic collisions (where objects bounce off of one another), but we don’t actually want to do that for the crocodile, so we disable it by setting the value to zero.
You may have noticed that the the physics body for the crocodile is being initialized using an SKTexture
object. Prior to iOS 8, to specify a collision region for a non-rectangular sprite, you would have needed to use a CGPath
to approximate the shape of your sprite image. This was a fiddly, manual process. In iOS 8, Sprite Kit added the option to specify the collision shape using a texture image. Sprite Kit automatically uses this image to generate a collision detection polygon that closely matches the desired shape.
You could simply re-use CrocMouthOpenImage
for the collision texture, but in this case, by using a separate mask image you can finely tune the collidable area to improve the gameplay. The CrocMaskImage
texture just includes the crocodile’s head and mouth, because a croc can’t eat a pineapple with its tail!
Now it’s time to animate the crocodile. Find the animateCrocodile()
method and add the following code:
let frames = [ SKTexture(imageNamed: CrocMouthClosedImage), SKTexture(imageNamed: CrocMouthOpenImage), ] let duration = 2.0 + drand48() * 2.0 let move = SKAction.animateWithTextures(frames, timePerFrame:0.25) let wait = SKAction.waitForDuration(duration) let rest = SKAction.setTexture(frames[0]) let sequence = SKAction.sequence([wait, move, wait, rest]) crocodile.runAction(SKAction.repeatActionForever(sequence)) |
This code creates an array of SKTexture
frames which you then animate using SKAction
s. In this case the array only contains two frames, but you could easily extend this principle for more complex animations.
The SKAction.sequence()
constructor creates a sequence of actions from an array. In this case, the texture animation is combined in sequence with a randomly-chosen delay period between 2 and 4 seconds.
The sequence action is wrapped in a repeatActionForever()
action, so that it will repeat indefinitely for the duration of the level. It is then run on the crocodile node using the node’s runAction(action:)
method.
That’s it! Prepare yourself to see a hungry-looking crocodile hopefully opening and closing its jaws.
Build and run the project to see this fierce reptile in action!
That’s pretty scary, right? As the player, it’s your job to keep this guy happy with pineapples, which (as everyone knows) are a crocodile’s favorite food!
You’ve got scenery and you’ve got a croc, now we need a pineapple.
Eyes on the Prize
Open up the GameScene.swift file again, and locate the setUpPrize()
method. Add the following:
prize = SKSpriteNode(imageNamed: PrizeImage) prize.position = CGPointMake(size.width * 0.5, size.height * 0.7) prize.zPosition = Layer.Prize prize.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: PrizeImage), size: prize.size) prize.physicsBody?.categoryBitMask = Category.Prize prize.physicsBody?.collisionBitMask = 0 prize.physicsBody?.contactTestBitMask = Category.Rope prize.physicsBody?.dynamic = PrizeIsDynamicsOnStart addChild(prize) |
Like the crocodile, the pineapple sprite uses a physics body. But unlike the crocodile, we want the pineapple to fall and bounce around realistically. Instead of merely setting dynamic = true
though (which would be redundant anyway, since that’s the default), it’s set to the constant PrizeIsDynamicsOnStart
that you set earlier. What’s that all about?
Don’t worry! All will be revealed later!
Let’s Get Physical
Sprite Kit makes use of iOS’ built-in physics engine (which, our spies tell us, is really the popular open-source Box 2D physics engine running behind the scenes). If you’re familiar with Cocos-2D, you may already have used Box 2D for implementing 2D game physics. The big difference when using Box 2D physics in Sprite Kit is that Apple has fully encapsulated the library in a Cocoa wrapper, so you won’t need to use C++ to interact with it.
To get started, locate the setUpPhysics()
method inside of GameScene.swift and add the following three lines:
physicsWorld.contactDelegate = self physicsWorld.gravity = CGVectorMake(0.0,-9.8) physicsWorld.speed = 1.0 |
This sets up the physics world’s contact delegate, gravity and speed. The gravity and speed values actually match the defaults for their respective properties. The former specifies the gravitational acceleration applied to physics bodies in the world, while the latter specifies the speed at which the simulation executes.
Since they’re the default values, you don’t really need to specify them here, but it’s good to know they exist in case you want to tweak your physics. Both of these properties can be found in the SKPhysicsWorld Class Reference.
You’ll see a compiler error on the first line because you’ve specified self
as the the physics delegate, but GameScene
doesn’t conform to the SKPhysicsContactDelegate
protocol. Fix that by adding that protocol to the class definition, like this:
class GameScene: SKScene, SKPhysicsContactDelegate { |
Build and run the app again. You should see the pineapple sail past the crocodile and fall into the water (behind the water, technically). That’s progress, but clearly not what we’re aiming for. You need to find a way to suspend the pineapple from the trees. It’s time to add the ropes.
Learn the Ropes
Adding a rope is not quite as straightforward as an ordinary sprite. The physics bodies used in Sprite Kit are designed to model rigid objects, but ropes are not rigid, they bend!
For this reason, you’ll implement each rope as an array of segments with flexible joints. In reality, they are more like chains than ropes, but it’s our game and we’ll call them what we like!
Positioning the rope segments manually would be extremely tedious and repetitive, and since performing repetitive tasks is what you learned to program computers for (!) you’re going to get the computer to do that work for you instead.
Each rope has three significant attributes:
anchorPoint
– a CGPoint indicating where the end of the rope connects to the treelength
– an Int representing the number of segments in the ropename
– a String used to identify which rope a given segment belong to (more on this later)
In this tutorial, the game has only one level. But in a real game you would 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, by storing it in a data file, perhaps using a property list or JSON.
For this reason it is convenient if the data can be modelled and manipulated independently of the the actual sprite nodes that make up the ropes on screen. Does that idea sound familiar at all? Right! It’s the MVC pattern!
Sprite Kit, like UIKit, follows the MVC pattern when it comes to creating game interfaces. The SKScene
is your controller and the SKNodes
are views. What you need now is a model to represent the rope data.
Since we’ll be loading our rope data from a file, the natural structure for representing the rope data is an NSArray
of NSDictionary
objects, which can be easily read from a property list using the NSArray(contentsOfFile:)
initializer.
It’s not generally good practice to store complex data in your app as plain old arrays or dictionaries, but since this data will only exist briefly while you are setting up the rope sprites, it’s not really worth creating a bespoke class or struct to store it.
In GameScene.swift, locate setUpRopes()
and add the following block of code:
// load rope data let dataFile = NSBundle.mainBundle().pathForResource(RopeDataFile, ofType: nil) let ropes = NSArray(contentsOfFile: dataFile!) // add ropes for i in 0..<ropes.count { // create rope let ropeData = ropes[i] as NSDictionary let length = Int(ropeData["length"] as NSNumber) * Int(UIScreen.mainScreen().scale) let relAnchorPoint = CGPointFromString(ropeData["relAnchorPoint"] as String) let anchorPoint = CGPoint(x: relAnchorPoint.x * self.view!.bounds.size.width, y: relAnchorPoint.y * self.view!.bounds.size.height)) let rope = RopeNode(length: length, anchorPoint: anchorPoint, name: "\(i)") // add to scene rope.addToScene(self) // connect the other end of the rope to the prize rope.attachToPrize(prize) } |
First, the code above loads the rope data from a property list. Take a look at the RopeData.plist file (inside Resources/Data, and you should see that the file contains an array of dictionaries, each containing a relAnchorPoint and length (expand the nodes in the property list editor to see the sub-structure):
The for
loop iterates over the indexes in the array. The reason for iterating over the indexes instead of just the array objects is that we need the index value in order to generate a unique name string for each rope. This will be important later.
For each rope dictionary, the relAnchorPoint and length are retrieved and used to initialize a new RopeNode
object. Length is specified in the plist measured in @1x, so you need to multiply by the actual screen scale. You do this because a “fixed” length would either be too loose (for lower resolutions) or too tight (for higher resolutions such as the 6 Plus). Similarly, relAnchorPoint is converted to a position on the screen relative the the screen’s bounds. This is to ensure that our points fall nicely on the trees for all device sizes. Finally, you attach the RopeNode
to the scene with addToScene()
, and then attach to the prize using attachToPrize()
.
If you try to run this code, nothing new will happen because the RopeNode
class is just a stub. Let’s go and fix that now.
In a Class of its Own
Open RopeNode.swift. RopeNode is a custom class that inherits from SKNode
. It doesn’t have any visual appearance of its own (which is why it doesn’t inherit from SKSpriteNode
), but instead acts as container for a collection of SKSpriteNode
s representing the rope segments.
Add the following properties to the class definition:
private let length: Int private let anchorPoint: CGPoint private var ropeSegments: [SKNode] = [] |
You’ll see some errors appearing because the length
and anchorPoint
properties haven’t been initialized. You’ve declared them as non-optional, but not assigned a value. Fix this by replacing the implementation of the init(length:anchorPoint:name:)
method with the following:
self.length = length self.anchorPoint = anchorPoint super.init() self.name = name |
Pretty straightforward, but for some reason there are still errors. There is a second initializer method, init(coder:)
– you aren’t calling that anywhere, so what is it for?
Because SKNode
implements the NSCoding
protocol, it inherits the required init(coder:)
method, and that means you have to initialize your non-optional properties there as well, because even though you aren’t using it, Swift won’t allow any possibility of a class being incorrectly initialized.
So you’d better update the init(coder:)
method too:
length = aDecoder.decodeIntegerForKey("length") anchorPoint = aDecoder.decodeCGPointForKey("anchorPoint") super.init(coder: aDecoder) |
And while you’re at it, may as well add the encodeWithCoder()
method as well, because it’s poor form to leave a job half finished!
override func encodeWithCoder(aCoder: NSCoder) { aCoder.encodeInteger(length, forKey: "length") aCoder.encodeCGPoint(anchorPoint, forKey: "anchorPoint") super.encodeWithCoder(aCoder) } |
You won’t be using NSCoding
to serialize the scene in this tutorial, but if you want to add the ability to save and load the game in future, these methods might well prove useful.
Next, you need to implement the addToScene()
method. This is where the real magic happens! This is a complex method, so you’ll write it in stages. First, find addToScene()
and add the following:
// add rope to scene zPosition = Layer.Rope scene.addChild(self) |
This adds the rope to the scene, and sets its z-offset, but not much else. Next, add this block of code to the method:
// create rope holder let ropeHolder = SKSpriteNode(imageNamed: RopeHolderImage) ropeHolder.position = anchorPoint ropeHolder.zPosition = Layer.Rope ropeSegments.append(ropeHolder) addChild(ropeHolder) ropeHolder.physicsBody = SKPhysicsBody(circleOfRadius: ropeHolder.size.width / 2) ropeHolder.physicsBody?.dynamic = false ropeHolder.physicsBody?.categoryBitMask = Category.RopeHolder ropeHolder.physicsBody?.collisionBitMask = 0 ropeHolder.physicsBody?.contactTestBitMask = Category.Prize |
This creates the rope holder, which is basically a nail for the rope to hang from. Like the crocodile, this is a static body (as specified by the dynamic = false
) which means that it won’t be affected by gravity, and won’t get moved around by the forces applied by the other nodes.
The rope holder is circular, so although you could define its collision shape using an image texture, it’s a bit more efficient just to use the SKPhysicsBody(circleOfRadius:)
constructor instead. The position of the rope holder matches the anchorPoint
that you specified when creating the RopeModel
.
Next, you’ll create the rope itself. Add the following code:
// add each of the rope parts for i in 0..<length { let ropeSegment = SKSpriteNode(imageNamed: RopeTextureImage) let offset = ropeSegment.size.height * CGFloat(i + 1) ropeSegment.position = CGPointMake(anchorPoint.x, anchorPoint.y - offset) ropeSegment.name = name ropeSegments.append(ropeSegment) addChild(ropeSegment) ropeSegment.physicsBody = SKPhysicsBody(rectangleOfSize: ropeSegment.size) ropeSegment.physicsBody?.categoryBitMask = Category.Rope ropeSegment.physicsBody?.collisionBitMask = Category.RopeHolder ropeSegment.physicsBody?.contactTestBitMask = Category.Prize } |
This loop creates an array of rope segments, equal in number to the length you specified when creating the RopeModel
. Each segment is a sprite, with its own physics body. The segments are rectangular, so, as with the rope holder, you can make Sprite Kit work a little less hard by using SKPhysicsBody(rectangleOfSize:)
to specify the physics body shape, instead of using the rope texture image.
Unlike the rope holder, the rope nodes are dynamic, so they can move around and are affected by gravity.
Try building and running the app now.
Uh oh! That probably wasn’t quite what we were expecting – the rope segments all fell off the screen like finely chopped spaghetti. The problem is that you haven’t joined them together yet.
To fix that, you need to add this final chunk of code to addToScene()
:
// set up joints between rope parts for i in 1...length { let nodeA = ropeSegments[i - 1] let nodeB = ropeSegments[i] let joint = SKPhysicsJointPin.jointWithBodyA(nodeA.physicsBody, bodyB: nodeB.physicsBody, anchor: CGPointMake(CGRectGetMidX(nodeA.frame), CGRectGetMinY(nodeA.frame))) scene.physicsWorld.addJoint(joint) } |
This code sets up physical joints between the segments, connecting them together. The type of joint you’ve used is an SKPhysicsJointPin
, which inherits from the SKPhysicsJoint
base class. A pin joint behaves as if you had hammered a pin through the two nodes, allowing them pivot, but not move closer or further apart from one another.
OK, so now if you build and run again, your ropes should hang realistically from the trees.
The final step is to attach the ropes to the pineapple. You already added the call to call attachToPrize()
– now you just need to write that method. Open up RopeNode.swift again, and scroll down to attachToPrize()
. Add the following code:
// align last segment of rope with prize let lastNode = ropeSegments.last! lastNode.position = CGPointMake(prize.position.x, prize.position.y + prize.size.height * 0.1) // set up connecting joint let joint = SKPhysicsJointPin.jointWithBodyA(lastNode.physicsBody, bodyB: prize.physicsBody, anchor: lastNode.position) prize.scene?.physicsWorld.addJoint(joint) |
The code first gets the last segment of the rope and positions it slightly above the center of the prize. We 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.
Finally, the code creates another pin joint to attach the rope segment to the prize.
Because you’ve only moved the last segment of the rope into position, you might think it would create a gap between the last two segments. Not so, however. The beauty of having a realistic physics engine is that won’t allow the laws of physics to be violated – because the segments of the rope are all joined together, moving one now moves them all.
Build and run the project. If all your joints and nodes are set up properly, you should see a screen similar to the one below:
It looks good. But the pineapple and ropes sure do bounce around a lot when the game first starts, right?
The problem is, you’re starting the pineapple off in an essentially arbitrary position, and letting the physics engine drop it into place. There’s a way to fix that so that the level doesn’t start quite so… energetically.
You may recall that when you created the pineapple sprite, you set its dynamic
property to the constant PrizeIsDynamicsOnStart
, which is currently set to true. The reason for that is that you weren’t sure where the pineapple would end up once the physics had resolved itself, so the position you placed the pineapple initially was just approximate.
But now that the joints are all in place, you can find out where the pineapple is destined to end up, and position it there to begin with. Run the app again, and once the pineapple has settled down and stopped bouncing, in Xcode, while the game is still running, find the update()
stub function in GameScene.swift and add a breakpoint by clicking in the left-hand margin (see picture below):
The game should break almost instantly at this line. Now, in the debug console, type po prize.position
, and you should see the value logged out as something like (x=178.95286560058594, y=395.62783813476563).
Now locate setUpPrize()
and find this line:
prize.position = CGPointMake(size.width * 0.5, size.height * 0.7) |
Change it to match whatever values you saw in the console. For example:
prize.position = CGPointMake(178.95286560058594, 395.62783813476563) |
You can remove your breakpoint now.
In Constants.swift, change the value of PrizeIsDynamicsOnStart
to false, and run the game again.
This looks much better! The ropes still bounce around a bit, but the pineapple is already sitting in the right position instead of bouncing into place when the game begins.
Making the Cut
The game can’t be called Cut the Verlet if your Verlets, uh, ropes have no fear of being cut, right?
In this section, you’re going to learn how to work with the touch methods that will allow your player to cut those ropes.
If you’ve worked with a UIView
before, you may have come across iOS’s touch handling methods. If not, they are well explained here.
Although SKScene
does not inherit from UIView
, it does inherit from UIResponder
, which makes the same touch handling methods available. For now, you’ll only need touchesMoved
.
Locate touchesMoved(_:withEvent:)
and add the following code:
for touch in touches { let startPoint = touch.locationInNode(self) let endPoint = touch.previousLocationInNode(self) // check if rope cut scene?.physicsWorld.enumerateBodiesAlongRayStart(startPoint, end: endPoint, usingBlock: { (body, point, normal, stop) -> Void in self.checkIfRopeCutWithBody(body) }) // produce some nice particles let emitter = SKEmitterNode(fileNamed: "Particle.sks") emitter.position = startPoint emitter.zPosition = Layer.Rope addChild(emitter) } |
This code works as follows: For each touch (we support multitouch slicing in this game) it gets the current and previous positions of the touch. Next, it loops through all of the bodes in the scene that lie between those two points, using the very handy enumerateBodiesAlongRayStart(_:end:usingBlock:)
method of SKScene
. For each body encountered it calls checkIfRopeCutWithBody()
, which you’ll write in a minute.
Finally, the code creates an SKEmitterNode by loading it from the Particle.sks file, 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. This is pure eye-candy, but it does make the game look 100x better!
Particle emitters are not within the scope of this tutorial, but now that you know they exist… you’ve got something else to do later!
Scroll down to the checkIfRopeCutWithBody()
method, and add this block of code to the method body:
let node = body.node! // if it has a name it must be a rope node if let name = node.name { //enable prize dynamics prize.physicsBody?.dynamic = true // cut the rope node.removeFromParent() // fade out all nodes matching name self.enumerateChildNodesWithName(name, usingBlock: { (node, stop) in let fadeAway = SKAction.fadeOutWithDuration(0.25) let removeNode = SKAction.removeFromParent() let sequence = SKAction.sequence([fadeAway, removeNode]) node.runAction(sequence) }) } |
The code above first checks if the node connected to the physics body has a name. Remember that there are other nodes in the scene besides rope 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 rope node segments, so if the node has a name then we can be certain that it’s part of a rope.
Once you know that the node you’ve swiped is a rope, the first thing you do is to enable the dynamic
property of the prize so that it can react properly after the rope is cut. (Depending on the value of the PrizeIsDynamicsOnStart
constant, the prize may already be dynamic, but if not this will set it moving.)
Next, you remove the node from the scene. Removing a node also removes its physicsBody
, and destroys any joints connected to it. The rope has now officially been cut!
Finally, you enumerate through all nodes in the scene whose name matches the name of the node that was swiped, using the scene’s enumerateChildNodesWithName(_:usingBlock:)
. The only nodes whose name should match are the other segments in the same rope, so you are basically just looping over the nodes in whichever rope was sliced.
For each node, you create an SKAction
sequence that first fades out the node, then removes it from the scene. The effect is that immediately after being sliced, the rope will fade away gracefully. I think this looks a bit nicer than leaving the ends flapping, but if you disagree, you can remove this enumeration block to leave the severed ends on screen. You could even add another gameplay constant to make that option configurable!
Build and run the project. You should be able to swipe and cut all three ropes and see the prize fall. By the way, aren’t those particles awesome?!
When Two Nodes Collide
You’re almost done! You’ve got a rope you can cut, but nothing happens once the pineapple falls. Even if it lands dead-center on the croc, it just falls through her, because the croc and prize’s collision masks are both set to zero.
If you changed the collision masks, the pineapple would bounce off, but that isn’t what you want either. You don’t want to realistically model the collision, you want to intercept it and handle it yourself. That’s where the SKPhysicsContactDelegate
comes in.
You may recall that when you wrote the setUpPhysics()
method, you specified that GameScene
would act as the contactDelegate
for the physicsWorld. You also added collision categories and bitmasks to the prize and croc so that the physics engine would be able to detect when they intersect each other. That was excellent foresight on your part!
Now all you need to do is implement didBeginContact()
of the SKPhysicsContactDelegate
, which will be triggered whenever an intersection is detected between two appropriately masked bodies. A stub for that method has been added for you – 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) { // fade the pineapple away let shrink = SKAction.scaleTo(0, duration: 0.08) let removeNode = SKAction.removeFromParent() let sequence = SKAction.sequence([shrink, removeNode]) prize.runAction(sequence) } |
This code checks if the two colliding bodies belong to the crocodile and prize respectively (you don’t know which order the nodes will be listed in, 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.
Chew on This!
The pineapple vanishing when it lands on the croc may not be the most compelling way to inform the player that their pet has been fed. Users will expect to see the crocodile munch down that pineapple!
You can fulfil that expectation with another animation. Inside the if
statement where you just triggered the pineapple shrink animation, add the following extra line:
runNomNomAnimationWithDelay(0.15) |
Now locate runNomNomAnimationWithDelay()
and add this code:
crocodile.removeAllActions() let openMouth = SKAction.setTexture(SKTexture(imageNamed: CrocMouthOpenImage)) let wait = SKAction.waitForDuration(delay) let closeMouth = SKAction.setTexture(SKTexture(imageNamed: CrocMouthClosedImage)) let sequence = SKAction.sequence([openMouth, wait, closeMouth]) crocodile.runAction(sequence) animateCrocodile() |
The code above removes any animation currently running on the crocodile node using removeAllActions()
. It then creates a new animation sequence that opens and closes the crocodile’s mouth and runs this sequence on the crocodile
. It then calls animateCrocodile()
to resume the idle opening and closing of the crocodile’s jaws.
This new animation will be triggered when the prize lands in the croc’s mouth. The 0.15 second delay is so that it triggers slightly after the pineapple has disappeared, giving the impression that the crocodile is chewing it.
While you’re at it, add the following method just before the touchesMoved(_:withEvent:)
method you filled in earlier:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { runNomNomAnimationWithDelay(1) } |
This will trigger the same chewing animation every time the user cuts a rope. The animation gives the illusion that the crocodile is opening its mouth in hope something yummy will fall into it.
Build and run.
Second Serving
The happy croc will now chew up the pineapple if it lands in her mouth. But once that’s happened, the game just hangs there. The hungry croc won’t be satisfied by just one pineapple, so you need a way to reset the game so you can keep on feeding her!
In GameScene.swift, find switchToNewGameWithTransition()
, and add the following block of code:
let delay = SKAction.waitForDuration(1) let transition = SKAction.runBlock({ let scene = GameScene(size: self.size) self.view?.presentScene(scene, transition: transition) }) runAction(SKAction.sequence([delay, transition])) |
The code above uses SKView
’s presentScene(scene:, transition:)
method to present the next scene.
In this case, the scene you are transitioning to is a new instance of the same GameScene
class. You also pass in a transition effect using the SKTransition
class. The transition is specified as an argument to the method so that you can use different transition effects depending on the outcome of the game.
Scroll back to didBeginContact()
, and inside the if
statement, after the prize fade and nomnom animations, add the following:
// transition to next level switchToNewGameWithTransition(SKTransition.doorwayWithDuration(1.0)) |
This calls the switchToNewGameWithTransition()
method we added earlier, using the SKTransition.doorwayWithDuration(duration:)
constructor to create a doorway transition. This shows the next level with an effect like a door opening. Pretty neat, huh?
That’s all well and good, but if the pineapple doesn’t land in the croc’s mouth, you’re still stuck. You need a way to detect that the prize has fallen into the water, and that all hope of the poor crocodile being fed is lost.
Heads: You Win, Tails: You Lose
You might be thinking that you need to add another physics body to the water so you can detect if the prize hits it, but that wouldn’t really help if the pineapple were to fly 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, and then end the game.
SKScene
provides an update()
method that is called once every frame. Find that method, and add the following logic:
if prize.position.y <= 0 { let transitions = [ SKTransition.doorsOpenHorizontalWithDuration(1.0), SKTransition.doorsOpenVerticalWithDuration(1.0), SKTransition.doorsCloseHorizontalWithDuration(1.0), SKTransition.doorsCloseVerticalWithDuration(1.0), SKTransition.flipHorizontalWithDuration(1.0), SKTransition.flipVerticalWithDuration(1.0), SKTransition.moveInWithDirection(.Left, duration:1.0), SKTransition.pushWithDirection(.Right, duration:1.0), SKTransition.revealWithDirection(.Down, duration:1.0), SKTransition.crossFadeWithDuration(1.0), SKTransition.fadeWithColor(UIColor.darkGrayColor(), duration:1.0), SKTransition.fadeWithDuration(1.0), ] // transition to next level let randomIndex = arc4random_uniform(UInt32(transitions.count)) switchToNewGameWithTransition(transitions[Int(randomIndex)]) } |
The if
statement logic here is quite simple – it just checks if the prize’s y coordinate is less than zero (the bottom of the screen). If so, it calls switchToNewGameWithTransition()
to start the level again.
Rather than always using the same transition effect, the code selects a random transition by using arc4random_uniform()
to index into an array of all of the available transitions (except doorwayWithDuration
, the transition we used for the success scenario), so you should see a different transition after each game-over.
Now build and run the project.
You should see the scene transition back to the starting conditions whenever the player scores a point or loses the prize.
The Sound of Music
While the game is technically complete, it lacks a certain pizazz. A silent game may quickly bore your users. It’s time to make it sing!
We’ve selected a nice jungle song from incompetech.com and some sound effects from freesound.org.
Sprite Kit will handle the sound effects for you, but to play the background music you’re going to use an AVAudioPlayer
. The reason for this is that because this game will play music in the background, we want the music to continue playing seamlessly between level transitions, and an SKAction
would be destroyed when the scene it’s running in gets replaced.
AVAudioPlayer
isn’t directly related to Sprite Kit, so this tutorial won’t cover it in detail. To learn more about AVAudioPlayer
, check out our Audio Tutorial for iOS.In a real game, with more complex audio requirements, you would probably create a singleton music manager class that could handled all of your music sequencing requirements. In this case however, you can get away with simply storing the AVAudioPlayer
in a global variable, that way it will persist and continue playing even when you transition between different scene instances.
To create a global variable, you declare it outside of any class or struct. Add the following code inside GameScene.swift, immediately after the import statements, but before the GameScene class definition:
private var backgroundMusicPlayer: AVAudioPlayer! |
private
, which restricts its scope to the file in which it was declared. Private global variables in Swift work like static variables in Objective-C implementation files. They persist for the duration of the app, but they don’t encourage the same sort of spaghetti code that can occur when using public
or internal
-scoped globals.Locate the setUpAudio()
method and add the following code:
if (backgroundMusicPlayer == nil) { let backgroundMusicURL = NSBundle.mainBundle().URLForResource(BackgroundMusicSound, withExtension: nil) backgroundMusicPlayer = AVAudioPlayer(contentsOfURL: backgroundMusicURL, error:nil) backgroundMusicPlayer.numberOfLoops = -1 } |
The code above checks if the global backgroundMusicPlayer
instance has been set yet. If not, it initializes a new AVAudioPlayer
with the BackgroundMusicSound
constant that you added to Constants.swift earlier (which is converted to an NSURL
), and assigns it to the property. The numberOfLoops
vale is set to -1, which indicates that the song should loop indefinitely.
You could simply recreate the player each time a scene is loaded instead of performing this nil check, but AVAudioPlayer
retains itself while playing, so creating a new player each time the level loads without stopping the old one would result in a “wall of sound”, with many copies of the background music playing simultaneously.
Next, add this code:
if (!backgroundMusicPlayer.playing) { backgroundMusicPlayer.play() } |
This starts the background music playing when the scene is first loaded (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 is already playing when the level begins.
While you’re here, you may as well set up all the sound effects that you will be using later. Unlike the music, you don’t want to play the sound effects right away. Instead, you’ll create some reusable SKAction
s that can be used to 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:
sliceSoundAction = SKAction.playSoundFileNamed(SliceSound, waitForCompletion: false) splashSoundAction = SKAction.playSoundFileNamed(SplashSound, waitForCompletion: false) nomNomSoundAction = SKAction.playSoundFileNamed(NomNomSound, waitForCompletion: false) |
This code initializes the sound actions using SKAction
’s playSoundFileNamed(_:waitForCompletion:)
method. Now it’s time to actually add the sound effects.
Scroll up to update()
and add the following line of code inside the if
statement, just above where the array of transitions are declared:
runAction(splashSoundAction) |
That will play the sound of a splash when the pineapple lands in the water. Next, find didBeginContact(contact:)
and add the following line of code just below the runNomNomAnimationWithDelay(0.08)
line:
runAction(nomNomSoundAction) |
That will play a chomping sound when the croc catches her prize. Finally, locate touchesBegan(_:withEvent:)
and add the following line of code just below the runNomNomAnimationWithDelay(1)
line:
runAction(sliceSoundAction) |
That will play a swiping sounds whenever the player touches the screen.
Build and run the project.
The discerning player may notice a slight sound bug: If you miss the croc, the splashing sound plays multiple times. This is due to level complete logic being triggered 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 levelOver = false |
Now modify update()
and didBeginContact()
by adding the following code at the top of each of them:
if levelOver { return } |
Finally, inside the if
statements of both methods, add some code to set the levelOver state to true:
levelOver = true |
Now, as soon as the game detects that the levelOver
flag has been set (either because the pineapple hit the ground, or the croc got her meal), it will stop checking for the game win/lose scenarios, and won’t keep repeatedly trying to play those sound effects. Build and run. There are no awkward sound collisions anymore, and you’ll soon have one very stuffed crocodile!
To The Max!
After playing a few rounds, the game seems a bit on the easy side. You’ll pretty quickly get to the point where you can feed the croc with a single well-timed slice through the three ropes. Suppose you want to make the game a bit harder – how might you do that?
The game is trickier if you slice a couple of ropes first and then have to time the last slice while the rope is swinging. It would be neat to re-engineer the game so that only one rope can be sliced at a time? Good thing you added that CanCutMultipleRopesAtOnce
constant, right? ;-)
In GameScene.swift, add one last property at the top of the GameScene
class definition:
private var ropeCut = false |
Now locate the checkIfRopeCutWithBody(body:)
method, and add the following if
statement at the top of the method:
if ropeCut && !CanCutMultipleRopesAtOnce { return } |
At the bottom of the if let
statement below, just before the closing bracket, add this line:
ropeCut = true |
Finally, find touchesBegan(_:withEvent:)
, and add this to reset the ropeCut
flag whenever the user tocuehs the screen:
ropeCut = false |
Build and run the game again. You should see now that it is only possible to slice one rope each time you swipe. To cut another, you have to lift your finger and then swipe again.
I think the game is a bit more fun this way, but if you disagree, there’s no need to modify the code again, just flip the value of the CanCutMultipleRopesAtOnce
configuration constant to true.
Where Do We Go From Here?
I hope you enjoyed working through this tutorial as much as I’ve enjoyed writing it. To compare notes, download the completed sample project here.
But, don’t let the fun stop here! Try adding new levels, different ropes, and maybe even a HUD with a score display and timer. Why not!? It’s only code!
If you’d like to learn more about Sprite Kit, be sure to check out our book, iOS Games by Tutorials.
If you have any questions or comments, feel free to join in the discussion below!
Create a Game Like Cut the Rope Using Sprite Kit and Swift is a post from: Ray Wenderlich
The post Create a Game Like Cut the Rope Using Sprite Kit and Swift appeared first on Ray Wenderlich.