How to Make a Line Drawing Game with Sprite Kit and Swift

Learn how to make a Line Drawing Game like Flight Control with Sprite Kit and Swift! By Jean-Pierre Distler.

Leave a rating/review
Save for later
Share

Learn how to make a line drawing game like Flight Control!

Learn how to make a line drawing game like Flight Control!

Learn how to make a line drawing game like Flight Control!

Note from Ray: This is a Swift update to a popular Objective-C tutorial on our site, released as part of the iOS 8 Feast. Enjoy!

Update 12/09/2014
Updated for Xcode 6.1.1.

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 Swift tutorial assumes you have working knowledge of Sprite Kit and Swift. If you’re new to Sprite Kit, check out the beginner tutorials on the site or the book, iOS Games by Tutorials. For an introduction to Swift, check out this beginner Swift tutorial here.

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 removed GameScene.sks, changed GameViewController‘s viewDidLoad method to create the game scene (without using the SKS file) and add it to the view. 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:

StarterProject

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 GameScene.swift and add the following methods:

override init(size: CGSize) {
  super.init(size: size)

  let bg = SKSpriteNode(imageNamed: "bg_2_grassy")
  bg.anchorPoint = CGPoint(x: 0, y: 0)
  addChild(bg)
          
  let pig = SKSpriteNode(imageNamed: "pig_1")
  pig.position = CGPoint(x: size.width / 2, y: size.height / 2)
  addChild(pig)
}

required init(coder aDecoder: NSCoder) {
  fatalError("init(coder:) has not been implemented")
}

Here you override init(size:) to add 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.

You also implement the required init(coder:) method, although only with a dummy implementation because you won’t need it in this game.

Build and run your game and behold your plump, rosy pig in the middle of a sunny, green field.

First_Run

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 Swift File template and click Next. Name the file Pig.swift and then click Create.

In Pig.swift add:

 
import SpriteKit

class Pig: SKSpriteNode {

}

First you need to import SpriteKit then you create the class Pig as a subclass of SKSpriteNode. Now add the following init methods to the Pig class:

init(imageNamed name: String) {
  let texture = SKTexture(imageNamed: name)
  super.init(texture: texture, color: nil, size: texture.size())
}
   
required init(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
}

In init(imageNamed:) you create a texture and then call super.init(texture:, color:, size:)
Now add a constant to your class. Add the following right after the line class Pig: SKSpriteNode:

let POINTS_PER_SEC: CGFloat = 80.0

This constant defines the speed of the pig as 80 points per second.

Unfortunately type casts aren’t that good for readability and so I decided to have one weird line of Swift code but less casts needed.

Note: You set the type of POINTS_PER_SEC explicitly to CGFloat. If you use Swifts type inference
POINTS_PER_SEC would be a Double. Many SpriteKit and also UIKit methods takes CGFloats as an argument, but Swift’s type safety avoids implicit type conversion, so you have to cast Swift’s Double and Float to CGFloat.

Unfortunately type casts aren’t that good for readability and so I decided to have one weird line of Swift code but less casts needed.

Next, declare two more properties by adding the following code right below POINTS_PER_SEC:

var wayPoints: [CGPoint] = []
var velocity = CGPoint(x: 0, y: 0)

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.

Now you need a method to add waypoints to the pig. Add the method addMovingPoint() like this:

func addMovingPoint(point: CGPoint) {
  wayPoints.append(point)
}

This method simply adds the given point to the wayPoints array.

Now add another method move(). You will add more code later but for the moment add it like shown below:

func move(dt: NSTimeInterval) {
  let currentPosition = position 
  var newPosition = position
  
  //1
  if wayPoints.count > 0 {
    let targetPoint = wayPoints[0]

    //2 TODO: Add movement logic here

    //3
    if frame.contains(targetPoint) {
      wayPoints.removeAtIndex(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:

  1. 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.
  2. This comment marks where you’ll put the code that updates the pig’s position. You’ll add that code next.
  3. Finally, you check if the pig has reached the waypoint by seeing if the pig’s frame contains the targetPoint. In this case, you remove the point from the array so that your next call to move 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.

Beam me up, piggy!

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
let offset = CGPoint(x: targetPoint.x - currentPosition.x, y: targetPoint.y - currentPosition.y)
let length = Double(sqrtf(Float(offset.x * offset.x) + Float(offset.y * offset.y)))
let direction = CGPoint(x:CGFloat(offset.x) / CGFloat(length), y: CGFloat(offset.y) / CGFloat(length))
velocity = CGPoint(x: direction.x * POINTS_PER_SEC, y: direction.y * POINTS_PER_SEC)

//2
newPosition = CGPoint(x:currentPosition.x + velocity.x * CGFloat(dt), y:currentPosition.y + velocity.y * CGFloat(dt))
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, a CGPoint representing the differences in both the x and y 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.

velocity

You divide offset‘s components by length to create a normalized vector (a vector of length 1) that points in the direction of the waypoint and you store it in direction.

Finally, you multiply direction by POINTS_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.

  1. 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, a CGPoint representing the differences in both the x and y 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.

    velocity

    You divide offset‘s components by length to create a normalized vector (a vector of length 1) that points in the direction of the waypoint and you store it in direction.

    Finally, you multiply direction by POINTS_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.

  2. You calculate the pig’s new position by multiplying velocity by dt and adding the result to the pig’s current position. Because velocity stores the distance the pig should travel in one second and dt holds the number of seconds that have passed since the last call to move, multiplying the two results in the distance the pig should travel in dt seconds.

You’re done here for the moment. It’s time to use your new class and move the pig.