In 2009, Firemint introduced the line drawing game to the world when they released the incredibly popular Flight Control.
In a line drawing game, you trace a line with your finger and then sprites follow the line that you drew.
In this tutorial, you’ll learn how to write your own line drawing game with Sprite Kit. Instead of being a mere clone of Flight Control, you will create a game called “Hogville” where your goal is to bring some cute and tired pigs to food and shelter.
This tutorial assumes you have some experience with Sprite Kit. While you don’t have to be an expert, you should know the basics, like how to create sprites and run actions on them. If you’ve got a big question mark in place of that knowledge, take some time to work through Ray’s Sprite Kit Tutorial for Beginners before proceeding.
Getting Started
To get started, download the starter project.
I created this starter project using the Sprite Kit Game template and set it to run in landscape mode. I also added all the artwork you’ll need – and a big thanks to Vicki Wenderlich for providing the art!
Build and run, and you should see a blank screen in landscape as a starting point:
Now you can get right to the business of adding your game elements and developing the line drawing gameplay.
Adding the Background… and a Pig!
After a long day of being a pig, all you want is some food and a bed—a pile of hay will do! It’s hard work rolling in the mud all day. In Hogville, it will be your player’s job to give the pigs what they want by drawing the lines to steer them home.
Before you start drawing lines, though, you need a pig to follow them. Your pig would be a bit unhappy floating in a black void, so you’ll also add a background to give the poor pig some familiar surroundings.
Open MyScene.m and find initWithSize:
. Inside the if
statement, add the following code:
SKSpriteNode *bg = [SKSpriteNode spriteNodeWithImageNamed:@"bg_2_grassy"]; bg.anchorPoint = CGPointZero; [self addChild:bg]; SKSpriteNode *pig = [SKSpriteNode spriteNodeWithImageNamed:@"pig_1"]; pig.position = CGPointMake(self.size.width / 2.0f, self.size.height / 2.0f); [self addChild:pig]; |
This adds the background image and a pig sprite. You place the lower-left corner of the background image in the lower-left corner of the scene by setting bg
‘s anchorPoint
to (0, 0) and using its default position.
Build and run your game and behold your plump, rosy pig in the middle of a sunny, green field.
Moving the Sprite
Next you need to create a class for the pig sprite. This will contain the path which the pig should follow, along with methods to make the pig follow this path over time.
To create a class for the pig sprite, go to File\New\File…, choose the iOS\Cocoa Touch\Objective-C class template and click Next. Name the class Pig, make it a subclass of SKSpriteNode, click Next and then Create.
Open Pig.h and add the following two method declarations to the interface:
- (void)addPointToMove:(CGPoint)point; - (void)move:(NSNumber *)dt; |
Now open Pig.m and add the following variable before the @implemenation
section:
static const int POINTS_PER_SEC = 80; |
This constant defines the speed of the pig as 80 points per second.
Next, declare two instance variables by adding the following code immediately after the @implementation
line:
{ NSMutableArray *_wayPoints; CGPoint _velocity; } |
_wayPoints
will do what its name suggests and store all the points along which the pig should move. _velocity
will store the pig’s current speed and direction.
Next, implement initWithImageNamed:
and initialize _waypoints
inside it:
- (instancetype)initWithImageNamed:(NSString *)name { if(self = [super initWithImageNamed:name]) { _wayPoints = [NSMutableArray array]; } return self; } |
Now that you’ve initialized _wayPoints
, you need a method to add waypoints to it. Implement addPointToMove:
by adding the following code to Pig.m:
- (void)addPointToMove:(CGPoint)point { [_wayPoints addObject:[NSValue valueWithCGPoint:point]]; } |
This method simply adds the given point to the _wayPoints
array. In order to store a CGPoint
in an NSArray
, you use NSValue
‘s valueWithCGPoint
method to store the CGPoint
in an object.
Now begin implementing move:
by adding the following code to Pig.m:
- (void)move:(NSNumber *)dt { CGPoint currentPosition = self.position; CGPoint newPosition; //1 if([_wayPoints count] > 0) { CGPoint targetPoint = [[_wayPoints firstObject] CGPointValue]; //2 TODO: Add movement logic here //3 if(CGRectContainsPoint(self.frame, targetPoint)) { [_wayPoints removeObjectAtIndex:0]; } } } |
You will call this method each frame to move the pig a little bit along its path. Here’s how this part of the method works:
- First you check to ensure there are waypoints left in the array. For the moment, the pig stops moving when it reaches the final point of the path. Later, you’ll make the pig a little smarter so it continues walking in its last direction even when no waypoints remain.
- This comment marks where you’ll put the code that updates the pig’s position. You’ll add that code next.
- Finally, you check if the pig has reached the waypoint by seeing if the pig’s
frame
contains thetargetPoint
. In this case, you remove the point from the array so that your next call tomove:
will use the next point. Note that it’s important to check if the frame contains the target point (rather than checking if the position equals the target point), effectively stopping the pig when he’s “close enough”. That makes some of the calculations later a bit easier.
You added that final if
statement in the above code because the pig isn’t guaranteed to reach the waypoint in just one call to move:
. That makes sense, because the pig needs to move at a constant speed, a little each frame.
Why? Let’s assume you have the first waypoint in the upper-left corner at (0, 50) and the second point at (300, 50). Something like this can happen if the player moves their finger very fast over the screen.
If you took the simple approach of setting the position to the first point in the array and then to the second point in the array, your pig would appear to teleport from one waypoint to the next. Have you ever seen a teleporting pig? I’m sure even Captain Kirk can’t make that claim.
With the logic to process the waypoints in place, it’s time to add the code that calculates and updates the pig’s new position along the path between the waypoints. In move:
, replace the //2 TODO: Add movement logic here
comment with the following code:
//1 CGPoint offset = CGPointMake(targetPoint.x - currentPosition.x, targetPoint.y - currentPosition.y); CGFloat length = sqrtf(offset.x * offset.x + offset.y * offset.y); CGPoint direction = CGPointMake(offset.x / length, offset.y / length); _velocity = CGPointMake(direction.x * POINTS_PER_SEC, direction.y * POINTS_PER_SEC); //2 newPosition = CGPointMake(currentPosition.x + _velocity.x * [dt doubleValue], currentPosition.y + _velocity.y * [dt doubleValue]); self.position = newPosition; |
Here’s what you’re doing with the code you just added:
-
You calculate a vector that points in the direction the pig should travel and has a length representing the distance the pig should move in
dt
seconds.To calculate the vector, first you find the difference between the pig’s current location and the next waypoint and store it as
offset
, aCGPoint
representing the differences in both thex
andy
directions.As you can see in the following image, the distance between the two points is the length of the hypotenuse of the right triangle formed between the pig’s current position and the waypoint.
You divide
offset
‘s components bylength
to create a normalized vector (a vector of length 1) that points in the direction of the waypoint and you store it indirection
.Finally, you multiply
direction
byPOINTS_PER_SEC
and store it in_velocity
, which now represents a vector pointing in the direction the pig should travel, with a length that is the distance the pig should travel in one second. - You calculate the pig’s new position by multiplying
_velocity
bydt
and adding the result to the pig’s current position. Because_velocity
stores the distance the pig should travel in one second anddt
holds the number of seconds that have passed since the last call tomove:
, multiplying the two results in the distance the pig should travel indt
seconds.
You’re done here for the moment. It’s time to use your new class and move the pig.
Responding to Touches
Open MyScene.m and add the following import so the scene can access your new class:
#import "Pig.h" |
Find this line in initWithSize:
:
SKSpriteNode *pig = [SKSpriteNode spriteNodeWithImageNamed:@"pig_1"]; |
Replace the above line with the following:
Pig *pig = [[Pig alloc] initWithImageNamed:@"pig_1"]; pig.name = @"pig"; |
You have simply replaced SKSpriteNode
with your new subclass, Pig
, and given it a name. You will use this name when you process new touches to identify pig nodes.
Add the following instance variables to MyScene
, just below the @implementation
line:
{ Pig *_movingPig; NSTimeInterval _lastUpdateTime; NSTimeInterval _dt; } |
_movingPig
will hold a reference to the pig the user wants to move. _lastUpdateTime
will store the time of the last call to update:
and _dt
will store the time elapsed between the two most recent calls to update:
.
A few steps remain before you get to see your pig move. Add the following code inside touchesBegan:withEvent:
:
CGPoint touchPoint = [[touches anyObject] locationInNode:self.scene]; SKNode *node = [self nodeAtPoint:touchPoint]; if([node.name isEqualToString:@"pig"]) { [(Pig *)node addPointToMove:touchPoint]; _movingPig = (Pig *)node; } |
What happens here? First, you find the location of the touch within the scene. After that, you use nodeAtPoint:
to identify the node at that location. The if
statement uses the node’s name to see if the user touched a pig or something else, such as the background.
name
property of SKNode
to check for the pig. This is like UIView
‘s tag
property: a simple way to identify a node without needing to store a reference. Later, you’ll see another use case for the name
property.
If the user touched a pig, you add touchPoint
as a waypoint and set _movingPig
to the touched node. You’ll need this reference in the next method to add more points to the path.
To draw a path, after the first touch the user needs to move their finger while continuously touching the screen. Add the following implementation of touchesMoved:withEvent:
to add more waypoints:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { CGPoint touchPoint = [[touches anyObject] locationInNode:self.scene]; if(_movingPig) { [_movingPig addPointToMove:touchPoint]; } } |
This is a simple method. You get the next position of the user’s finger and if you found a pig in touchesBegan:withEvent:
, as indicated by a non-nil _movingPig
value, you add the position to this pig as the next waypoint.
So far, you can store a path for the pig—now let’s make the pig follow this path. Add the following code to update:
inside MyScene.m:
_dt = currentTime - _lastUpdateTime; _lastUpdateTime = currentTime; [self enumerateChildNodesWithName:@"pig" usingBlock:^(SKNode *pig, BOOL *stop) { [(Pig *)pig move:@(_dt)]; }]; |
- First, you calculate the time since the last call to
update:
and store it in_dt
. Then, you assigncurrentTime
to_lastUpdateTime
so you have it for the next call. - Here is the other use case for the
name
property. You useSKScene
‘s methodenumerateChildNodesWithName:usingBlock:
to enumerate over all nodes with the namepig
. On these nodes, you callmove:
, passing_dt
as the argument. SinceSKNode
has no method calledmove:
, you cast it toPig
to make Xcode and the compiler happy.
Now build and run, and let the pig follow your finger as you draw a path.
The pig doesn’t face in the direction it’s moving, but otherwise this is a good result!
But wait a minute—isn’t this a line drawing game? So where is the line?
Drawing Lines
Believe it or not, there is only one important step left to complete a line drawing game prototype that you can expand. Drawing the lines!
At the moment, only the pig knows the path it wants to travel, but the scene also needs to know this path to draw it. The solution to this problem is a new method for your Pig
class.
Open Pig.h and add the following method declaration to the interface:
- (CGPathRef)createPathToMove; |
Now open Pig.m and implement this new method as follows:
- (CGPathRef)createPathToMove { //1 CGMutablePathRef ref = CGPathCreateMutable(); //2 for(int i = 0; i < [_wayPoints count]; ++i) { CGPoint p = [_wayPoints[i] CGPointValue]; //3 if(i == 0) { CGPathMoveToPoint(ref, NULL, p.x, p.y); } else { CGPathAddLineToPoint(ref, NULL, p.x, p.y); } } return ref; } |
- First, you create a mutable
CGPathRef
so you can add points to it. - This
for
loop iterates over all the stored waypoints to build the path. - Here you check if the path is just starting, indicated by an
i
value of zero. If so, you move to the point’s location; otherwise, you add a line to the point. If this is confusing, think about how you would draw a path with pen and paper.CGPathMoveToPoint()
is the moment you put the pen on the paper after moving it to the starting point, whileCGPathAddLineToPoint()
is the actual drawing with the pen on the paper. - At the end, you return the path.
CGPath
objects, so you need to call CGPathRelease()
when you’re done with your path. You’ll do that soon!
Open MyScene.m and add this method to draw the pig’s path:
- (void)drawLines { //1 [self enumerateChildNodesWithName:@"line" usingBlock:^(SKNode *node, BOOL *stop) { [node removeFromParent]; }]; //2 [self enumerateChildNodesWithName:@"pig" usingBlock:^(SKNode *node, BOOL *stop) { //3 SKShapeNode *lineNode = [SKShapeNode node]; lineNode.name = @"line"; lineNode.strokeColor = [SKColor grayColor]; lineNode.lineWidth = 1.0f; //4 CGPathRef path = [(Pig *)node createPathToMove]; lineNode.path = path; CGPathRelease(path); [self addChild:lineNode]; }]; } |
Here’s what’s happening:
- You’ll redraw the path every frame, so first you remove any old line nodes. To do so, you enumerate over all nodes with the name “line” and remove them from the scene.
- After that, you enumerate over all the pigs in your scene using the same method.
- For each pig, you create an SKShapeNode and name it “line”. Next you set the stroke color of the shape to gray and the width to 1. You can use any color you want, but I think gray will be visible on the most backgrounds.
- You use the method you just added to
Pig
to create a new path and assign it tolineNode
‘spath
property. Then you callCGPathRelease
to free the path’s memory. If you forgot to do that, you would create a memory leak that would eventually crash your app. Finally, you addlineNode
to your scene so that the scene will render it.
At last, to draw the path, add this line at the end of update:
in MyScene.m:
[self drawLines]; |
Build and run, ready your finger and watch as the game draws your path onscreen—and hopefully, your pig follows it!
Continuous Movement
Actually, the pig stops moving after it reaches the last waypoint. For Hogville, it makes more sense and will be more challenging if the pigs keep moving. Otherwise, you could simply draw a path to the pigs straight to their pens and the game would be over!
Let’s correct this. Open Pig.m and add the following code at the end of move:
:
else { newPosition = CGPointMake(currentPosition.x + _velocity.x * [dt doubleValue], currentPosition.y + _velocity.y * [dt doubleValue]); } |
This code simply continues to move the pig with its most recently calculated velocity when it runs out of waypoints. Remember, velocity includes both direction and speed.
Now find this line inside move:
:
self.position = newPosition; |
Move the above line to the end of the method so that after the if-else
statements.
Build and run, and watch your pig moving and moving and moving… until you can’t see it anymore. This little piggy went off screen!
What you need is a method to check if the pig is about to leave the screen and if so, switch its direction.
Also in Pig.m, add this method:
- (CGPoint)checkBoundaries:(CGPoint)point { //1 CGPoint newVelocity = _velocity; CGPoint newPosition = point; //2 CGPoint bottomLeft = CGPointZero; CGPoint topRight = CGPointMake(self.scene.size.width, self.scene.size.height); //3 if (newPosition.x <= bottomLeft.x) { newPosition.x = bottomLeft.x; newVelocity.x = -newVelocity.x; } else if (newPosition.x >= topRight.x) { newPosition.x = topRight.x; newVelocity.x = -newVelocity.x; } if (newPosition.y <= bottomLeft.y) { newPosition.y = bottomLeft.y; newVelocity.y = -newVelocity.y; } else if (newPosition.y >= topRight.y) { newPosition.y = topRight.y; newVelocity.y = -newVelocity.y; } //4 _velocity = newVelocity; return newPosition; } |
This looks more complicated than it is, so let’s look more closely:
- First, you assign the current velocity and point to local variables.
- Here you define the important points on the screen. You can use
bottomLeft
to check if the pig is moving off screen from the left or bottom sides andtopRight
to check the top and right sides of the screen. You perform these checks inside the followingif
statements, one for each side of the screen. - The first
if
statement checks thex
value ofnewPosition
. If this value is zero or less, the pig is leaving the screen from the left side. To avoid this, you set the pig’sx
-position to the left boundary—zero—and reverse thex
-component of the velocity so the pig starts moving in the opposite direction. The otherif
statements do the same for the remaining three bounds of the screen. - At the end, you change
_velocity
to whatever value you calculated and then returnnewPosition
.
To make use of this new method, change the last line in move:
from this:
self.position = newPosition; |
To this:
self.position = [self checkBoundaries:newPosition]; |
Build and run your project again and watch the pig bounce off of the screen’s borders. You’ve got yourself a pig pen!
Rotating the Sprite
Before adding the actual gameplay, there are some minor improvements you should make. First, the pig doesn’t rotate at all, which looks a bit weird. Like most animals, pigs normally walk facing forward rather than backward or to the side!
Rotate the pig so that it faces the direction it’s moving by adding the following line to the end of move:
in Pig.m:
self.zRotation = atan2f(_velocity.y, _velocity.x) + M_PI_2; |
atan2f
returns the angle between the x-axis and the given point. You add M_PI_2
because the pig image you use faces down and the value you receive from atan2f
assumes the image faces to the right. Therefore, by adding M_PI_2
, you rotate the pig by 90° counterclockwise.
Note: If this is too much math for you, check out our open source SKTUtils library. The library includes a ton of helper functions that abstracts common math like this; for example it includes a CGPointToAngle
method that would be helpful here.
Animating the Sprite
Now that the pig faces the direction it’s moving, it’s time to add a pretty animation. Add the following new instance variable in Pig.m‘s @implementation
section:
SKAction *_moveAnimation; |
Next, go to initWithImageNamed:
and add the following lines directly after the line that creates _wayPoints
:
SKTexture *texture1 = [SKTexture textureWithImageNamed:@"pig_1"]; SKTexture *texture2 = [SKTexture textureWithImageNamed:@"pig_2"]; SKTexture *texture3 = [SKTexture textureWithImageNamed:@"pig_3"]; _moveAnimation = [SKAction animateWithTextures:@[texture1, texture2, texture3] timePerFrame:0.1]; |
This creates three textures, each of which represents a frame of the animation. The last line creates an SKAction
that animates the pig’s movement using the three textures and stores the action in _moveAnimation. The game will show each frame for 0.1 seconds, resulting in a nice walking animation for the pig.
The last step is to run the animation. In Pig.m, go to move:
and add the following lines at the beginning of the method:
if(![self actionForKey:@"moveAction"]) { [self runAction:_moveAnimation withKey:@"moveAction"]; } |
First, you check if an animation named “moveAction” is already running and add it if there is no such animation. This ensures that the game adds the animation only once. Without this check, you wouldn’t see the animation because it would start from the beginning every time you called move:
.
Build and run, and watch your pig animate around the screen as you direct it.
Your Gameplay Strategy
Let’s pause to think about how the gameplay will work and what you need to achieve it.
The basic idea behind Hogville is that pigs will appear at random positions on the left side of the screen and from there, move in random directions. The time between the appearances of each new pig will decrease until it reaches a threshold defined by you.
The player must herd all of these pigs to a trough of food where they can eat and after that, bring them to a barn where they can sleep. If any two pigs collide, the game is over.
Here are the needed steps:
- Add sprites for the food trough and barn.
- Spawn pigs over time.
- Add collision handling.
- Add logic that controls when a pig needs food or is ready to sleep.
- Handle win/lose conditions.
You will go step by step through this list to finish your game. Don’t panic—this tutorial is here to guide you!
Completing the Scene
To complete the first step, open MyScene.h and add the following property to the interface:
@property (nonatomic, strong) SKSpriteNode *homeNode; |
homeNode
will display an image of a barn and act as the final goal for your pigs. You will need to access this property later in the Pig
class, so you make it a public property instead of a private variable.
Then open MyScene.m and add the following instance variable to the @implementation
section:
NSTimeInterval _currentSpawnTime; |
You’ll use _currentSpawnTime
to track the wait time between pig spawns. After each new pig appears, you’ll reduce this value to speed up the next spawn.
Add this new method in MyScene.m to set up the sprites in your scene:
- (void)loadLevel { //1 SKSpriteNode *bg = [SKSpriteNode spriteNodeWithImageNamed:@"bg_2_grassy"]; bg.anchorPoint = CGPointZero; [self addChild:bg]; //2 SKSpriteNode *foodNode = [SKSpriteNode spriteNodeWithImageNamed:@"trough_3_full"]; foodNode.name = @"food"; foodNode.zPosition = 0; foodNode.position = CGPointMake(250.0f, 200.0f); // More code later [self addChild:foodNode]; //3 self.homeNode = [SKSpriteNode spriteNodeWithImageNamed:@"barn"]; self.homeNode.name = @"home"; self.homeNode.zPosition = 0; self.homeNode.position = CGPointMake(380.0f, 20.0f); [self addChild:self.homeNode]; _currentSpawnTime = 5.0; } |
Let’s take a tour of this code:
- This is simply the code to create the background that you already have in
initWithSize:
. You’ll remove it from that method soon. - Here you create an
SKSpriteNode
for the trough and give it the name “food”. You place the node near the center of the screen and add it to the scene. - This creates the barn and positions it in the lower-right corner of your scene. Finally, you set the current spawn time to five seconds. You could make the game easier or harder by increasing or decreasing the initial spawn time.
Now replace all of the code within the if
statement of initWithSize:
with the following call to loadLevel
:
[self loadLevel]; |
Build and run, and you’ll see the stage is now set:
But no pigs are anywhere to be found! Let’s bring in some grunts.
Spawning Pigs
To spawn some pigs, add the following new method to MyScene.m:
- (void)spawnAnimal { //1 _currentSpawnTime -= 0.2; //2 if(_currentSpawnTime < 1.0) { _currentSpawnTime = 1.0; } //3 Pig *pig = [[Pig alloc] initWithImageNamed:@"pig_1"]; pig.position = CGPointMake(20.0f, arc4random() % 300); pig.name = @"pig"; pig.zPosition = 1; [self addChild:pig]; //4 [self runAction: [SKAction sequence:@[[SKAction waitForDuration:_currentSpawnTime], [SKAction performSelector:@selector(spawnAnimal) onTarget:self]]]]; } |
Here’s a step-by-step breakdown of the code above:
- This decreases the time between spawns by 0.2 seconds every time the game spawns a pig.
- You ensure the spawn time never falls below one second, because anything faster than that would make the game too difficult, and if it hit zero, things would probably break.
- Here you create a pig and add it to the scene like you did before in
initWithSize:
. Now you set the pig’s position with a fixed x-value of 20 and a random y-value that ranges between zero and 299. Setting the pig’szPosition
to 1 makes sure the pig renders on top of the lines in the scene, which you’ve added with the defaultzPosition
of zero. - This runs an action sequence. A sequence performs a set of actions in order, one at a time. The result is that the
performSelector
action callsspawnAnimal
again afterwaitForDuration
waits for_currentSpawnTime
seconds. Because you reduce_currentSpawnTime
each time you call this method, you end up callingspawnAnimal
with less and less of a delay.
Now add a call to your method in MyScene.m in initWithSize:
, immediately after the call to [self loadLevel];
:
[self spawnAnimal]; |
Build and run, and watch the pigs appear faster and faster over time.
Detecting Collisions
As you can see in the image above, the pigs move through the trough, barn and even other pigs.
This is a sign your game needs collision detection! Fortunately, you don’t have to create it from scratch. Sprite Kit includes a physics engine that you can use for collision detection.
First, add some physics categories to your scene. Open MyScene.h and add the following typedef
above the @interface
part:
typedef NS_OPTIONS(uint32_t, LDPhysicsCategory) { LDPhysicsCategoryAnimal = 1 << 0, LDPhysicsCategoryFood = 1 << 1, }; |
This creates two categories, one for each type of physics body you will have. You can use these categories to detect collisions between different physics bodies. There are two types of collisions that can occur in this game, those between two pigs and those between a pig and the food trough.
Now open MyScene.m and add a category extension to your scene.
@interface MyScene () <SKPhysicsContactDelegate> @end |
This tells the compiler that your scene is implementing the SKPhysicsContactDelegate protocol.
Now find initWithSize:
and add this right before the call to loadLevel
:
self.physicsWorld.gravity = CGVectorMake(0.0f, 0.0f); self.physicsWorld.contactDelegate = self; |
This configures your physics world. The first line disables gravity in your scene and the second registers your scene as the contact delegate of the physics world. Sprite Kit notifies this delegate whenever two appropriately configured physics bodies begin to touch or stop touching.
To process these collision events, add the following method to MyScene.m:
- (void)didBeginContact:(SKPhysicsContact *)contact { //1 SKNode *firstNode = contact.bodyA.node; SKNode *secondNode = contact.bodyB.node; //2 uint32_t collision = firstNode.physicsBody.categoryBitMask | secondNode.physicsBody.categoryBitMask; //3 if(collision == (LDPhysicsCategoryAnimal | LDPhysicsCategoryAnimal)) { NSLog(@"Animal collision detected"); } else if(collision == (LDPhysicsCategoryAnimal | LDPhysicsCategoryFood)) { NSLog(@"Food collision detected."); } else { NSLog(@"Error: Unknown collision category %d", collision); } } |
Let’s break down what’s happening above:
- These two lines give you the nodes that just collided. There is no specific order for the nodes, so you have to check the objects yourself if you care which is which.
- You perform a bitwise-OR of the categories of the two collided nodes and store it in
collision
. - Here you figure out what kind of collision occurred by comparing
collision
with the bit mask for an animal/animal or animal/food collision. For the moment, you simply log the detected collision to the console.
There’s something essential missing, though. Can you guess what it is?
Adding Physics Bodies
To detect collisions, you have to configure a physics body on any sprite you want to engage in collisions. Start by configuring the trough in MyScene.m by replacing the comment //More code later
in loadLevel
with the following code:
foodNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:foodNode.size]; foodNode.physicsBody.categoryBitMask = LDPhysicsCategoryFood; foodNode.physicsBody.dynamic = NO; |
Your physics body needs geometry to define its shape. In this case, a rectangle with the node’s size will suffice. Since this is the food trough, you assign the corresponding category to it. The last line tells the physics engine not to move this object. That is, things can bounce off of the object, but no force will affect the object itself.
Note: In this game, collisions will not affect the motion of any object—you will use the physics engine only for collision detection.
The second node that needs a physics body is the pig. Open Pig.m and import the scene in order to access the category enum:
#import "MyScene.h" |
Then add the following to initWithImageNamed:
, after the line that initializes _moveAnimation
:
self.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:self.size.width / 2.0f]; self.physicsBody.categoryBitMask = LDPhysicsCategoryAnimal; self.physicsBody.contactTestBitMask = LDPhysicsCategoryAnimal | LDPhysicsCategoryFood; self.physicsBody.collisionBitMask = kNilOptions; |
This is similar to what you did for the trough, except you use a circular physics shape because it fits the pig better than a rectangle. You set the category to indicate the object is an animal and you set contactTestBitMask
to indicate that this node should produce contact notifications whenever it touches any other physics body that belongs to any category inside the bit mask.
You also set the object’s collisionBitMask
to kNilOptions
(which is simply zero) to indicate to the physics engine that forces generated during collisions should not affect the pigs. Basically, you allow pigs to pass right through other physics objects.
Challenge: What kinds of nodes will generate contact notifications when a pig touches them?
Build and run, and let a pig collide with the trough and with another pig. Have a look at the console output and check that the correct logs appear.
Feeding the Pigs
With your collision detection working, you can make it possible to feed your pigs. Inside Pig.m, add the following instance variables to the @implementation
section:
BOOL _hungry; BOOL _eating; |
These flags will keep track of the pig’s current state.
To begin with a hungry pig, add the following line inside initWithImageNamed:
, just after the lines that set up the physics body:
_hungry = YES; |
Still inside Pig.m, change move:
by wrapping its current contents inside an if
statement that checks to make sure the pig is not eating, as shown below:
if(!_eating) { //existing move: code goes here. } |
This prevents your pig from moving while it’s eating. As your mother always said, “Don’t eat and run at the same time!”
When a pig is finished eating, you want it to start walking again. Add the following method to Pig.m to send the pig off in a random direction:
- (void)moveRandom { //1 [_wayPoints removeAllObjects]; //2 int width = (int)CGRectGetWidth(self.scene.frame); int height = (int)CGRectGetHeight(self.scene.frame); //3 CGPoint randomPoint = CGPointMake(arc4random() % width, arc4random() % height); [_wayPoints addObject:[NSValue valueWithCGPoint:randomPoint]]; } |
This method has three simple steps:
- First, you remove all existing waypoints to make the path truly random.
- Then, you get the width and height of the scene to have a range for the random numbers.
- With these values, you create a random
CGPoint
inside your scene and add it as a waypoint. This new waypoint is enough to get the pig moving again.
Now add the following method, which you’ll call when the pig gets to the trough:
- (void)eat { //1 if(_hungry) { //2 [self removeActionForKey:@"moveAction"]; _eating = YES; _hungry = NO; //3 SKAction *blockAction = [SKAction runBlock:^{ _eating = NO; [self moveRandom]; }]; [self runAction:[SKAction sequence:@[[SKAction waitForDuration:1.0], blockAction]]]; } } |
Here’s how you feed a pig:
- When the pig is hungry, it will start eating.
- You remove the walking animation and set
_eating
toYES
. Your pig will stand still on the trough and eat. Once it finishes eating it is no longer hungry, so you set_hungry
toNO
. - Like everything in life, eating takes time, so you run a sequence action that waits for a second and then executes
blockAction
, which sets_eating
toNO
and calls the method you just added to start the pig walking again. You could decrease the eating time to make the game easier.
You want to call eat
from your scene, so add the declaration to the interface in Pig.h:
- (void)eat; |
Now open MyScene.m and find didBeginContact:
. Replace the NSLog
statement that logs “Food collision detected” with the following code:
if([firstNode.name isEqualToString:@"pig"]) { [(Pig *)firstNode eat]; } else { [(Pig *)secondNode eat]; } |
You know this collision is between the pig and the trough, so you simply figure out which node is the pig and call eat
on it.
Build and run, and guide the pig to a trough. He will stop to eat, then move off to a random direction.
Can’t you just imagine that pig squealing with delight? ;]
Finishing the Game
Your game is almost complete, but you still need win and lose conditions. This is an “endless game” in that the player keeps going as long as they can until they lose.
However, you need to make it so that pigs who have eaten can be guided to the barn, at which point they will be removed from the scene. A good place for this check is the move:
method of Pig
.
Open Pig.m and add the following instance variable to the @implementation
:
BOOL _removing; |
You’ll use this flag to mark pigs while they are in the process of leaving the game. This is because you will make them fade off the screen when they’re leaving the game, and you want to have a flag that prevents you from running this animation twice.
Still in Pig.m, add the following new method:
- (void)checkForHome { //1 if(_hungry || _removing) { return; } //2 SKSpriteNode *homeNode = ((MyScene *)self.scene).homeNode; CGRect homeRect = homeNode.frame; //3 if(CGRectIntersectsRect(self.frame, homeRect)) { _removing = YES; [_wayPoints removeAllObjects]; [self removeAllActions]; //4 [self runAction: [SKAction sequence: @[[SKAction group: @[[SKAction fadeAlphaTo:0.0f duration:0.5], [SKAction moveTo:homeNode.position duration:0.5]]], [SKAction removeFromParent]]]]; } } |
What’s happening here?
- Hungry pigs won’t go to sleep, so you first check if the pig is hungry or is already set to be removed from the game.
- Here you get the
frame
rectangle of thehomeNode
. - You then check if the pig’s frame overlaps the barn’s. If that’s the case, you set the pig’s
_removing
flag toYES
and clear its waypoints and any running actions. - Here you run another sequence of actions that first runs a group of actions simultaneously, and when those are done it removes the pig from the scene. The group of actions fades out the pig’s sprite while it moves the pig to the center of the barn.
Now call this method by adding the following line to move:
, right after the line that sets the pig’s zRotation
:
[self checkForHome]; |
Build and run, and guide those pigs home!
Game Over: When Pigs Collide
When two pigs collide, the game will show a “Game Over!” message and then give the player the chance to start a new round.
Open MyScene.m and add a BOOL
instance variable to the @implementation
section to indicate when the game is over:
BOOL _gameOver; |
You don’t want the pigs moving or spawning when the game is over, so wrap the current contents of update:
inside an if
statement, as follows:
if(!_gameOver) { // existing code here } |
Also, add the following code at the beginning of spawnAnimal
to make sure new pigs don’t appear after the player has lost:
if(_gameOver) { return; } |
Now add this method, which you’ll call when two pigs collide:
- (void)handleAnimalCollision { _gameOver = YES; SKLabelNode *gameOverLabel = [SKLabelNode labelNodeWithFontNamed:@"Thonburi-Bold"]; gameOverLabel.text = @"Game Over!"; gameOverLabel.name = @"label"; gameOverLabel.fontSize = 35.0f; gameOverLabel.position = CGPointMake(self.size.width / 2.0f, self.size.height / 2.0f + 20.0f); gameOverLabel.zPosition = 5; SKLabelNode *tapLabel = [SKLabelNode labelNodeWithFontNamed:@"Thonburi-Bold"]; tapLabel.text = @"Tap to restart."; tapLabel.name = @"label"; tapLabel.fontSize = 25.0f; tapLabel.position = CGPointMake(self.size.width / 2.0f, self.size.height / 2.0f - 20.0f); tapLabel.zPosition = 5; [self addChild:gameOverLabel]; [self addChild:tapLabel]; } |
Here you first set _gameOver
to YES
, which ends the game. You add two labels centered on the screen telling the user that the game is over and that they can tap the screen to restart. You set each label’s zPosition
to five to ensure they are in front of all other nodes.
Now call this new method in didBeganContact:
. Replace the NSLog
statement that logs “Animal collision detected” with the following line:
[self handleAnimalCollision]; |
You told the player that they can tap the screen to restart, but right now that isn’t true. Add the following method to MyScene.m to handle restarting:
- (void)restartGame { [self enumerateChildNodesWithName:@"line" usingBlock:^(SKNode *node, BOOL *stop) { [node removeFromParent]; }]; [self enumerateChildNodesWithName:@"pig" usingBlock:^(SKNode *node, BOOL *stop) { [node removeFromParent]; }]; [self enumerateChildNodesWithName:@"label" usingBlock:^(SKNode *node, BOOL *stop) { [node removeFromParent]; }]; _currentSpawnTime = 5.0f; _gameOver = NO; [self spawnAnimal]; } |
restartGame
removes all lines, pigs and labels from the scene, sets the spawn time back to five seconds, sets _gameOver
to NO
and begins spawning animals.
You need to call this method if the user taps the screen while the game is over, so add the following lines to the top of touchesBegan:withEvent:
:
if(_gameOver) { [self restartGame]; } |
All right, then! Build and run once more, and enjoy the line drawing game you created. See if you’re a natural-born swineherd.
Adding Polish
You may have noticed some places where the game behaves strangely. For example, look at the behavior of spawning pigs—they appear to walk in place! You can fix this easily by calling moveRandom
on the pigs when you spawn them.
You’ll need to access this method from your scene, so add the following declaration to the interface in Pig.h:
- (void)moveRandom; |
Then, inside MyScene.m, call this method in spawnAnimal
, just after the line that adds the pig to the scene:
[pig moveRandom]; |
Build and run, and now the pigs move right away:
Now let’s take a closer look at the way your game currently handles touches. Run the app and add a path to a pig, but before the pig arrives at the endpoint, try to change the path by drawing a new one.
As you can see, the pig initially ignores your second command and continues along the first path. Then, after reaching the end point, it moves back to the place where you tried to change direction and follows the second path. Every new path is just tacked onto the old one!
It would be much better if the game replaced the old path with a new path. To make this happen, first open Pig.h and declare a new method:
- (void)clearWayPoints; |
Next, open Pig.m and implement this method as follows:
- (void)clearWayPoints { [_wayPoints removeAllObjects]; } |
This method simply removes all objects from _wayPoints
.
Now go to MyScene.m and add the following line to touchesBegan:withEvent:
, just above the line that calls addPointToMove:
:
[(Pig *)node clearWayPoints]; |
You call clearWayPoints
every time the player touches a pig.
Build and run, and now the pigs listen to your every command!
Challenge: Add a HUD to the scene showing the player how many pigs they have removed. See if you can accomplish this by yourself before checking the solution below.
Where to Go From Here?
Here is the final project with all the code from the above tutorial.
There is still a lot of room for improvement in your game. But that’s OK—it gives you plenty of opportunities to practice your skills, old and new!
- You could reduce the area searched when removing waypoints, which would make the pig look like it was on top of the line rather than having the line always just out of the pig’s reach.
- If you move your finger slowly while creating a line, it currently adds far too many points, producing an unpleasant visual experience. You could apply a smoothing algorithm to the points, such as the basic one described here, to reduce redundant points and improve the quality of both the line rendering and the pig’s motion.
- It’s currently possible for a new pig to spawn with no warning right on top of an existing pig, ending the game immediately. How would you fix this problem?
- You could add an indicator to warn the player of incoming pigs.
- You could add different levels that you load from a plist, as well as different animals.
- You could use the additional trough images provided with the starter project to make the trough appear empty after a certain amount of feeding, and require the player to refill the trough by tapping on it.
- To tell you the truth, I really don’t know why the game should end when two animals collide. It would be funnier if the animals began running in random directions for a short time after a collision. Because everything must come to an end (this tutorial, too) you could add a counter that ends the game after five collisions.
Whether or not you implement any of these suggestions, I’m sure you have your own excellent ideas, and now you know more of what you need to bring them to the App Store. Make sure to drop me a note when your game is available.
How to Make a Line Drawing Game with Sprite Kit is a post from: Ray Wenderlich
The post How to Make a Line Drawing Game with Sprite Kit appeared first on Ray Wenderlich.