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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Make a Game Like Candy Crush With SpriteKit and Swift: Part 2
30 mins
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!
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! :]