How to Make a Line Drawing Game with Sprite Kit
Learn how to make a line drawing game like Flight Control and Harbor Master in this Sprite Kit tutorial! By Jean-Pierre Distler.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Make a Line Drawing Game with Sprite Kit
50 mins
- Getting Started
- Adding the Background… and a Pig!
- Moving the Sprite
- Responding to Touches
- Drawing Lines
- Continuous Movement
- Rotating the Sprite
- Animating the Sprite
- Your Gameplay Strategy
- Completing the Scene
- Spawning Pigs
- Detecting Collisions
- Adding Physics Bodies
- Feeding the Pigs
- Finishing the Game
- Game Over: When Pigs Collide
- Adding Polish
- Where to Go From Here?
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];
p = [self.scene convertPointToView:p];
//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. You will use a CAShapeLayer to draw the path so you must convert the point from Sprite Kit to UIKit coordinates. - 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
NSMutableArray *temp = [NSMutableArray array];
for(CALayer *layer in self.view.layer.sublayers) {
if([layer.name isEqualToString:@"line"]) {
[temp addObject:layer];
}
}
[temp makeObjectsPerformSelector:@selector(removeFromSuperlayer)];
//2
[self enumerateChildNodesWithName:@"pig" usingBlock:^(SKNode *node, BOOL *stop) {
//3
CAShapeLayer *lineLayer = [CAShapeLayer layer];
lineLayer.name = @"line";
lineLayer.strokeColor = [UIColor grayColor].CGColor;
lineLayer.fillColor = nil;
//4
CGPathRef path = [(Pig *)node createPathToMove];
lineLayer.path = path;
CGPathRelease(path);
[self.view.layer addSublayer:lineLayer];
}];
}
Here’s what’s happening:
- You'll redraw the path every frame, so first you remove any old lines. To do so, you enumerate over all layers and store every layer with the name "line" inside a temporary array. After that you remove them from the view.
- Next, you enumerate over all the pigs in your scene.
- For each pig, you create an CAShapeLayer and name it "line". Next you set the stroke color of the shape to gray and the fill color to nil. 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 tolineLayer
'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 addlineLayer
to your view's layer 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!