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 2 of 5 of this article. Click here to view the first page.

Removing Chains

Now you’re going to remove those cookies with a nice animation.

First, you need to update the data model by removing the Cookie objects from the array for the 2D grid. When that’s done, you can tell GameScene to animate the sprites for these cookies out of existence.

Removing the cookies from the model is simple enough. Add the following method to Level.swift:

private func removeCookies(in chains: Set<Chain>) {
  for chain in chains {
    for cookie in chain.cookies {
      cookies[cookie.column, cookie.row] = nil
    }
  }
}

Each chain has a list of cookies and each cookie knows its position in the grid, so you simply set that element to nil to remove the cookie from the data model.

In removeMatches(), replace the print() statements with the following:

removeCookies(in: horizontalChains)
removeCookies(in: verticalChains)

That takes care of the data model. Now switch to GameScene.swift and add the following method:

func animateMatchedCookies(for chains: Set<Chain>, completion: @escaping () -> Void) {
  for chain in chains {
    for cookie in chain.cookies {
      if let sprite = cookie.sprite {
        if sprite.action(forKey: "removing") == nil {
          let scaleAction = SKAction.scale(to: 0.1, duration: 0.3)
          scaleAction.timingMode = .easeOut
          sprite.run(SKAction.sequence([scaleAction, SKAction.removeFromParent()]),
                     withKey: "removing")
        }
      }
    }
  }
  run(matchSound)
  run(SKAction.wait(forDuration: 0.3), completion: completion)
}

This loops through all the chains and all the cookies in each chain, and then triggers the animations.

Because the same Cookie could be part of two chains, you need to ensure you add only one animation to the sprite, not two. That's why the action is added using the key "removing". If such an action already exists, you won't add a new animation to the sprite.

When the shrinking animation is done, the sprite is removed from the cookie layer. The wait(forDuration:) action at the end ensures that the rest of the game will only continue after the animations finish.

Open GameViewController.swift and replace handleMatches() with the following to call this new animation:

func handleMatches() {
  let chains = level.removeMatches()

  scene.animateMatchedCookies(for: chains) {
    self.view.isUserInteractionEnabled = true
  }
}

Try it out. Build and run, and make some matches.

Dropping Cookies Into Empty Tiles

Removing cookie chains leaves holes in the grid. Other cookies should now fall down to fill up those holes. You’ll tackle this in two steps:

  1. Update the model.
  2. Animate the sprites.

Add this new method to Level.swift:

func fillHoles() -> [[Cookie]] {
    var columns: [[Cookie]] = []
    // 1
    for column in 0..<numColumns {
      var array: [Cookie] = []
      for row in 0..<numRows {
        // 2
        if tiles[column, row] != nil && cookies[column, row] == nil {
          // 3
          for lookup in (row + 1)..<numRows {
            if let cookie = cookies[column, lookup] {
              // 4
              cookies[column, lookup] = nil
              cookies[column, row] = cookie
              cookie.row = row
              // 5
              array.append(cookie)
              // 6
              break
            }
          }
        }
      }
      // 7
      if !array.isEmpty {
        columns.append(array)
      }
    }
    return columns
}

This method detects where there are empty tiles and shifts any cookies down to fill up those tiles. It starts at the bottom and scans upward. If it finds a square that should have a cookie but doesn’t, it then finds the nearest cookie above it and moves that cookie to the empty tile.

Here is how it all works:

  1. You loop through the rows, from bottom to top.
  2. If there’s a tile at a position but no cookie, then there’s a hole. Remember that the tiles array describes the shape of the level.
  3. You scan upward to find the cookie that sits directly above the hole. Note that the hole may be bigger than one square — for example, if this was a vertical chain — and that there may be holes in the grid shape, as well.
  4. If you find another cookie, move that cookie to the hole.
  5. You add the cookie to the array. Each column gets its own array and cookies that are lower on the screen are first in the array. It’s important to keep this order intact, so the animation code can apply the correct delay. The farther up the piece is, the bigger the delay before the animation starts.
  6. Once you’ve found a cookie, you don't need to scan up any farther so you break out of the inner loop.
  7. If a column does not have any holes, then there's no point in adding it to the final array.

At the end, the method returns an array containing all the cookies that have been moved down, organized by column.

You’ve already updated the data model for these cookies with the new positions, but the sprites need to catch up. GameScene will animate the sprites and GameViewController is the in-between object to coordinate between the the model (Level) and the view (GameScene).

Switch to GameScene.swift and add a new animation method:

func animateFallingCookies(in columns: [[Cookie]], completion: @escaping () -> Void) {
  // 1
  var longestDuration: TimeInterval = 0
  for array in columns {
    for (index, cookie) in array.enumerated() {
      let newPosition = pointFor(column: cookie.column, row: cookie.row)
      // 2
      let delay = 0.05 + 0.15 * TimeInterval(index)
      // 3
      let sprite = cookie.sprite!   // sprite always exists at this point
      let duration = TimeInterval(((sprite.position.y - newPosition.y) / tileHeight) * 0.1)
      // 4
      longestDuration = max(longestDuration, duration + delay)
      // 5
      let moveAction = SKAction.move(to: newPosition, duration: duration)
      moveAction.timingMode = .easeOut
      sprite.run(
        SKAction.sequence([
          SKAction.wait(forDuration: delay),
          SKAction.group([moveAction, fallingCookieSound])]))
    }
  }

  // 6
  run(SKAction.wait(forDuration: longestDuration), completion: completion)
}

Here’s how this works:

  1. As with the other animation methods, you should only call the completion block after all the animations are finished. Because the number of falling cookies may vary, you can’t hardcode this total duration but instead have to compute it.
  2. The higher up the cookie is, the bigger the delay on the animation. That looks more dynamic than dropping all the cookies at the same time. This calculation works because fillHoles() guarantees that lower cookies are first in the array.
  3. Likewise, the duration of the animation is based on how far the cookie has to fall — 0.1 seconds per tile. You can tweak these numbers to change the feel of the animation.
  4. You calculate which animation is the longest. This is the time the game must wait before it may continue.
  5. You perform the animation, which consists of a delay, a movement and a sound effect.
  6. You wait until all the cookies have fallen down before allowing the gameplay to continue.

Now you can tie it all together. Open GameViewController.swift. Replace the existing handleMatches() with the following:

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

This now calls fillHoles() to update the model, which returns the array that describes the fallen cookies and then passes that array onto the scene so it can animate the sprites to their new positions. Build and run to try it out!

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.