How to Make a Game Like Candy Crush with SpriteKit and Swift: Part 3

Updated for Xcode 9.3 and Swift 4.1. Learn how to make a Candy Crush-like mobile game, using Swift and SpriteKit to animate and build the logic of your game. 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 built the game foundation.
  • In the second part, you worked on swipes and swapping cookies.
  • (You’re here) In the third part, you’ll find and remove chains, refill the grid and keep 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.

Getting Started

You’ve done everything needed to allow the player to swap cookies. Swaps always lead to a chain of three or more matching cookies. The next thing to do is to remove those matching cookies from the screen and reward the player with points.

This is the sequence of events:

Finding the Chains

The game only lets the player make a swap if it will result in a chain of three or more cookies of the same type. But a single swipe could create multiple chains. To find them all, you’ll make a class that describes a chain.

Go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Chain.swift and click Create.

Replace the contents of Chain.swift with:

class Chain: Hashable, CustomStringConvertible {
  var cookies: [Cookie] = []

  enum ChainType: CustomStringConvertible {
    case horizontal
    case vertical

    var description: String {
      switch self {
      case .horizontal: return "Horizontal"
      case .vertical: return "Vertical"
      }
    }
  }

  var chainType: ChainType

  init(chainType: ChainType) {
    self.chainType = chainType
  }

  func add(cookie: Cookie) {
    cookies.append(cookie)
  }

  func firstCookie() -> Cookie {
    return cookies[0]
  }

  func lastCookie() -> Cookie {
    return cookies[cookies.count - 1]
  }

  var length: Int {
    return cookies.count
  }

  var description: String {
    return "type:\(chainType) cookies:\(cookies)"
  }

  var hashValue: Int {
    return cookies.reduce (0) { $0.hashValue ^ $1.hashValue }
  }

  static func ==(lhs: Chain, rhs: Chain) -> Bool {
    return lhs.cookies == rhs.cookies
  }
}

A chain has a list of cookie objects and a type: it’s either horizontal — a row of cookies — or vertical — a column. The type is defined as an enum; it is nested inside Chain because these two things are tightly coupled. You can also add more complex chain types, such as L- and T-shapes.

You’re using an array to store cookies and not a set because you need to know their order. This makes it easier to combine multiple chains into a single one — or to detect L- or T-shapes.

Open Level.swift. You’re going to add a method named removeMatches(), but before you get to that, you need a couple of helper methods to do the heavy lifting of finding chains.

To find a chain, you’ll need a pair of for loops that step through each square of the level grid.

While stepping through the cookies in a row horizontally, you want to find the first cookie that starts a chain. You know a cookie begins a chain if at least the next two cookies on its right are of the same type. Then, you skip over all cookies of the same type until you find one that breaks the chain. You repeat this until you’ve looked at all possibilities.

Add this method to scan for horizontal chains:

private func detectHorizontalMatches() -> Set<Chain> {
  // 1
  var set: Set<Chain> = []
  // 2
  for row in 0..<numRows {
    var column = 0
    while column < numColumns-2 {
      // 3
      if let cookie = cookies[column, row] {
        let matchType = cookie.cookieType
        // 4
        if cookies[column + 1, row]?.cookieType == matchType &&
          cookies[column + 2, row]?.cookieType == matchType {
          // 5
          let chain = Chain(chainType: .horizontal)
          repeat {
            chain.add(cookie: cookies[column, row]!)
            column += 1
          } while column < numColumns && cookies[column, row]?.cookieType == matchType

          set.insert(chain)
          continue
        }
      }
      // 6
      column += 1
    }
  }
  return set
}

Here’s how this method works:

  1. You create a new set to hold the horizontal chains. Later, you’ll remove the cookies in these chains from the playing field.
  2. You loop through the rows and columns. You don’t need to look at the last two columns because these cookies can never begin a new chain.
  3. You skip over any gaps in the level design.
  4. You check whether the next two columns have the same cookie type.
  5. At this point, there is a chain of at least three cookies, but potentially there are more. This steps through all the matching cookies until it finds a cookie that breaks the chain or the end of the grid. Then, it adds all the matching cookies to a new Chain.
  6. If the next two cookies don’t match the current one or if there is an empty tile, then there is no chain, so you skip over the cookie.

Next, add this method to scan for vertical cookie matches:

private func detectVerticalMatches() -> Set<Chain> {
  var set: Set<Chain> = []

  for column in 0..<numColumns {
    var row = 0
    while row < numRows-2 {
      if let cookie = cookies[column, row] {
        let matchType = cookie.cookieType

        if cookies[column, row + 1]?.cookieType == matchType &&
          cookies[column, row + 2]?.cookieType == matchType {
          let chain = Chain(chainType: .vertical)
          repeat {
            chain.add(cookie: cookies[column, row]!)
            row += 1
          } while row < numRows && cookies[column, row]?.cookieType == matchType

          set.insert(chain)
          continue
        }
      }
      row += 1
    }
  }
  return set
}

The vertical version has the same kind of logic, but loops first by column and then by row.

You don’t immediately remove the cookies from the level as soon as you detect that they’re part of a chain because they may be part of two chains at the same time.

Now that the two match detectors are ready, add the implementation for removeMatches():

func removeMatches() -> Set<Chain> {
  let horizontalChains = detectHorizontalMatches()
  let verticalChains = detectVerticalMatches()

  print("Horizontal matches: \(horizontalChains)")
  print("Vertical matches: \(verticalChains)")

  return horizontalChains.union(verticalChains)
}

This method calls the two helper methods and then combines their results into a single set. Later, you’ll add more logic to this method but for now you’re only interested in finding the matches and returning the set.

You still need to call removeMatches() from somewhere and that somewhere is GameViewController.swift. Add this helper method:

func handleMatches() {
  let chains = level.removeMatches()
  // TODO: do something with the chains set
}

Later, you'll fill out this method with code to remove cookie chains and drop other cookies into the empty tiles.

In handleSwipe(), replace the call to scene.animateSwap(swap) and its completion closure with this:

scene.animate(swap, completion: handleMatches)

Build and run, and swap two cookies to make a chain. You should now see something like this in Xcode’s console:

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.