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
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

A Smarter Way to Fill the Array

When you run the game now, there may already be such chains on the screen. That’s no good; you only want matches after the user swaps two cookies or after new cookies fall down the screen.

Here’s your rule: Whenever it’s the user’s turn to make a move, no matches may exist on the board. To guarantee this is the case, you have to make the method that fills up the cookies array a bit smarter.

Go to Level.swift and find createInitialCookies(). Replace the single line that calculates the random cookieType with the following:

var cookieType: CookieType
repeat {
  cookieType = CookieType.random()
} while (column >= 2 &&
  cookies[column - 1, row]?.cookieType == cookieType &&
  cookies[column - 2, row]?.cookieType == cookieType)
  || (row >= 2 &&
    cookies[column, row - 1]?.cookieType == cookieType &&
    cookies[column, row - 2]?.cookieType == cookieType)

This piece of logic picks the cookie type at random and makes sure that it never creates a chain of three or more.

If the new random cookie causes a chain of three, the method tries again. The loop repeats until it finds a random cookie that does not create a chain of three or more. It only has to look to the left or below because there are no cookies yet on the right or above.

Run the app and verify that there are no longer any chains in the initial state of the game.

Track Allowable Swaps

You only want to let the player swap two cookies if it would result in either (or both) of these cookies making a chain of three or more.

You need to add some logic to the game to detect whether a swap results in a chain. To accomplish this, you'll build a list of all possible moves after the level is shuffled. Then you only have to check if the attempted swap is in that list. You're going to need a set of Swaps which means Swap must be Hashable.

Open Swap.swift and add Hashable to the struct declaration:

struct Swap: CustomStringConvertible, Hashable {

Next add the following methods to implement Hashable:

var hashValue: Int {
  return cookieA.hashValue ^ cookieB.hashValue
}

static func ==(lhs: Swap, rhs: Swap) -> Bool {
  return (lhs.cookieA == rhs.cookieA && lhs.cookieB == rhs.cookieB) ||
    (lhs.cookieB == rhs.cookieA && lhs.cookieA == rhs.cookieB)
}

You declare that a swap is equal if it refers to the same two cookies, regardless of the order.

In Level.swift, add a new property:

private var possibleSwaps: Set<Swap> = []

Again, you’re using a Set here instead of an Array because the order of the elements in this collection isn’t important. This Set will contain Swap objects. If the player tries to swap two cookies that are not in the set, then the game won’t accept the swap as a valid move.

At the start of each turn, you need to detect which cookies the player can swap. You’re going to make this happen in shuffle(). Still in Level.swift, replace that method with:

func shuffle() -> Set<Cookie> {
  var set: Set<Cookie>
  repeat {
    set = createInitialCookies()
    detectPossibleSwaps()
    print("possible swaps: \(possibleSwaps)")
  } while possibleSwaps.count == 0

  return set
}

As before, this calls createInitialCookies() to fill up the level with random cookie objects. But then it calls a new method that you will add shortly, detectPossibleSwaps(), to fill up the new possibleSwaps set.

detectPossibleSwaps() will use a helper method to see if a cookie is part of a chain. Add this method now:

private func hasChain(atColumn column: Int, row: Int) -> Bool {
  let cookieType = cookies[column, row]!.cookieType

  // Horizontal chain check
  var horizontalLength = 1

  // Left
  var i = column - 1
  while i >= 0 && cookies[i, row]?.cookieType == cookieType {
    i -= 1
    horizontalLength += 1
  }

  // Right
  i = column + 1
  while i < numColumns && cookies[i, row]?.cookieType == cookieType {
    i += 1
    horizontalLength += 1
  }
  if horizontalLength >= 3 { return true }

  // Vertical chain check
  var verticalLength = 1

  // Down
  i = row - 1
  while i >= 0 && cookies[column, i]?.cookieType == cookieType {
    i -= 1
    verticalLength += 1
  }

  // Up
  i = row + 1
  while i < numRows && cookies[column, i]?.cookieType == cookieType {
    i += 1
    verticalLength += 1
  }
  return verticalLength >= 3
}

A chain is three or more consecutive cookies of the same type in a row or column.

Given a cookie in a particular square on the grid, this method first looks to the left. As long as it finds a cookie of the same type, it increments horizontalLength and keeps going left. It then checks the other three directions.

Now that you have this method, you can implement detectPossibleSwaps(). Here’s how it will work at a high level:

  1. It will step through the rows and columns of the 2-D grid and simply swap each cookie with the one next to it, one at a time.
  2. If swapping these two cookies creates a chain, it will add a new Swap object to the list of possibleSwaps.
  3. Then, it will swap these cookies back to restore the original state and continue with the next cookie until it has swapped them all.
  4. It will go through the above steps twice: once to check all horizontal swaps and once to check all vertical swaps.

It’s a big one, so you’ll take it in parts!

First, add the outline of the method:

func detectPossibleSwaps() {
  var set: Set<Swap> = []

  for row in 0..<numRows {
    for column in 0..<numColumns {
      if let cookie = cookies[column, row] {

        // TODO: detection logic goes here
      }
    }
  }

  possibleSwaps = set
}

This is pretty simple: The method loops through the rows and columns, and for each spot, if there is a cookie rather than an empty square, it performs the detection logic. Finally, the method places the results into the possibleSwaps property. Ignore the two warnings for now.

The detection will consist of two separate parts that do the same thing but in different directions. First you want to swap the cookie with the one on the right, and then you want to swap the cookie with the one above it. Remember, row 0 is at the bottom so you’ll work your way up.

Add the following code where it says “TODO: detection logic goes here”:

// Have a cookie in this spot? If there is no tile, there is no cookie.
if column < numColumns - 1,
  let other = cookies[column + 1, row] {
  // Swap them
  cookies[column, row] = other
  cookies[column + 1, row] = cookie

  // Is either cookie now part of a chain?
  if hasChain(atColumn: column + 1, row: row) ||
    hasChain(atColumn: column, row: row) {
    set.insert(Swap(cookieA: cookie, cookieB: other))
  }

  // Swap them back
  cookies[column, row] = cookie
  cookies[column + 1, row] = other
}

This attempts to swap the current cookie with the cookie on the right, if there is one. If this creates a chain of three or more, the code adds a new Swap object to the set.

Now add the following code directly below the code above:

if row < numRows - 1,
            let other = cookies[column, row + 1] {
            cookies[column, row] = other
            cookies[column, row + 1] = cookie
            
            // Is either cookie now part of a chain?
            if hasChain(atColumn: column, row: row + 1) ||
              hasChain(atColumn: column, row: row) {
              set.insert(Swap(cookieA: cookie, cookieB: other))
            }
            
            // Swap them back
            cookies[column, row] = cookie
            cookies[column, row + 1] = other
          }
        }
        else if column == numColumns - 1, let cookie = cookies[column, row] {
          if row < numRows - 1,
            let other = cookies[column, row + 1] {
            cookies[column, row] = other
            cookies[column, row + 1] = cookie
            
            // Is either cookie now part of a chain?
            if hasChain(atColumn: column, row: row + 1) ||
              hasChain(atColumn: column, row: row) {
              set.insert(Swap(cookieA: cookie, cookieB: other))
            }
            
            // Swap them back
            cookies[column, row] = cookie
            cookies[column, row + 1] = other
          }

The If statement does exactly the same thing, but for the cookie above instead of on the right. The Else If statement covers vertical swaps in last column (which would otherwise be undetected.)

Now run the app and you should see something like this in the Xcode console:

possible swaps: [
swap type:SugarCookie square:(6,5) with type:Cupcake square:(7,5),
swap type:Croissant square:(3,3) with type:Macaroon square:(4,3),
swap type:Danish square:(6,0) with type:Macaroon square:(6,1),
swap type:Cupcake square:(6,4) with type:SugarCookie square:(6,5),
swap type:Croissant square:(4,2) with type:Macaroon square:(4,3),
. . .
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.