How to Make a Game Like Candy Crush With SpriteKit and Swift: Part 2

In the second half of this tutorial about making a Candy Crush-like mobile game using Swift and SpriteKit, you’ll learn how to finish the game including detecting swipes, swapping cookies and finding cookie chains. By Kevin Colligan.

Leave a rating/review
Download materials
Save for later
Share

Update note: This SpriteKit tutorial has been updated for Xcode 9.3 and Swift 4.1 by Kevin Colligan. The original tutorial was written by Matthijs Hollemans and subsequently updated by Morten Faarkrog.

Welcome back to our “How to Make a Game Like Candy Crush” tutorial with SpriteKit and Swift series.

  • In the first part, you toured the starter project, built the game foundation and wrote the logic for loading levels.
  • (You’re here) In the second part, you’ll focus on detecting swipes and swapping cookies.
  • In the third part, you’ll work on finding and removing chains, refilling the board and keeping score.

This tutorial picks up where you left off in the last part. Use the Download Materials button at the top or bottom of this tutorial to download the starter project if you need it.

Add Swipe Gestures

In Cookie Crunch Adventure, you want the player to be able to swap two cookies by swiping left, right, up or down.

Detecting swipes is a job for GameScene. If the player touches a cookie on the screen, it might be the start of a valid swipe motion. Which cookie to swap with the touched cookie depends on the direction of the swipe.

To recognize the swipe motion, you’ll use the touchesBegan, touchesMoved and touchesEnded methods from GameScene.

Go to GameScene.swift and add two private properties to the class:

private var swipeFromColumn: Int?
private var swipeFromRow: Int?

These properties record the column and row numbers of the cookie that the player first touched when she started her swipe movement. These are implicitly initialized to nil, as they are optionals.

You first need to add a new convertPoint(_:) method. It’s the opposite of pointFor(column:, row:), so add this right below it.

private func convertPoint(_ point: CGPoint) -> (success: Bool, column: Int, row: Int) {
  if point.x >= 0 && point.x < CGFloat(numColumns) * tileWidth &&
    point.y >= 0 && point.y < CGFloat(numRows) * tileHeight {
    return (true, Int(point.x / tileWidth), Int(point.y / tileHeight))
  } else {
    return (false, 0, 0)  // invalid location
  }
}

This method takes a CGPoint that is relative to the cookiesLayer and converts it into column and row numbers. The return value of this method is a tuple with three values: 1) the boolean that indicates success or failure; 2) the column number; and 3) the row number. If the point falls outside the grid, this method returns false for success.

Now add touchesBegan(_:with:):

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
  // 1
  guard let touch = touches.first else { return }
  let location = touch.location(in: cookiesLayer)
  // 2
  let (success, column, row) = convertPoint(location)
  if success {
    // 3
    if let cookie = level.cookie(atColumn: column, row: row) {
      // 4
      swipeFromColumn = column
      swipeFromRow = row
    }
  }
}

The game will call touchesBegan(_:with:) whenever the user puts her finger on the screen. Here’s what the method does, step by step:

  1. It converts the touch location, if any, to a point relative to the cookiesLayer.
  2. Then, it finds out if the touch is inside a square on the level grid by calling convertPoint(_:). If so, this might be the start of a swipe motion. At this point, you don’t know yet whether that square contains a cookie, but at least the player put her finger somewhere inside the 9x9 grid.
  3. Next, the method verifies that the touch is on a cookie rather than on an empty square. You'll get a warning here about cookie being unused, but you'll need it later so ignore the warning.
  4. Finally, it records the column and row where the swipe started so you can compare them later to find the direction of the swipe.

To perform a valid swipe, the player also has to move her finger out of the current square. You’re only interested in the general direction of the swipe, not the exact destination.

The logic for detecting the swipe direction goes into touchesMoved(_:with:), so add this method next:

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
  // 1
  guard swipeFromColumn != nil else { return }

  // 2
  guard let touch = touches.first else { return }
  let location = touch.location(in: cookiesLayer)

  let (success, column, row) = convertPoint(location)
  if success {

    // 3
    var horizontalDelta = 0, verticalDelta = 0
    if column < swipeFromColumn! {          // swipe left
      horizontalDelta = -1
    } else if column > swipeFromColumn! {   // swipe right
      horizontalDelta = 1
    } else if row < swipeFromRow! {         // swipe down
      verticalDelta = -1
    } else if row > swipeFromRow! {         // swipe up
      verticalDelta = 1
    }

    // 4
    if horizontalDelta != 0 || verticalDelta != 0 {
      trySwap(horizontalDelta: horizontalDelta, verticalDelta: verticalDelta)

      // 5
      swipeFromColumn = nil
    }
  }
}

Here is what this does:

  1. If swipeFromColumn is nil, then either the swipe began outside the valid area or the game has already swapped the cookies and you need to ignore the rest of the motion. You could keep track of this in a separate boolean but using swipeFromColumn is just as easy; that's why you made it an optional.
  2. This is similar to what touchesBegan(_:with:) does to calculate the row and column numbers currently under the player’s finger.
  3. Here the method figures out the direction of the player’s swipe by simply comparing the new column and row numbers to the previous ones. Note that you’re not allowing diagonal swipes. Since you're using else if statements, only one of horizontalDelta or verticalDelta will be set.
  4. trySwap(horizontalDelta:verticalDelta:) is called only if the player swiped out of the old square.
  5. By setting swipeFromColumn back to nil, the game will ignore the rest of this swipe motion.

The hard work of cookie swapping goes into a new method:

private func trySwap(horizontalDelta: Int, verticalDelta: Int) {
  // 1
  let toColumn = swipeFromColumn! + horizontalDelta
  let toRow = swipeFromRow! + verticalDelta
  // 2
  guard toColumn >= 0 && toColumn < numColumns else { return }
  guard toRow >= 0 && toRow < numRows else { return }
  // 3
  if let toCookie = level.cookie(atColumn: toColumn, row: toRow),
    let fromCookie = level.cookie(atColumn: swipeFromColumn!, row: swipeFromRow!) {
    // 4
    print("*** swapping \(fromCookie) with \(toCookie)")
  }
}

This is called "try swap" for a reason. At this point, you only know that the player swiped up, down, left or right, but you don’t yet know if there is a cookie to swap in that direction.

  1. You calculate the column and row numbers of the cookie to swap with.
  2. It is possible that the toColumn or toRow is outside the 9x9 grid. This can occur when the user swipes from a cookie near the edge of the grid. The game should ignore such swipes.
  3. The final check is to make sure that there is actually a cookie at the new position. You can't swap if there’s no second cookie! This happens when the user swipes into a gap where there is no tile.
  4. When you get here, it means everything is OK and this is a valid swap! For now, you log both cookies to the Xcode console.

For completeness’s sake, you should also implement touchesEnded(_:with:), which is called when the user lifts her finger from the screen, and touchesCancelled(_:with:), which happens when iOS decides that it must interrupt the touch (for example, because of an incoming phone call).

Add the following:

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  swipeFromColumn = nil
  swipeFromRow = nil
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
  touchesEnded(touches, with: event)
}

If the gesture ends, regardless of whether it was a valid swipe, you reset the starting column and row numbers to nil.

Build and run, and try out different swaps:

Valid swipe

You won’t see anything happen in the game yet, but the debug pane logs your attempts to make a valid swap.

Kevin Colligan

Contributors

Kevin Colligan

Author

Alex Curran

Tech Editor

Jean-Pierre Distler

Final Pass Editor

Richard Critz

Team Lead

Over 300 content creators. Join our team.