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.
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 3
40 mins
- Getting Started
- Finding the Chains
- Removing Chains
- Dropping Cookies Into Empty Tiles
- Adding New Cookies
- A Cascade of Cookies
- Scoring Points
- Calculating the Score
- Animating Point Values
- Handle Combo Scenarios
- Handle Winning and Losing Scenarios
- The Look of Victory or Defeat
- Animating the Transitions
- Manual Shuffling
- Going to the Next Level
- Where to Go From Here?
Calculating the Score
The scoring rules are simple:
- A 3-cookie chain is worth 60 points.
- Each additional cookie in the chain increases the chain’s value by 60 points.
Thus, a 4-cookie chain is worth 120 points, a 5-cookie chain is worth 180 points and so on.
It’s easiest to store the score inside the Chain
object, so each chain knows how many points it’s worth.
Add the following to Chain.swift:
var score = 0
The score is model data, so it needs to be calculated by Level
. Add the following method to Level.swift:
private func calculateScores(for chains: Set<Chain>) {
// 3-chain is 60 pts, 4-chain is 120, 5-chain is 180, and so on
for chain in chains {
chain.score = 60 * (chain.length - 2)
}
}
Now call this method from removeMatches()
, just before the return statement:
calculateScores(for: horizontalChains)
calculateScores(for: verticalChains)
You need to call it twice because there are two sets of chain objects.
Now that the level object knows how to calculate the scores and stores them inside the Chain
objects, you can update the player’s score and display it onscreen.
This happens in GameViewController.swift, so open that. Inside handleMatches()
, just before the call to self.level.fillHoles()
, add the following lines:
for chain in chains {
self.score += chain.score
}
self.updateLabels()
This simply loops through the chains, adds their scores to the player’s total and then updates the labels.
Try it out. Swap a few cookies and observe your increasing score:
Animating Point Values
It would be fun to show the point value of each chain with a cool little animation. In GameScene.swift, add a new method:
func animateScore(for chain: Chain) {
// Figure out what the midpoint of the chain is.
let firstSprite = chain.firstCookie().sprite!
let lastSprite = chain.lastCookie().sprite!
let centerPosition = CGPoint(
x: (firstSprite.position.x + lastSprite.position.x)/2,
y: (firstSprite.position.y + lastSprite.position.y)/2 - 8)
// Add a label for the score that slowly floats up.
let scoreLabel = SKLabelNode(fontNamed: "GillSans-BoldItalic")
scoreLabel.fontSize = 16
scoreLabel.text = String(format: "%ld", chain.score)
scoreLabel.position = centerPosition
scoreLabel.zPosition = 300
cookiesLayer.addChild(scoreLabel)
let moveAction = SKAction.move(by: CGVector(dx: 0, dy: 3), duration: 0.7)
moveAction.timingMode = .easeOut
scoreLabel.run(SKAction.sequence([moveAction, SKAction.removeFromParent()]))
}
This creates a new SKLabelNode
with the score and places it in the center of the chain. The numbers will float up a few pixels before disappearing.
Call this new method from animateMatchedCookies(for:completion:)
, right after for chain in chains
:
animateScore(for: chain)
When using SKLabelNode
, SpriteKit needs to load the font and convert it to a texture. That only happens once, but it does create a small delay, so it’s smart to pre-load this font before the game starts in earnest.
Open GameScene.swift, and at the bottom of init(size:)
, add the following line:
let _ = SKLabelNode(fontNamed: "GillSans-BoldItalic")
Now try it out. Build and run, and score some points!
Handle Combo Scenarios
What makes games like Candy Crush Saga fun is the ability to make combos, or more than one match in a row.
Of course, you should reward the player for making a combo by giving extra points. To that effect, you’ll add a combo multiplier, where the first chain is worth its normal score, but the second chain is worth twice its score, the third chain is worth three times its score, and so on.
In Level.swift, add the following private property:
private var comboMultiplier = 0
Replace calculateScores(for:)
with:
private func calculateScores(for chains: Set<Chain>) {
// 3-chain is 60 pts, 4-chain is 120, 5-chain is 180, and so on
for chain in chains {
chain.score = 60 * (chain.length - 2) * comboMultiplier
comboMultiplier += 1
}
}
The method now multiplies the chain’s score by the combo multiplier and then increments the multiplier so it’s one higher for the next chain.
You also need a method to reset this multiplier on the next turn. Add the following method to Level.swift:
func resetComboMultiplier() {
comboMultiplier = 1
}
Open GameViewController.swift and find beginGame()
. Add this line just before the call to shuffle()
:
level.resetComboMultiplier()
Add the same line at the top of beginNextTurn()
.
And now you have combos. Try it out!
Handle Winning and Losing Scenarios
The player only has so many moves to reach the target score. Fail, then it’s game over. The logic for this isn’t difficult to add.
Create a new method in GameViewController.swift:
func decrementMoves() {
movesLeft -= 1
updateLabels()
}
This simply decrements the counter keeping track of the number of moves and updates the onscreen labels.
Call it from the bottom of beginNextTurn()
:
decrementMoves()
Build and run to see it in action. After each swap, the game clears the matches and decreases the number of remaining moves by one.
Of course, you still need to detect when the player runs out of moves (game over!) or when the target score is reached (success and eternal fame!), and respond accordingly.
The Look of Victory or Defeat
Main.storyboard contains an image view that you’ll update with a win or lose graphic. First, make sure the view doesn’t make an early appearance.
Open GameViewController.swift, and in viewDidLoad()
, before you present the scene, make sure to hide this image view:
gameOverPanel.isHidden = true
Add the property for the tap recognizer at the top of the file:
var tapGestureRecognizer: UITapGestureRecognizer!
Now add a new method to show the game over panel:
func showGameOver() {
gameOverPanel.isHidden = false
scene.isUserInteractionEnabled = false
self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(hideGameOver))
self.view.addGestureRecognizer(self.tapGestureRecognizer)
}
This un-hides the image view, disables touches on the scene to prevent the player from swiping and adds a tap gesture recognizer that will restart the game.
Add one more method:
@objc func hideGameOver() {
view.removeGestureRecognizer(tapGestureRecognizer)
tapGestureRecognizer = nil
gameOverPanel.isHidden = true
scene.isUserInteractionEnabled = true
beginGame()
}
This hides the game over panel again and restarts the game.
The logic that detects whether it’s time to show the game over panel goes into decrementMoves()
. Add the following lines to the bottom of that method:
if score >= level.targetScore {
gameOverPanel.image = UIImage(named: "LevelComplete")
showGameOver()
} else if movesLeft == 0 {
gameOverPanel.image = UIImage(named: "GameOver")
showGameOver()
}
If the current score is greater than or equal to the target score, the player has won the game! If the number of moves remaining is 0, the player has lost the game.
In either case, the method loads the proper image into the image view and calls showGameOver()
to put it on the screen.
Try it out. When you beat the game, you should see this: