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.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 6 of this article. Click here to view the first page.

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.

Note: You use the 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)];
}];
  1. First, you calculate the time since the last call to update: and store it in _dt. Then, you assign currentTime to _lastUpdateTime so you have it for the next call.
  2. Here is the other use case for the name property. You use SKScene‘s method enumerateChildNodesWithName:usingBlock: to enumerate over all nodes with the name pig. On these nodes, you call move:, passing _dt as the argument. Since SKNode has no method called move:, you cast it to Pig to make Xcode and the compiler happy.

Now build and run, and let the pig follow your finger as you draw a path.

Pig following mouse

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;
}
  1. First, you create a mutable CGPathRef so you can add points to it.
  2. 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.
  3. 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, while CGPathAddLineToPoint() is the actual drawing with the pen on the paper.
  4. At the end, you return the path.
Note: Are you thinking about memory leaks? You're correct that ARC does not support 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:

  1. 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.
  2. Next, you enumerate over all the pigs in your scene.
  3. 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.
  4. You use the method you just added to Pig to create a new path and assign it to lineLayer's path property. Then you call CGPathRelease 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 add lineLayer 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!

Drawn_Lines