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

Adding New Cookies

There’s one more thing to do to complete the game loop. Falling cookies leave their own holes at the top of each column.

You need to top up these columns with new cookies. Add a new method to Level.swift:

func topUpCookies() -> [[Cookie]] {
  var columns: [[Cookie]] = []
  var cookieType: CookieType = .unknown

  for column in 0..<numColumns {
    var array: [Cookie] = []

    // 1
    var row = numRows - 1
    while row >= 0 && cookies[column, row] == nil {
      // 2
      if tiles[column, row] != nil {
        // 3
        var newCookieType: CookieType
        repeat {
          newCookieType = CookieType.random()
        } while newCookieType == cookieType
        cookieType = newCookieType
        // 4
        let cookie = Cookie(column: column, row: row, cookieType: cookieType)
        cookies[column, row] = cookie
        array.append(cookie)
      }

      row -= 1
    }
    // 5
    if !array.isEmpty {
      columns.append(array)
    }
  }
  return columns
}

Here’s how it works:

  1. You loop through the column from top to bottom. This while loop ends when cookies[column, row] is not nil — that is, when it has found a cookie.
  2. You ignore gaps in the level, because you only need to fill up grid squares that have a tile.
  3. You randomly create a new cookie type. It can’t be equal to the type of the last new cookie, to prevent too many "freebie" matches.
  4. You create the new Cookie object and add it to the array for this column.
  5. As before, if a column does not have any holes, you don't add it to the final array.

The array that topUpCookies() returns contains a sub-array for each column that had holes. The cookie objects in these arrays are ordered from top to bottom. This is important to know for the animation method coming next.

Switch to GameScene.swift and the new animation method:

func animateNewCookies(in columns: [[Cookie]], completion: @escaping () -> Void) {
  // 1
  var longestDuration: TimeInterval = 0

  for array in columns {
    // 2
    let startRow = array[0].row + 1

    for (index, cookie) in array.enumerated() {
      // 3
      let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
      sprite.size = CGSize(width: tileWidth, height: tileHeight)
      sprite.position = pointFor(column: cookie.column, row: startRow)
      cookiesLayer.addChild(sprite)
      cookie.sprite = sprite
      // 4
      let delay = 0.1 + 0.2 * TimeInterval(array.count - index - 1)
      // 5
      let duration = TimeInterval(startRow - cookie.row) * 0.1
      longestDuration = max(longestDuration, duration + delay)
      // 6
      let newPosition = pointFor(column: cookie.column, row: cookie.row)
      let moveAction = SKAction.move(to: newPosition, duration: duration)
      moveAction.timingMode = .easeOut
      sprite.alpha = 0
      sprite.run(
        SKAction.sequence([
          SKAction.wait(forDuration: delay),
          SKAction.group([
            SKAction.fadeIn(withDuration: 0.05),
            moveAction,
            addCookieSound])
          ]))
    }
  }
  // 7
  run(SKAction.wait(forDuration: longestDuration), completion: completion)
}

This is very similar to the “falling cookies” animation. The main difference is that the cookie objects are now in reverse order in the array, from top to bottom. This is what the method does:

  1. The game is not allowed to continue until all the animations are complete, so you calculate the duration of the longest animation to use later in step 7.
  2. The new cookie sprite should start out just above the first tile in this column. An easy way to find the row number of this tile is to look at the row of the first cookie in the array, which is always the top-most one for this column.
  3. You create a new sprite for the cookie.
  4. The higher the cookie, the longer you make the delay, so the cookies appear to fall after one another.
  5. You calculate the animation’s duration based on far the cookie has to fall.
  6. You animate the sprite falling down and fading in. This makes the cookies appear less abruptly out of thin air at the top of the grid.
  7. You wait until the animations are done before continuing the game.

Finally, in GameViewController.swift, once again replace handleMatches() with the following:

func handleMatches() {
  let chains = level.removeMatches()
  scene.animateMatchedCookies(for: chains) {
    let columns = self.level.fillHoles()
    self.scene.animateFallingCookies(in: columns) {
      let columns = self.level.topUpCookies()
      self.scene.animateNewCookies(in: columns) {
        self.view.isUserInteractionEnabled = true
      }
    }
  }
}

Try it out by building and running.

A Cascade of Cookies

When the cookies fall down to fill up the holes and new cookies drop from the top, these actions sometimes create new chains of three or more. You need to remove these matching chains and ensure other cookies take their place. This cycle should continue until there are no matches left on the board. Only then should the game give control back to the player.

Handling these possible cascades may sound like a tricky problem, but you’ve already written all the code to do it! You just have to call handleMatches() again and again and again until there are no more chains.

In GameViewController.swift, inside handleMatches(), change the line that sets isUserInteractionEnabled to:

self.handleMatches()

Yep, you’re seeing that right: handleMatches() calls itself. This is called recursion and it’s a powerful programming technique. There’s only one thing you need to watch out for with recursion: at some point, you need to stop it, or the app will go into an infinite loop and eventually crash.

For that reason, add the following to the top of handleMatches(), right after the line that calls level.removeMatches()

if chains.count == 0 {
  beginNextTurn()
  return
}

If there are no more matches, the player gets to move again and the function exits to prevent another recursive call.

Finally, add this new method:

func beginNextTurn() {
  level.detectPossibleSwaps()
  view.isUserInteractionEnabled = true
}

We now have an endless supply of cookies. Build and run to see how this looks.

Scoring Points

In Cookie Crunch Adventure, the player’s objective is to score a certain number of points within a maximum number of swaps. Both of these values come from the JSON level file.

GameViewController.swift includes all the necessary properties to hold the data, and Main.storyboard holds the views which display it.

Because the target score and maximum number of moves are stored in the JSON level file, you should load them into Level. Add the following properties to Level.swift:

var targetScore = 0
var maximumMoves = 0

Still in Level.swift, add these two lines at the end of init(filename:):

targetScore = levelData.targetScore
maximumMoves = levelData.moves

Copy the values retrieved from the JSON into the level itself.

Back in GameViewController.swift, add the following method:

func updateLabels() {
  targetLabel.text = String(format: "%ld", level.targetScore)
  movesLabel.text = String(format: "%ld", movesLeft)
  scoreLabel.text = String(format: "%ld", score)
}

You’ll call this method after every turn to update the text inside the labels.

Add the following lines to the top of beginGame(), before the call to shuffle():

movesLeft = level.maximumMoves
score = 0
updateLabels()

This resets everything to the starting values. Build and run, and your display should look like this:

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.