Ever feed a pineapple to a crocodile? In this tutorial, you’ll find out how!
Prior to iOS 7, if you wanted to make a game, you either had to know a bit of arcane Open GL magic, or rely on a third party library to do the heavy lifting for you. Once you had your graphics engine in place, often times, you need to add an additional layer of physics to it, adding another library to simulate real world behavior. This could often result in additional dependencies as well as additional language requirements (such as C or C++).
With the introduction of iOS 7, Apple changed all of this. In one fell swoop, developers now had access to a 2D graphics and physics engine, all accessible through Objective-C. Now, developers could focus on making a game as opposed to managing the architecture of one.
The game framework was called SpriteKit and in this tutorial, you are going to learn how to make a game similar to Cut the Rope, an award-winning physics-based puzzle game. You’ll learn how to:
- Add objects to scenes;
- Create animation sequences;
- Add music;
- And, most of all, how to work within Sprite Kit’s physics world!
By the end of this tutorial, you’ll be well on your way to using Sprite Kit in your own projects.
Just keep in mind that this is not an entry level level tutorial. If classes such as SKNode or SKAction are entirely new to you, then check out our Sprite Kit Tutorial for Beginners. That tutorial will get you quickly up to speed so you can start feeding pineapples to crocodiles.
Wait. What?
Read on.
Getting Started
In this tutorial, you’ll be creating a game called Cut the Verlet. This game is modeled after Cut the Rope which tasks you to cut a rope holding a candy so it will drop into the mouth of a rather hungry, albeit, impatient creature. Each level adds additional challenges such as spiders and buzz saws. This game was demonstrated in a previous tutorial using Cocos2D, but in this tutorial, you will be using Sprite Kit to build it out.
So what is a Verlet? A verlet is short for verlet integration which is way to model particle trajectories in motion and also a great tool to model your rope physics. Gustavo Ambrozio, the author of Cocos2D version of this tutorial, provides an excellent overview of verlets and how they are applied to this game. Give that section a read before continuing with this tutorial. Think of it as required reading. :]
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’s contents are in four main folders, as shown below:
- Classes contains the primary files such as the main view controller, the scene and the rope object class. You will be adding to these classes throughout this tutorial.
- Helpers contains the files responsible for maintaining the game’s data and its constants throughout the program.
- Resources/Sounds contains the project’s sound files.
- Resources/Other contains the files you need to add particles to the scene.
- Other Resources contains all the resources from third-party sources. This tutorial will be using SKUtils which is a module that was created by the iOS Game by Tutorials team.
- Images.xcassets contains the image assets for this project.
In addition, I’ve added all of the necessary #import
statements to the starter project. This includes #import
statements in the CutTheVerlet-Prefix.pch file.
Note: A .pch file is what is known as a precompiled header file. Precompiled headers are tool used to speed up the compilation of files. They also remove the requirement of importing those files when used throughout your project. Just bear in mind that pre-compiled headers can obscure dependencies between classes.
Close the Resources and Other Resources folders; you won’t be making any changes in those areas. You’ll work directly only with the files located in the Classes and Helpers folders.
It’s time to begin!
Creating Constants
A constant is a variable you can rely upon: once you set its value, that value never changes. Constants are a great way to make your code more manageable. Global constants in particular can make your code easier to read and maintain.
In this project, you’ll create global constants to define sprite texture image names, sound file names, sprite node names, the z-order or zPosition
of your sprites and the category defined for each sprite, which you’ll use for collision detection.
Open TLCSharedConstants.h and add the following code above the @interface
line:
typedef NS_ENUM(int, Layer)
{
LayerBackground,
LayerForeground,
LayerCrocodile,
LayerRope,
LayerPrize
};
typedef NS_OPTIONS(int, EntityCategory)
{
EntityCategoryCrocodile = 1 << 0,
EntityCategoryRopeAttachment = 1 << 1,
EntityCategoryRope = 1 << 2,
EntityCategoryPrize = 1 << 3,
EntityCategoryGround = 1 << 4
};
extern NSString *const kImageNameForRopeHolder;
extern NSString *const kImageNameForRopeTexture;
extern NSString *const kImageNameForCrocodileBaseImage;
extern NSString *const kImageNameForCrocodileMouthOpen;
extern NSString *const kImageNameForCrocodileMouthClosed;
extern NSString *const kSoundFileNameForCutAction;
extern NSString *const kSoundFileNameForSplashAction;
extern NSString *const kSoundFileNameForBiteAction;
extern NSString *const kSoundFileNameForBackgroundMusic;
extern NSString *const kImageNameForPrize;
extern NSString *const kNodeNameForPrize; |
The code above declares two typedef
variables of type int
: EntityCategory
and Layer
. You’ll use these to determine the collision category and zPosition
of a sprite when you add it to the scene—more about this soon.
The code also declares a group of constant NSString
variables using the const
keyword. The extern
keyword comes in handy when creating global variables, as it allows you to create unambiguous declarations. That means you can declare a variable here but set its value elsewhere.
Solution Inside: Why do programmers name constants with a ‘k’ prefix? |
SelectShow> |
There’s a small debate as to why we use k, but the general consensus is that it originates from Hungarian notation, where k designates a constant. Or is it a konstant? :]
|
Remember that part about declaring a variable here and setting it elsewhere? Well, the elsewhere in this case is TLCSharedConstants.m.
Open TLCSharedConstants.m and add the following code above the @implementation
line:
NSString *const kImageNameForRopeHolder = @"ropeHolder";
NSString *const kImageNameForRopeTexture = @"ropeTexture";
NSString *const kImageNameForCrocodileBaseImage = @"croc";
NSString *const kImageNameForCrocodileMouthOpen = @"croc01";
NSString *const kImageNameForCrocodileMouthClosed = @"croc00";
NSString *const kSoundFileNameForCutAction = @"cut.caf";
NSString *const kSoundFileNameForSplashAction = @"splash.caf";
NSString *const kSoundFileNameForBiteAction = @"bite.caf";
NSString *const kSoundFileNameForBackgroundMusic = @"CheeZeeJungle.caf";
NSString *const kImageNameForPrize = @"pineapple";
NSString *const kNodeNameForPrize = @"pineapple"; |
Here you use string values to set the names of images and sound clips. If you’ve played Cut the Rope, you can probably figure out what the variable names represent. You also set a string value for the sprite node name that you’ll use in the collision detection methods, which the Collision Detection section of this tutorial will explain.
Now that you’ve got your constants in place, you can begin adding nodes to your scene, starting with the scenery itself—the background and foreground!
Note: While it may seem a bit premature to add some of these things, it’ll be easier to follow the tutorial if you define them right from the start. You’ll discover more about each one as you use them.
Adding the Background and Foreground
The starter project provides stub versions of the project’s methods—adding the code is your job. The first steps are to initialize the scene and add a background.
Open TLCMyScene.m and add the following properties to the interface declaration:
@property (nonatomic, strong) SKNode *worldNode;
@property (nonatomic, strong) SKSpriteNode *background;
@property (nonatomic, strong) SKSpriteNode *ground;
@property (nonatomic, strong) SKSpriteNode *crocodile;
@property (nonatomic, strong) SKSpriteNode *treeLeft;
@property (nonatomic, strong) SKSpriteNode *treeRight; |
Here you define properties to hold references to the different nodes in the scene.
Now add the following block of code to initWithSize:
, just after the comment that reads /* add setup here */
:
self.worldNode = [SKNode node];
[self addChild:self.worldNode];
[self setupBackground];
[self setupTrees]; |
The code above creates an SKNode
object and assigns it to the world
property. It then adds the node to the scene using addChild:
.
It also calls two methods, one for setting up the background and one for setting up the two trees. Because the two methods are almost identical, I’ll explain them together once you’ve added them.
First, locate setupBackground
and add the following:
self.background = [SKSpriteNode spriteNodeWithImageNamed:@"background"];
self.background.anchorPoint = CGPointMake(0.5, 1);
self.background.position = CGPointMake(self.size.width/2, self.size.height);
self.background.zPosition = LayerBackground;
[self.worldNode addChild:self.background];
self.ground = [SKSpriteNode spriteNodeWithImageNamed:@"ground"];
self.ground.anchorPoint = CGPointMake(0.5, 1);
self.ground.position = CGPointMake(self.size.width/2, self.background.frame.origin.y);
self.ground.zPosition = LayerBackground;
[self.worldNode addChild:self.ground];
SKSpriteNode *water = [SKSpriteNode spriteNodeWithImageNamed:@"water"];
water.anchorPoint = CGPointMake(0.5, 1);
water.position = CGPointMake(self.size.width/2, self.ground.frame.origin.y + 10);
water.zPosition = LayerBackground;
[self.worldNode addChild:water]; |
Next, locate setupTrees
and add this code:
self.treeLeft = [SKSpriteNode spriteNodeWithImageNamed:@"treeLeft"];
self.treeLeft.anchorPoint = CGPointMake(0.5, 1);
self.treeLeft.position = CGPointMake(self.size.width * .20, self.size.height);
self.treeLeft.zPosition = LayerForeground;
[self.worldNode addChild:self.treeLeft];
self.treeRight = [SKSpriteNode spriteNodeWithImageNamed:@"treeRight"];
self.treeRight.anchorPoint = CGPointMake(0.5, 1);
self.treeRight.position = CGPointMake(self.size.width * .86, self.size.height);
self.treeRight.zPosition = LayerForeground;
[self.worldNode addChild:self.treeRight]; |
Now that everything is in place, it’s time to explain.
In setupBackground
and setupTrees
, you create an SKSpriteNode
and initialize it using spriteNodeWithImageNamed:
, whereby you pass in the image name and assign it to its equivalently-named variable. In other words, you initialize the property variables.
You then change each of the anchorPoints
from the default value of (0.5, 0.5) to a new value of (0.5, 1).
Note: For more information about the unit coordinate system, review
Working with Sprites in Apple’s Sprite Kit Programming Guide.
You also set the sprite’s position
(location) and zPosition
(depth). For the most part, you’re only taking the size of the scene’s width and height into consideration when you set the sprite’s position.
The ground
sprite, however, needs to be positioned directly under the edge of the background. You accomplish this by getting a handle on the background’s frame using self.background.frame.origin.y
.
Likewise, you want the water sprite, which isn’t using a declared variable, directly under the ground
sprite with a little space in between. You achieve this using self.ground.frame.origin.y + 10
.
Recall that in TLCSharedConstants.h, you specified some constants for use with the sprite’s zPosition
. You use two of them in the code above: LayerBackground
and LayerForeground
. Since SKSpriteNode
inherits from SKNode
, you have access to all of SKNode’s properties, including zPosition
.
Finally, you add all of the newly created sprites to your world node.
While it’s possible to add sprites directly to a scene, creating a world node to contain things will be better in the long run, especially because you’re going to apply physics to the world.
You’ve got official approval for your first build and run! So… what are you waiting for?
Build and run your project. If you did everything right, you should see the following screen:
It’s a lonely world out there. It’s time to bring out the crocodiles!
Adding and Animating the Crocodile Node
Adding the crocodile node is not much different from adding the background and foreground.
Locate setupCrocodile
inside of TLCMyScene.m and add the following block of code:
self.crocodile = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForCrocodileMouthOpen];
self.crocodile.anchorPoint = CGPointMake(0.5, 1);
self.crocodile.position = CGPointMake(self.size.width * .75, self.background.frame.origin.y + (self.crocodile.size.height - 5));
self.crocodile.zPosition = LayerCrocodile;
[self.worldNode addChild:self.crocodile];
[self animateCrocodile]; |
The code above uses two of the constants you set up earlier: kImageNameForCrocodileMouthClosed
and LayerCrocodile
. It also sets the position
of the crocodile node based on the background node’s frame.origin.y
location and the crocodile node’s size.
Just as before, you set the zPosition
to place the crocodile node on top of the background and foreground. By default, Sprite Kit will “layer” nodes based on the order in which they’re added. You can choose a node’s depth yourself by giving it a different zPosition
.
Now it’s time to animate the crocodile
.
Find animateCrocodile
and add the following code:
NSMutableArray *textures = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i <= 1; i++) {
NSString *textureName = [NSString stringWithFormat:@"%@0%d", kImageNameForCrocodileBaseImage, i];
SKTexture *texture = [SKTexture textureWithImageNamed:textureName];
[textures addObject:texture];
}
CGFloat duration = RandomFloatRange(2, 4);
SKAction *move = [SKAction animateWithTextures:textures timePerFrame:0.25];
SKAction *wait = [SKAction waitForDuration:duration];
SKAction *rest = [SKAction setTexture:[textures objectAtIndex:0]];
SKAction *animateCrocodile = [SKAction sequence:@[wait, move, wait, rest]];
[self.crocodile runAction: [SKAction repeatActionForever:animateCrocodile]]; |
The previous code creates an array of SKTexture
objects which you then animate using SKAction
objects.
You also use a constant to set the base image name and set the number of images in your animation using a for
loop. There are only two images for this animation, croc00 and croc01. Finally, you use a series of SKAction
objects to animate the crocodile node.
An SKAction sequence:
allows you to set multiple actions and run them as… you guessed it… a sequence!
Once you’ve established the sequence, you run the action on the node using the node’s runAction
method. In the code above, you use repeatActionForever:
to instruct the node to animate indefinitely.
The final step in adding and animating your crocodile is to make the call to setupCrocodile
. You’ll do this in initWithSize:
.
Toward the top of TLCMyScene.m, locate initWithSize:
and add the following line after [self setupTrees];
:
That’s it! Prepare yourself to see a mean-looking crocodile wildly open and shut its jaws in the hope of eating whatever may be around.
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 pineapple, which everyone knows is a crocodile’s favorite food. ;]
If your screen doesn’t look like the one above, you may have missed a step somewhere along the way.
You’ve got scenery and you’ve got a player, so let’s institute some ground rules to get this party started—physics!
Adding Physics to Your World
SpriteKit makes use of iOS’ packaged physics engine which in reality is just Box 2D under the covers. If you’ve ever used Cocos-2D, then you may have used Box 2D for managing your physics. The big difference between using Box 2D in Sprite Kit is that Apple has encapsulated the library in an Objective-C wrapper so you won’t need to use C++ to access it.
To get started, locate initWithSize:
inside of TLCMyScene.m and add the following three lines after the [self add child:self.worldNode]
line:
self.worldNode.scene.physicsWorld.contactDelegate = self;
self.worldNode.scene.physicsWorld.gravity = CGVectorMake(0.0,-9.8);
self.worldNode.scene.physicsWorld.speed = 1.0; |
Also, add the following to the end of the @interface
line at the top of the file:
<SKPhysicsContactDelegate> |
The code above sets the world node’s contact delegate, gravity and speed. Remember, this is the node that will contain all your other nodes, so it makes sense to add your physics here.
The gravity and speed values above are 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 need to specify them above, 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.
Get Ready for the Ropes!
Now for the part you’ve been eagerly anticipating… the ropes! Excuse me—I mean, the verlets.
This project uses the TLCGameData
class as a means of setting up the ropes. In a production environment, you’d likely use a PLIST or some other data store to configure the levels.
In a moment, you’re going to create an array of TLCGameData
objects to represent your datastore.
Open TLCGameData.h and add the following properties:
@property (nonatomic, assign) int name;
@property (nonatomic, assign) CGPoint ropeLocation;
@property (nonatomic, assign) int ropeLength;
@property (nonatomic, assign) BOOL isPrimaryRope; |
These will serve as your data model. Again, in a production environment, you’d benefit from using a PLIST rather than programmatically creating your game data.
Now go back to TLCMyScene.m and add the following after the last #import
statement:
#define lengthOfRope1 24
#define lengthOfRope2 18
#define lengthOfRope3 15 |
Then, add two properties to hold the prize and level data. Add them right after the other properties:
@property (nonatomic, strong) SKSpriteNode *prize;
@property (nonatomic, strong) NSMutableArray *ropes; |
Once you’ve done that, locate setupGameData
and add the following block of code:
self.ropes = [NSMutableArray array];
TLCGameData *rope1 = [[TLCGameData alloc] init];
rope1.name = 0;
rope1.ropeLocation = CGPointMake(self.size.width *.12, self.size.height * .94);
rope1.ropeLength = lengthOfRope1;
rope1.isPrimaryRope = YES;
[self.ropes addObject:rope1];
TLCGameData *rope2 = [[TLCGameData alloc] init];
rope2.name = 1;
rope2.ropeLocation = CGPointMake(self.size.width *.85, self.size.height * .90);
rope2.ropeLength = lengthOfRope2;
rope2.isPrimaryRope = NO;
[self.ropes addObject:rope2];
TLCGameData *rope3 = [[TLCGameData alloc] init];
rope3.name = 2;
rope3.ropeLocation = CGPointMake(self.size.width *.86, self.size.height * .76);
rope3.ropeLength = lengthOfRope3;
rope3.isPrimaryRope = NO;
[self.ropes addObject:rope3]; |
The code above sets basic parameters for your ropes. The most important is the property isPrimaryRope
, because it determines how the ropes are connected to the prize. When creating your ropes, only one should have this property set to YES
.
Finally, add two more calls to initWithSize:
: [self setupGameData]
and [self setupRopes]
. When you’re done, initWithSize:
should look like this:
if (self = [super initWithSize:size]) {
/* Setup your scene here */
self.worldNode = [SKNode node];
[self addChild:self.worldNode];
self.worldNode.scene.physicsWorld.contactDelegate = self;
self.worldNode.scene.physicsWorld.gravity = CGVectorMake(0.0,-9.8);
self.worldNode.scene.physicsWorld.speed = 1.0;
[self setupSounds];
[self setupGameData];
[self setupBackground];
[self setupTrees];
[self setupCrocodile];
[self setupRopes];
}
return self; |
Now you can build the ropes!
The Rope Class
In this section, you’ll begin creating the class that handles the ropes.
Open TLCRope.h. You’re going to add two blocks of code to this file. Add the first block, the delegate’s protocol, before the @interface
section:
@protocol TLCRopeDelegate
- (void)addJoint:(SKPhysicsJointPin *)joint;
@end |
Add the second block of code, which includes the declaration of a custom init
method, after the @interface
section:
@property (strong, nonatomic) id<TLCRopeDelegate> delegate;
- (instancetype)initWithLength:(int)length usingAttachmentPoint:(CGPoint)point toNode:(SKNode*)node withName:(NSString *)name withDelegate:(id<TLCRopeDelegate>)delegate;
- (void)addRopePhysics;
- (NSUInteger)getRopeLength;
- (NSMutableArray *)getRopeNodes; |
Delegation requires one object to define a protocol containing methods to which it expects its delegate to respond. The delegate class must then declare that it follows this protocol and implement the required methods.
Note: While you don’t need a delegate for the rope object to correctly function, this tutorial includes a delegate to show how to implement one. The final project includes a commented line showing an alternate method that foregoes delegates.
Once you’ve finished your header file, open TLCRope.m and add the following properties to the @interface
section:
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSMutableArray *ropeNodes;
@property (nonatomic, strong) SKNode *attachmentNode;
@property (nonatomic, assign) CGPoint attachmentPoint;
@property (nonatomic, assign) int length; |
Your next step is to add the code for the custom init
. Locate #pragma mark Init Method
and add the following block of code:
- (instancetype)initWithLength:(int)length usingAttachmentPoint:(CGPoint)point toNode:(SKNode*)node withName:(NSString *)name withDelegate:(id<TLCRopeDelegate>)delegate;
{
self = [super init];
if (self)
{
self.delegate = delegate;
self.name = name;
self.attachmentNode = node;
self.attachmentPoint = point;
self.ropeNodes = [NSMutableArray arrayWithCapacity:length];
self.length = length;
}
return self;
} |
This is simple enough: You take the values that you passed into init
and use them to set the private properties in your class.
The next two methods you need to add are getRopeLength
and getRopeNodes
.
Locate #pragma mark Helper Methods
and add the following:
- (NSUInteger)getRopeLength
{
return self.ropeNodes.count;
}
- (NSMutableArray *)getRopeNodes
{
return self.ropeNodes;
} |
The two methods above serve as a means of reading the values of the private properties.
You may have noticed that the code above refers to rope nodes, plural. That’s because in this game, each of your ropes will be made up of many nodes to give it a fluid look and feel. Let’s see how that will work in practice.
Creating the Parts of the Rope
Although the next few methods you write will be incomplete, it’s for a good reason: to fully understand what’s happening, it’s important to take things step by step.
The first step is to add the rope parts, minus the physics.
Still working in TLCRope.m, locate #pragma mark Setup Physics
and add the following method:
- (void)addRopePhysics
{
// keep track of the current rope part position
CGPoint currentPosition = self.attachmentPoint;
// add each of the rope parts
for (int i = 0; i < self.length; i++) {
SKSpriteNode *ropePart = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForRopeTexture];
ropePart.name = self.name;
ropePart.position = currentPosition;
ropePart.anchorPoint = CGPointMake(0.5, 0.5);
[self addChild:ropePart];
[self.ropeNodes addObject:ropePart];
/* TODO - Add Physics Here */
// set the next rope part position
currentPosition = CGPointMake(currentPosition.x, currentPosition.y - ropePart.size.height);
}
} |
In the code above, which you’ll call after the object has been initialized, you create each rope part and add it to the ropeNodes
array. You also give each part a name so you can reference it later. Finally, you add it as a child of the actual TLCRope object using addChild:
.
Soon, you’ll replace the TODO comment above with some code to give these rope parts their own physics bodies.
Now that you have everything in place, you’re almost ready to build and run to see your ropes. The final step is to add the ropes and the attached prize to the main game scene, which is exactly what you’re about to do.
Adding the Ropes and Prize to the Scene
Since the project uses a delegate pattern for TLCRope
, you need to declare this in whatever class will act as its delegate. In this case, it’s TLCMyScene
.
Open TLCMyScene.m and locate the @interface
line. Change it to read as follows:
@interface TLCMyScene() <SKPhysicsContactDelegate, TLCRopeDelegate> |
You’ll work with three methods to add the ropes to the scene, and they are all interconnected: setupRopes
, addRopeAtPosition:withLength:withName
and setupPrizeUsingPrimaryRope
.
Starting with the first, locate setupRopes
and add the following block of code:
// get ropes data
for (int i = 0; i < [self.ropes count]; i++) {
TLCGameData *currentRecord = [self.ropes objectAtIndex:i];
// 1
TLCRope *rope = [self addRopeAtPosition:currentRecord.ropeLocation withLength:currentRecord.ropeLength withName:[NSString stringWithFormat:@"%i", i]];
// 2
[self.worldNode addChild:rope];
[rope addRopePhysics];
// 3
if (currentRecord.isPrimaryRope) {
[self setupPrizeUsingPrimaryRope:rope];
}
}
self.prize.position = CGPointMake(self.size.width * .50, self.size.height * .80); |
Here’s the breakdown:
- First you created a new rope passing in the location and the length. You will be writing this method in just a moment.
- The next section adds the rope to the world and then sets up the physics.
- This final section of code sets the physics for the prize so long as the rope is the primary one.
Locate addRopeAtPosition:withLength:withName
and replace its current contents with this block of code:
SKSpriteNode *ropeHolder = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForRopeHolder];
ropeHolder.position = location;
ropeHolder.zPosition = LayerRope;
[self.worldNode addChild:ropeHolder];
CGPoint ropeAttachPos = CGPointMake(ropeHolder.position.x, ropeHolder.position.y -8);
TLCRope *rope = [[TLCRope alloc] initWithLength:length usingAttachmentPoint:ropeAttachPos toNode:ropeHolder withName:name withDelegate:self];
rope.zPosition = LayerRope;
rope.name = name;
return rope; |
Essentially, you’re using this method to create the individual ropes to display in your scene.
You’ve seen everything that’s happening here before. First you initialize an SKSpriteNode
using one of the constants for the image name, and then you set its position
and zPosition
with constants. This SKSpriteNode
will act as the “holder” for your rope.
The code then continues to initialize your rope object and set its zPosition
and name
.
Finally, the last piece of the puzzle gets you the prize! Clever, isn’t it? =]
Locate setupPrizeUsingPrimaryRope
and add the following:
self.prize = [SKSpriteNode spriteNodeWithImageNamed:kImageNameForPrize];
self.prize.name = kNodeNameForPrize;
self.prize.zPosition = LayerPrize;
self.prize.anchorPoint = CGPointMake(0.5, 1);
SKNode *positionOfLastNode = [[rope getRopeNodes] lastObject];
self.prize.position = CGPointMake(positionOfLastNode.position.x, positionOfLastNode.position.y + self.prize.size.height * .30);
[self.worldNode addChild:self.prize]; |
You may have noticed in setupRopes
and in the game data’s rope object a property for isPrimaryRope
. This property lets you loop through the data and select a rope to use as the primary rope for connecting to the prize.
When you set isPrimaryRope
to YES
, the code above executes and finds the end of the passed-in rope object by getting the last object in the rope’s ropeNodes
array. It does this using the helper method getRopeNodes
from the TLCRope
class.
Note: If you’re wondering why the prize’s position moves to the last node within the TLCRope
object, this is due to a bug in Sprite Kit. When you set a physics body, if the position of the node is not set beforehand, the body will behave unpredictably. The previous code uses the last rope part, of a single rope (of your choosing) to use as it’s initial position
And now for the moment you’ve been waiting for … Build and run your project!
Wait… Why isn’t the pineapple attached to the ropes? Why does everything look so stiff?
Don’t worry! The solution to these problems is… more physics!
Adding Physics Bodies to the Ropes
Remember that TODO comment you added earlier? It’s time to replace that with some physics to get things moving!
Open TLCRope.m and locate addRopePhysics
. Replace the TODO comment with the following code:
CGFloat offsetX = ropePart.frame.size.width * ropePart.anchorPoint.x;
CGFloat offsetY = ropePart.frame.size.height * ropePart.anchorPoint.y;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0 - offsetX, 7 - offsetY);
CGPathAddLineToPoint(path, NULL, 7 - offsetX, 7 - offsetY);
CGPathAddLineToPoint(path, NULL, 7 - offsetX, 0 - offsetY);
CGPathAddLineToPoint(path, NULL, 0 - offsetX, 0 - offsetY);
CGPathCloseSubpath(path);
ropePart.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];
ropePart.physicsBody.allowsRotation = YES;
ropePart.physicsBody.affectedByGravity = YES;
ropePart.physicsBody.categoryBitMask = EntityCategoryRope;
ropePart.physicsBody.collisionBitMask = EntityCategoryRopeAttachment;
ropePart.physicsBody.contactTestBitMask = EntityCategoryPrize;
[ropePart skt_attachDebugFrameFromPath:path color:[SKColor redColor]];
CGPathRelease(path); |
The code above creates a physics body for each of your rope parts, allowing you to set a series of physical characteristics for each node, like shape, size, mass, gravity and friction effects.
Physics bodies are created using the class method SKPhysicsBody bodyWithPolygonFromPath:
. This method takes one parameter: a path. A handy online tool for generating a path is SKPhysicsBody Path Generator.
In addition to setting the physics bodies for each node, the code above also sets some key properties to handle collisions: categoryBitMask
, collisionBitMask
and contactTestBitMask
. Each is assigned one of the constants you defined earlier. The tutorial will cover these properties in depth later.
If you were to run your app right now, each rope component would fall to the bottom of your screen. That’s because you’ve added a physics body to each but have yet to connect them together.
To fuse your rope, you’re going to use SKPhysicsJoint
s. Add the following method below addRopePhysics
:
- (void)addRopeJoints
{
// setup joint for the initial attachment point
SKNode *nodeA = self.attachmentNode;
SKSpriteNode *nodeB = [self.ropeNodes objectAtIndex:0];
SKPhysicsJointPin *joint = [SKPhysicsJointPin jointWithBodyA: nodeA.physicsBody
bodyB: nodeB.physicsBody
anchor: self.attachmentPoint];
// force the attachment point to be stiff
joint.shouldEnableLimits = YES;
joint.upperAngleLimit = 0;
joint.lowerAngleLimit = 0;
[self.delegate addJoint:joint];
// setup joints for the rest of the rope parts
for (int i = 1; i < self.length; i++) {
SKSpriteNode *nodeA = [self.ropeNodes objectAtIndex:i-1];
SKSpriteNode *nodeB = [self.ropeNodes objectAtIndex:i];
SKPhysicsJointPin *joint = [SKPhysicsJointPin jointWithBodyA: nodeA.physicsBody
bodyB: nodeB.physicsBody
anchor: CGPointMake(CGRectGetMidX(nodeA.frame),
CGRectGetMinY(nodeA.frame))];
// allow joint to rotate freely
joint.shouldEnableLimits = NO;
joint.upperAngleLimit = 0;
joint.lowerAngleLimit = 0;
[self.delegate addJoint:joint];
}
} |
This method connects all of the rope parts by using the SKPhysicsJoint
class. This class allows two connected bodies to rotate independently around the anchor points, resulting in a “rope-like” feel.
You connect (anchor) the first rope part to the attachmentNode
at the attachmentPoint
and link each subsequent node to the one before.
Note: In the code above, there is a call to the delegate to add the joints to the scene. As I mentioned before, this call isn’t necessary. You could simply use [self.scene.physicsWorld addJoint:joint];
to accomplish the same thing.
Now add a call to this new method at the very bottom of addRopePhysics
:
Build and run. Ack!
While you have some nice fluid ropes, they don’t contribute much if they just fall off the screen. :] That’s because you haven’t set up the physics bodies on the nodes in TLCScene.m. It’s time to add physics bodies to the prize and the rope holders!
Anchoring the Ends of the Ropes
Open TLCMyScene.m and locate addRopeAtPosition:withLength:withName:
. Right below the line [self.worldNode addChild:ropeHolder];
, add the following block of code:
CGFloat offsetX = ropeHolder.frame.size.width * ropeHolder.anchorPoint.x;
CGFloat offsetY = ropeHolder.frame.size.height * ropeHolder.anchorPoint.y;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0 - offsetX, 6 - offsetY);
CGPathAddLineToPoint(path, NULL, 6 - offsetX, 6 - offsetY);
CGPathAddLineToPoint(path, NULL, 6 - offsetX, 0 - offsetY);
CGPathAddLineToPoint(path, NULL, 0 - offsetX, 0 - offsetY);
CGPathCloseSubpath(path);
ropeHolder.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];
ropeHolder.physicsBody.affectedByGravity = NO;
ropeHolder.physicsBody.dynamic = NO;
ropeHolder.physicsBody.categoryBitMask = EntityCategoryRopeAttachment;
ropeHolder.physicsBody.collisionBitMask = 0;
ropeHolder.physicsBody.contactTestBitMask = EntityCategoryPrize;
[ropeHolder skt_attachDebugFrameFromPath:path color:[SKColor redColor]];
CGPathRelease(path); |
Here you add an SKPhysicsBody
for each of the rope holders and set their collision properties. You want the holders to act as solid anchors, which you achieve by disabling their affectedByGravity
and dynamic
properties.
Next, locate the delegate method, addJoint:
, and add this line:
[self.worldNode.scene.physicsWorld addJoint:joint]; |
The above method adds the joints you just created in TLCRope.m to the scene. This is the line that holds the rope parts together!
The next step is to add a physics body to the prize and set its collision detection properties.
Locate setupPrizeUsingPrimaryRope:
. Before the [self.worldNode addChild:self.prize];
line, add the following block of code:
CGFloat offsetX = self.prize.frame.size.width * self.prize.anchorPoint.x;
CGFloat offsetY = self.prize.frame.size.height * self.prize.anchorPoint.y;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 18 - offsetX, 75 - offsetY);
CGPathAddLineToPoint(path, NULL, 5 - offsetX, 65 - offsetY);
CGPathAddLineToPoint(path, NULL, 3 - offsetX, 55 - offsetY);
CGPathAddLineToPoint(path, NULL, 4 - offsetX, 34 - offsetY);
CGPathAddLineToPoint(path, NULL, 8 - offsetX, 7 - offsetY);
CGPathAddLineToPoint(path, NULL, 21 - offsetX, 2 - offsetY);
CGPathAddLineToPoint(path, NULL, 33 - offsetX, 4 - offsetY);
CGPathAddLineToPoint(path, NULL, 38 - offsetX, 20 - offsetY);
CGPathAddLineToPoint(path, NULL, 34 - offsetX, 53 - offsetY);
CGPathAddLineToPoint(path, NULL, 36 - offsetX, 62 - offsetY);
CGPathCloseSubpath(path);
self.prize.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];
self.prize.physicsBody.allowsRotation = YES;
self.prize.physicsBody.affectedByGravity = YES;
self.prize.physicsBody.density = 1;
self.prize.physicsBody.dynamic = NO;
self.prize.physicsBody.categoryBitMask = EntityCategoryPrize;
self.prize.physicsBody.collisionBitMask = 0;
self.prize.physicsBody.contactTestBitMask = EntityCategoryRope;
[self.prize skt_attachDebugFrameFromPath:path color:[SKColor redColor]];
CGPathRelease(path); |
Just like before, you add a physics body and set its collision detection properties.
To connect the prize to the end of the ropes, you need to do two things.
First, locate setupRopes
. At the end of the for
loop, add the following so that it’s the last line in the loop:
// connect the other end of the rope to the prize
[self attachNode:self.prize toRope:rope]; |
Then, locate attachNode:toRope
and add the following block of code:
SKNode *previous = [[rope getRopeNodes] lastObject];
node.position = CGPointMake(previous.position.x, previous.position.y + node.size.height * .40);
SKSpriteNode *nodeAA = [[rope getRopeNodes] lastObject];
SKPhysicsJointPin *jointB = [SKPhysicsJointPin jointWithBodyA: previous.physicsBody
bodyB: node.physicsBody
anchor: CGPointMake(CGRectGetMidX(nodeAA.frame), CGRectGetMinY(nodeAA.frame))];
[self.worldNode.scene.physicsWorld addJoint:jointB]; |
The code above gets the last node from the TLCRope
object and creates a new SKPhysicsJointPin
to attach the prize.
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, right? Hmm… Maybe it’s a little stiff? Then again, maybe that’s the effect you want in your game. If not, you can give it a more fluid appearance.
Go to the top of TLCMyScene.m and add the following line below your other #define
statements:
#define prizeIsDynamicsOnStart YES |
Then, locate setupRopes
and change the last two lines to this:
// reset prize position and set if dynamic; depends on your game play
self.prize.position = CGPointMake(self.size.width * .50, self.size.height * .80);
self.prize.physicsBody.dynamic = prizeIsDynamicsOnStart; |
Build and run the project again.
Notice how much more fluid the ropes feel? Of course, it you prefer it the other way, change the value for prizeIsDynamicsOnStart
to NO
. It’s your game, after all! :]
A Few More Physics Bodies
Since you’ve already got physics bodies on your mind, it makes sense to set them up for the player and water nodes. Once you have those configured, you’ll be primed to start work on collision detection.
In TLCMyScene.m, locate setupCrocodile
and add the following block of code just before the [self.worldNode addChild:self.crocodile];
line:
CGFloat offsetX = self.crocodile.frame.size.width * self.crocodile.anchorPoint.x;
CGFloat offsetY = self.crocodile.frame.size.height * self.crocodile.anchorPoint.y;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 47 - offsetX, 77 - offsetY);
CGPathAddLineToPoint(path, NULL, 5 - offsetX, 51 - offsetY);
CGPathAddLineToPoint(path, NULL, 7 - offsetX, 2 - offsetY);
CGPathAddLineToPoint(path, NULL, 78 - offsetX, 2 - offsetY);
CGPathAddLineToPoint(path, NULL, 102 - offsetX, 21 - offsetY);
CGPathCloseSubpath(path);
self.crocodile.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];
self.crocodile.physicsBody.categoryBitMask = EntityCategoryCrocodile;
self.crocodile.physicsBody.collisionBitMask = 0;
self.crocodile.physicsBody.contactTestBitMask = EntityCategoryPrize;
self.crocodile.physicsBody.dynamic = NO;
[self.crocodile skt_attachDebugFrameFromPath:path color:[SKColor redColor]];
CGPathRelease(path); |
Just as with the rope nodes, you establish a path for your player node’s physics body and set its collision detection properties, each of which I’ll explain momentarily.
Last but not least, the water also needs a physics body so you can detect when the prize lands there rather than in the mouth of the hungry crocodile.
Locate setupBackground
. Before the [self.worldNode addChild:water];
line, add the following block of code:
// make the size a little shorter so the prize will look like it’s landed in the water
CGSize bodySize = CGSizeMake(water.frame.size.width, water.frame.size.height -100);
water.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:bodySize];
water.physicsBody.dynamic = NO;
water.physicsBody.categoryBitMask = EntityCategoryGround;
water.physicsBody.collisionBitMask = EntityCategoryPrize;
water.physicsBody.contactTestBitMask = EntityCategoryPrize; |
Once again, you add a physics body, but this time you use bodyWithRectangleOfSize:
. You also set the body’s collision detection properties.
Notice that you are assigning EntityCategoryGround
as the categoryBitMask for the water object. In reality EntityCategoryGround represents the point of failure for your fruit as opposed to the physical ground. If you wanted to include additional traps such as spinning buzz saws, you would assign it EntityCategoryGround bit mask.
Note: You may have noticed a call to skt_attachDebugFrameFromPath:
for most of the physics bodies. This is a method from SKNode+SKTDebugDraw
, which is part of a group of Sprite Kit utilities developed by Razeware. This particular method helps with debugging physics bodies. To turn it on, open SKNode+SKTDebugDraw.m and change the line BOOL SKTDebugDrawEnabled = NO;
to BOOL SKTDebugDrawEnabled = YES;
. This will draw a shape that represents your physics body. Don’t forget to turn it off when you’re done!
Making the Cut
It can’t be named Cut the Verlet if your verlets 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 players to cut those ropes. The first step is to define some basic variables.
Still working in TLCMyScene.m, add the following properties to the @interface
section:
@property (nonatomic, assign) CGPoint touchStartPoint;
@property (nonatomic, assign) CGPoint touchEndPoint;
@property (nonatomic, assign) BOOL touchMoving; |
You’ll need these for tracking the user’s touches.
Then, add your final definition at the top of TLCMyScene.m:
#define canCutMultipleRopesAtOnce NO |
This will be useful if you want to make changes to the way the game functions.
iOS incudes a few methods that deal with handling touch events. You’ll be working with three: touchesBegan:withEvent:
, touchesEnded:withEvent:
and touchesMoved:withEvent:
.
Locate touchesBegan:withEvent:
and add the following code:
self.touchMoving = NO;
for (UITouch *touch in touches) {
self.touchStartPoint = [touch locationInNode:self];
} |
The code above sets the variable based on the location of the user’s touch.
Next, locate touchesEnded:withEvent:
and add this:
for (UITouch *touch in touches) {
if (touches.count == 1 && self.touchMoving) {
self.touchEndPoint = [touch locationInNode:self];
if (canCutMultipleRopesAtOnce) {
/* allow multiple ropes to be cut */
[self.worldNode.scene.physicsWorld enumerateBodiesAlongRayStart:self.touchStartPoint end:self.touchEndPoint usingBlock:^(SKPhysicsBody *body, CGPoint point, CGVector normal, BOOL *stop)
{
[self checkRopeCutWithBody:body];
}];
}
else {
/* allow only one rope to be cut */
SKPhysicsBody *body = [self.worldNode.scene.physicsWorld bodyAlongRayStart:self.touchStartPoint end:self.touchEndPoint];
[self checkRopeCutWithBody:body];
}
}
}
self.touchMoving = NO; |
This code does a few things. First, it makes sure the user is touching the screen with only one finger, and then it determines if the user is moving that finger. Finally, it retrieves and sets the property touchEndPoint
. With that information, you can take the appropriate action based on whether you’re allowing only one rope or multiple ropes to be cut with a single swipe.
To cut multiple ropes, you use SKPhysicsWorld
’s method enumerateBodiesAlongRayStart:end:usingBlock:
to capture multiple touch points. To cut a single rope, you use bodyAlongRayStart:end:
to get only the first touch point. Then you pass that information to the custom method, checkRopeCutWithBody:
.
Finally, locate touchesMoved:withEvent:
and add this code:
if (touches.count == 1) {
for (UITouch *touch in touches) {
NSString *particlePath = [[NSBundle mainBundle] pathForResource:@"TLCParticle" ofType:@"sks"];
SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:particlePath];
emitter.position = [touch locationInNode:self];
emitter.zPosition = LayerRope;
emitter.name = @"emitter";
[self.worldNode addChild:emitter];
self.touchMoving = YES;
}
} |
Technically, you don’t need most of the code above, but it does provide for really cool effects when your users swipe the screen. You do, however, need to set the touchMoving
property to YES
, as the code above does. As you saw earlier, you’re evaluating this variable to determine if the user is moving her finger.
So, what does the rest of the code do?
It uses an SKEmitterNode to automatically generate awesome green particles that appear onscreen whenever the user swipes.
The code above loads a particle file and adds it to the worldNode
. 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. =]
With the touch events complete, it’s time to finish the method that you call in touchesEnded:withEvent:
.
Locate checkRopeCutWithBody:
and add the following block of code:
SKNode *node = body.node;
if (body) {
self.prize.physicsBody.affectedByGravity = YES;
self.prize.physicsBody.dynamic = YES;
[self.worldNode enumerateChildNodesWithName:node.name usingBlock:^(SKNode *node, BOOL *stop)
{
for (SKPhysicsJoint *joint in body.joints) {
[self.worldNode.scene.physicsWorld removeJoint:joint];
}
SKSpriteNode *ropePart = (SKSpriteNode *)node;
SKAction *fadeAway = [SKAction fadeOutWithDuration:0.25];
SKAction *removeNode = [SKAction removeFromParent];
SKAction *sequence = [SKAction sequence:@[fadeAway, removeNode]];
[ropePart runAction: sequence];
}];
} |
The code above enumerates through all the child nodes in worldNode
and if it comes across any joints, it removes them. Rather than remove them abruptly, it uses an SKAction
sequence to fade out the node first.
Build and run the project. You should be able to swipe and cut all three ropes—as well as the prize (for now). Toggle the canCutMultipleRopesAtOnce
setting to see how the behavior differs. By the way, aren’t those particles awesome?
Collision Detection
You’re almost done! You’ve got swiping in place, physics out of the way, and you’ve specified all of the collision properties—but what exactly do they mean? How do they work? And, more importantly, how do they work with one another?
Here are a few key things to note:
- You need to specify that the
TLCMyScene
acts as a contact delegate: SKPhysicsContactDelegate
.
- You need to set the delegate on the world node:
self.worldNode.scene.physicsWorld.contactDelegate = self
.
- You need to specify a
categoryBitMask
, a collisionBitMask
and a contactTestBitMask
.
- You need to implement the delegate methods.
You did the first two when you set up the physics for worldNode
. You took care of the third when you set up the rest of the node’s physics bodies. That was excellent foresight on your part! =]
That leaves number four on the list: implement the methods. Before doing so, however, you need to create a few properties.
In the @implementation
section of TLCMyScene.m, add the following:
@property (nonatomic, assign) BOOL scoredPoint;
@property (nonatomic, assign) BOOL hitGround; |
Now you’re ready to modify the delegate method.
Locate didBeginContact:
and add the following block of code:
SKPhysicsBody *other = (contact.bodyA.categoryBitMask == EntityCategoryPrize ? contact.bodyB : contact.bodyA);
if (other.categoryBitMask == EntityCategoryCrocodile) {
if (!self.hitGround) {
NSLog(@"scoredPoint");
self.scoredPoint = YES;
}
return;
}
else if (other.categoryBitMask == EntityCategoryGround) {
if (!self.scoredPoint) {
NSLog(@"hitGround");
self.hitGround = YES;
return;
}
} |
The code above executes anytime the scene’s physicsWorld
detects a collision. It checks the body’s categoryBitMask
and, based on its value, either scores a point or registers a ground hit.
The three settings, categoryBitMask
, collisionBitMask
and contactTestBitMask
, all work in tandem with one another.
- The
categoryBitMask
sets the category to which the sprite belongs.
- The
collisionBitMask
sets the category with which a sprite may collide.
- The
contactTestBitMask
defines which categories trigger notifications to the delegate.
Check the SKPhysicsBody Class Reference for more detailed information.
This game uses five categories, as defined within the TLCSharedConstants
class. Open TLCSharedConstants.m and take another look. You will see some collision categories you set set up earlier in the tutorial.
EntityCategoryCrocodile = 1 << 0,
EntityCategoryRopeAttachment = 1 << 1,
EntityCategoryRope = 1 << 2,
EntityCategoryPrize = 1 << 3,
EntityCategoryGround = 1 << 4 |
You want to detect when the prize collides with the crocodile node and when the prize collides with the water. You’re not going to award points or end the game based on any contact with the rope nodes, but setting categories for the rope and rope attachment points will help make the rope look more realistic at its attachment point.
Build and run the project. When you cut the ropes, you should see log statements corresponding with where the prize lands.
Bonus Animation!
While you do have the faint outline of game, users are not going to be staring at their console to see the win or fail condition. Also, if you cut the correct rope, the fruit falls through the crocodile as opposed to the crocodile eating it. Users will expect to see the crocodile munch down that pineapple.
It’s time to fulfill that expectation with animation. To do this, you’ll modify nomnomnomActionWithDelay:
.
In TLCMyScene.m, find nomnomnomActionWithDelay:
and add the following block of code:
[self.crocodile removeAllActions];
SKAction *openMouth = [SKAction setTexture:[SKTexture textureWithImageNamed:kImageNameForCrocodileMouthOpen]];
SKAction *wait = [SKAction waitForDuration:duration];
SKAction *closeMouth = [SKAction setTexture:[SKTexture textureWithImageNamed:kImageNameForCrocodileMouthClosed]];
SKAction *nomnomnomAnimation = [SKAction sequence:@[openMouth, wait, closeMouth]];
[self.crocodile runAction: [SKAction repeatAction:nomnomnomAnimation count:1]];
if (!self.scoredPoint) {
[self 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
. At that point, if the player hasn’t scored a point, it runs animateCrocodile
, which resets the random opening and closing of the crocodile’s jaw.
Next, locate checkRopeCutWithBody:
and, after the self.prize.physicsBody.dynamic = YES;
line, add the following two lines of code:
[self nomnomnomActionWithDelay:1]; |
This code executes every time the user cuts a rope. It runs the method you just created. The animation gives the illusion that the crocodile is opening its mouth in hope something yummy will fall into it.
You also need to run this method in didBeginContact:
so that when the prize touches the crocodile, he opens his mouth to eat it.
In didBeginContact:
, after the self.scoredPoint = YES;
line, add the following line:
[self nomnomnomActionWithDelay:.15]; |
Just as before, you run nomnomnomActionWithDelay
, except this time you run it when the prize
collides with the crocodile
. This makes the crocodile appear to eat the prize.
Build and run.
The food falls right through the crocodile. You can fix this by making a few simple changes.
Locate checkForScore
and add the following block of code:
if (self.scoredPoint) {
self.scoredPoint = NO;
SKAction *shrink = [SKAction scaleTo:0 duration:0.08];
SKAction *removeNode = [SKAction removeFromParent];
SKAction *sequence = [SKAction sequence:@[shrink, removeNode]];
[self.prize runAction: sequence];
} |
The code above checks the value of the scoredPoint
property. If this is set to YES
, the code sets it back to NO
, runs the action to play the nomnomnom sound and then removes the prize
from the scene using an SKAction sequence
.
You want this code to execute continually to keep track of your variable. To make that happen, you need to modify update:
.
Locate update:
and add the following line:
update:
invokes before each frame of the animation renders. Here you call the method that checks if the player scored a point.
The next thing you need to do is check for a ground hit. Locate checkForGroundHit
and add the following block of code:
if (self.hitGround) {
self.hitGround = NO;
SKAction *shrink = [SKAction scaleTo:0 duration:0.08];
SKAction *removeNode = [SKAction removeFromParent];
SKAction *sequence = [SKAction sequence:@[shrink, removeNode]];
[self.prize runAction: sequence];
} |
Almost like checkForScore
, this code checks the value of hitGround
. If the value is YES
, the code resets it to NO
, runs the action to play the splash sound and then removes the prize
from the scene using an SKAction sequence
.
Once again, you need to call this method from update:
. Locate update:
and add the following line:
[self checkForGroundHit]; |
With everything in place, build and run the project.
You should see and hear all of the fabulous things you added. But, you’ll also notice that once you score a point or miss the crocodile’s mouth, the game just hangs there. You can fix that!
Adding a Scene Transition
In TLCMyScene.m, find switchToNewGameWithTransition:
and add the following block of code:
SKView *skView = (SKView *)self.view;
TLCMyScene *scene = [[TLCMyScene alloc] initWithSize:self.size];
[skView presentScene:scene transition:transition]; |
The code above uses SKView’s presentScene:transition:
to present the next scene.
In this case, you present TLCMyScene
. You also pass in a transition using the SKTransition
class.
You need to call this method in two places: checkForScore
and checkForGroundHit
.
In checkForGroundHit
, add the following line of code at the end of the if
statement (within the braces):
SKTransition *sceneTransition = [SKTransition fadeWithDuration:1.0];
[self performSelector:@selector(switchToNewGameWithTransition:) withObject:sceneTransition afterDelay:1.0]; |
Next, in checkForScore
, add the following line of code, also at the end of the if
statement (but in between the braces):
/* Various kinds of scene transitions */
NSArray * 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:SKTransitionDirectionLeft duration:1.0],
[SKTransition pushWithDirection:SKTransitionDirectionRight duration:1.0],
[SKTransition revealWithDirection:SKTransitionDirectionDown duration:1.0],
[SKTransition crossFadeWithDuration:1.0],
[SKTransition doorwayWithDuration:1.0],
[SKTransition fadeWithColor:[UIColor darkGrayColor] duration:1.0],
[SKTransition fadeWithDuration:1.0]
];
int randomIndex = arc4random_uniform((int) transitions.count);
[self performSelector:@selector(switchToNewGameWithTransition:) withObject:transitions[randomIndex] afterDelay:1.0]; |
The code above includes all of the available transitions, stored in an NSArray. The code then selects a random transition by using the arc4random_uniform function. The random transition is then provided to switchToNewGameWithTransition:
so you should see a different transition after each game.
Now build and run the project.
You should see the scene transition to a new one whenever the player scores a point or loses the prize.
You need to make one final modification to handle when the prize leaves the screen. This can happen if the user cuts the ropes in such a way as to “throw” the prize off the screen.
To handle this case, add the following code to checkForPrize
:
[self.worldNode enumerateChildNodesWithName:kNodeNameForPrize usingBlock:^(SKNode *node, BOOL *stop)
{
if (node.position.y <= 0) {
[node removeFromParent];
self.hitGround = YES;
}
}]; |
The code above enumerates through the child nodes in the worldNode
to find one that matches the specified constant, which in this case is the name of the prize node. If the code finds the right node, it assumes the node has not made contact with the player
and therefore sets the variable hitGround
to YES
.
Again, you need to add a call to checkForPrize
in update:
. Adding the following line to update:
:
Finally, remember that your user can still swipe the prize to score an easy victory. You may have noticed this in your testing. I call this the Cheater Bug. To fix this, locate checkRopeCutWithBody:
and add the following just above the for
loop line (for (SKPhysicsJoint *joint in body.joints) { … }
):
if ([node.name isEqualToString:kNodeNameForPrize]) {
return;
} |
The code above checks if the user has swiped the prize node by looking for a name match. If there is a match, the method returns and does nothing.
Life Without Music and Sound. So Boring!
While the game is technically complete, it lacks a certain pop. A silent game may quickly bore your users. It’s time to add a little “juice” to make things pop.
I’ve selected a nice jungle song from incompetech.com and some sound effects from freesound.org.
Because this game will play music in the background, it makes sense to use a single AVPlayer
in the App Delegate. You don’t need to add it because the starter project already contains a property in TLCAppDelegate.h for an AVAudioPlayer
(backgroundMusicPlayer
). You simply need to add the playBackgroundMusic:
method and then call that method.
Open TLCMyScene.m and locate playBackgroundMusic
. Add the following code:
NSError *error;
NSURL *backgroundMusicURL = [[NSBundle mainBundle] URLForResource:filename withExtension:nil];
TLCAppDelegate *appDelegate = (TLCAppDelegate *)[[UIApplication sharedApplication] delegate];
if (!appDelegate.backgroundMusicPlayer) // not yet initialized, go ahead and set it up
{
appDelegate.backgroundMusicPlayer = nil;
appDelegate.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
appDelegate.backgroundMusicPlayer.numberOfLoops = -1;
appDelegate.backgroundMusicPlayer.volume = 1.0;
[appDelegate.backgroundMusicPlayer prepareToPlay];
}
if (!appDelegate.backgroundMusicPlayer.isPlaying) // is it currently playing? if not, play music
{
[appDelegate.backgroundMusicPlayer play];
} |
The code above checks if the instance of backgroundMusicPlayer
has been initialized. If not, it initializes it with some basic settings, like the number of loops, the volume and the URL to play, which is passed into the method as a parameter.
Note: AVAudioPlayer
isn’t specific to Sprite Kit, so this tutorial won’t cover it in detail. To learn more about
AVAudioPlayer
, check out our
Audio Tutorial for iOS.
Once the method has initialized the music player, it checks if the music player is already playing, and turns it on if it’s not.
You need this check so that when the scene reloads after the player scores a point or the prize hits the ground, the music won’t “skip” or “restart.” Is this necessary? No. Does it sound better? Absolutely.
Locate setupSounds
and add the following line:
[self playBackgroundMusic:kSoundFileNameForBackgroundMusic]; |
That line makes a call to the method you just wrote. By the way, did you catch that constant you’re using? If you did, you score two extra points. You defined the constant kSoundFileNameForBackgroundMusic
in TLCSharedConstants.m earlier.
You may as well add sound effects while you’re at it!
For the last time, locate the @interface
section of TLCMyScene.m and add the following properties:
@property (nonatomic, strong) SKAction *soundCutAction;
@property (nonatomic, strong) SKAction *soundSplashAction;
@property (nonatomic, strong) SKAction *soundNomNomNomAction; |
Next, locate setupSounds
. Just above the last line, add the code below:
self.soundCutAction = [SKAction playSoundFileNamed:kSoundFileNameForCutAction waitForCompletion:NO];
self.soundSplashAction = [SKAction playSoundFileNamed:kSoundFileNameForSplashAction waitForCompletion:NO];
self.soundNomNomNomAction = [SKAction playSoundFileNamed:kSoundFileNameForBiteAction waitForCompletion:NO]; |
This code initializes the variables using SKAction
’s playSoundFileNamed:waitForCompletion:
method.
In TLCMyScene.m, find checkForGroundHit
and add the following line of code just above SKAction *shrink = [SKAction scaleTo:0 duration:0.08];
line:
[self runAction:self.soundSplashAction]; |
Find checkForScore
and add the following line of code just above SKAction *shrink = [SKAction scaleTo:0 duration:0.08];
:
[self runAction:self.soundNomNomNomAction]; |
Find checkRopeCutWithBody:
and add the following line of code just above [self nomnomnomActionWithDelay:1];
line:
[self runAction:self.soundCutAction]; |
Finally, locate initWithSize:
and add the following line before [self setupBackground];
line:
Build and run the project.
The app should be popping now, yet the discerning player may notice a slight sound bug. In some instances, you may hear both the nom-nom sound but also, the splashing sound. This is due to the prize triggering multiple collisions before it is removed from the scene. To fix this, add a new property in the interface section:
@property (nonatomic, assign) BOOL roundOutcome; |
Next, add the following code to both checkForScore
and checkForGroundHit
at the top of each if
block.
Finally, replace the contents of update:
with the following::
if (!self.roundOutcome) {
[self checkForScore];
[self checkForGroundHit];
[self checkForPrize];
} |
By containing all of the checks in a block, you insure that the methods will not be called once an outcome has occurred. Build and run and swipe away. There’s no sound collisions and you will have one very stuffed crocodile :]
Where to Go From Here?
I hope you enjoyed working through this tutorial as much as I’ve enjoyed writing it. To compare notes, download the CutTheVerlet-Finished 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!
How to Create a Game Like Cut the Rope Using Sprite Kit is a post from: Ray Wenderlich
The post How to Create a Game Like Cut the Rope Using Sprite Kit appeared first on Ray Wenderlich.