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

Animating the Swaps

To describe the swapping of two cookies, you'll create a new type: Swap. This is another model object whose only purpose it is to say, “The player wants to swap cookie A with cookie B.”

Create a new Swift File named Swap.swift. Replace its contents with the following:

struct Swap: CustomStringConvertible {
  let cookieA: Cookie
  let cookieB: Cookie
  
  init(cookieA: Cookie, cookieB: Cookie) {
    self.cookieA = cookieA
    self.cookieB = cookieB
  }
  
  var description: String {
    return "swap \(cookieA) with \(cookieB)"
  }
}

Now that you have an object that can describe an attempted swap, the question becomes: Who will handle the logic of actually performing the swap? The swipe detection logic happens in GameScene, but all the real game logic so far is in GameViewController.

That means GameScene must have a way to communicate back to GameViewController that the player performed a valid swipe and that a swap must be attempted. One way to communicate is through a delegate protocol, but since this is the only message that GameScene must send back to GameViewController, you’ll use a closure.

Add the following property to the top of GameScene.swift:

var swipeHandler: ((Swap) -> Void)?

The type of this variable is ((Swap) -> Void)?. Because of the -> you can tell this is a closure or function. This closure or function takes a Swap object as its parameter and does not return anything.

It’s the scene’s job to handle touches. If it recognizes that the user made a swipe, it will call the closure that's stored in the swipe handler. This is how it communicates back to the GameViewController that a swap needs to take place.

Still in GameScene.swift, add the following code to the bottom of trySwap(horizontal:vertical:), replacing the print() statement:

if let handler = swipeHandler {
  let swap = Swap(cookieA: fromCookie, cookieB: toCookie)
  handler(swap)
}

This creates a new Swap object, fills in the two cookies to be swapped and then calls the swipe handler to take care of the rest. Because swipeHandler can be nil, you use optional binding to get a valid reference first.

GameViewController will decide whether the swap is valid; if it is, you'll need to animate the two cookies. Add the following method to do this in GameScene.swift:

func animate(_ swap: Swap, completion: @escaping () -> Void) {
  let spriteA = swap.cookieA.sprite!
  let spriteB = swap.cookieB.sprite!

  spriteA.zPosition = 100
  spriteB.zPosition = 90

  let duration: TimeInterval = 0.3

  let moveA = SKAction.move(to: spriteB.position, duration: duration)
  moveA.timingMode = .easeOut
  spriteA.run(moveA, completion: completion)

  let moveB = SKAction.move(to: spriteA.position, duration: duration)
  moveB.timingMode = .easeOut
  spriteB.run(moveB)

  run(swapSound)
}

This is basic SKAction animation code: You move cookie A to the position of cookie B and vice versa.

The cookie that was the origin of the swipe is in cookieA and the animation looks best if that one appears on top, so this method adjusts the relative zPosition of the two cookie sprites to make that happen.

After the animation completes, the action on cookieA calls a completion block so the caller can continue doing whatever it needs to do. That’s a common pattern for this game: The game waits until an animation is complete and then it resumes.

Now that you've handled the view, there's still the model to deal with before getting to the controller! Open Level.swift and add the following method:

func performSwap(_ swap: Swap) {
  let columnA = swap.cookieA.column
  let rowA = swap.cookieA.row
  let columnB = swap.cookieB.column
  let rowB = swap.cookieB.row

  cookies[columnA, rowA] = swap.cookieB
  swap.cookieB.column = columnA
  swap.cookieB.row = rowA

  cookies[columnB, rowB] = swap.cookieA
  swap.cookieA.column = columnB
  swap.cookieA.row = rowB
}

This first makes temporary copies of the row and column numbers from the Cookie objects because they get overwritten. To make the swap, it updates the cookies array, as well as the column and row properties of the Cookie objects, which shouldn’t go out of sync. That’s it for the data model.

Go to GameViewController.swift and add the following method:

func handleSwipe(_ swap: Swap) {
  view.isUserInteractionEnabled = false

  level.performSwap(swap)
  scene.animate(swap) {
    self.view.isUserInteractionEnabled = true
  }
}

You first tell the level to perform the swap, which updates the data model and then, tell the scene to animate the swap, which updates the view. Over the course of this tutorial, you’ll add the rest of the gameplay logic to this function.

While the animation is happening, you don’t want the player to be able to touch anything else, so you temporarily turn off isUserInteractionEnabled on the view. You turn it back on in the completion block that is passed to animate(_:completion:).

Also add the following line to viewDidLoad(), just before the line that presents the scene:

scene.swipeHandler = handleSwipe

This assigns the handleSwipe(_:) function to GameScene’s swipeHandler property. Now whenever GameScene calls swipeHandler(swap), it actually calls a function in GameViewController.

Build and run the app. You can now swap the cookies! Also, try to make a swap across a gap — it won’t work!

Swap cookies

Highlighting the Cookies

In Candy Crush Saga, the candy you swipe lights up for a brief moment. You can achieve this effect in Cookie Crunch Adventure by placing a highlight image on top of the sprite.

The texture atlas has highlighted versions of the cookie sprites that are brighter and more saturated. The CookieType enum already has a function to return the name of this image.

Time to improve GameScene by adding this highlighted cookie on top of the existing cookie sprite. Adding it as a new sprite, as opposed to replacing the existing sprite’s texture, makes it easier to crossfade back to the original image.

In GameScene.swift, add a new private property to the class:

private var selectionSprite = SKSpriteNode()

Add the following method:

func showSelectionIndicator(of cookie: Cookie) {
  if selectionSprite.parent != nil {
    selectionSprite.removeFromParent()
  }

  if let sprite = cookie.sprite {
    let texture = SKTexture(imageNamed: cookie.cookieType.highlightedSpriteName)
    selectionSprite.size = CGSize(width: tileWidth, height: tileHeight)
    selectionSprite.run(SKAction.setTexture(texture))

    sprite.addChild(selectionSprite)
    selectionSprite.alpha = 1.0
  }
}

This gets the name of the highlighted sprite image from the Cookie object and puts the corresponding texture on the selection sprite. Simply setting the texture on the sprite doesn't give it the correct size but using an SKAction does.

You also make the selection sprite visible by setting its alpha to 1. You add the selection sprite as a child of the cookie sprite so that it moves along with the cookie sprite in the swap animation.

Add the opposite method, hideSelectionIndicator():

func hideSelectionIndicator() {
  selectionSprite.run(SKAction.sequence([
    SKAction.fadeOut(withDuration: 0.3),
    SKAction.removeFromParent()]))
}

This method removes the selection sprite by fading it out.

What remains, is for you to call these methods. First, in touchesBegan(_:with:), in the if let cookie = ... section — Xcode is helpfully pointing it out with a warning — add:

showSelectionIndicator(of: cookie)

And in touchesMoved(_:with:), after the call to trySwap(horizontalDelta:verticalDelta:), add:

hideSelectionIndicator()

There is one last place to call hideSelectionIndicator(). If the user just taps on the screen rather than swipes, you want to fade out the highlighted sprite, too. Add these lines to the top of touchesEnded():

if selectionSprite.parent != nil && swipeFromColumn != nil {
  hideSelectionIndicator()
}

Build and run, and light up some cookies! :]

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.