Update note: This SpriteKit tutorial has been updated for Xcode 7.3 and Swift 2.2 by Morten Faarkrog. The original tutorial was written by Matthijs Hollemans.
Welcome back to the fourth and final part of the “How to Make a Game Like Candy Crush” tutorial with SpriteKit and Swift series!
- In the first part, you put some of the foundation in place. You setup the gameplay view, the sprites, and the logic for loading levels.
- In the second part you continued expanding on the foundation of the game. You focused on detecting swipes and swapping cookies, as well as creating some nice visual effects for your game.
- In the third part, you’ll work on finding and removing chains and refilling the level with new yummy cookies after successful swipes.
- (You’re here) Finally, in the fourth part, you’ll complete the gameplay by adding support for scoring points, winning and losing, shuffling the cookies, and more.
This Swift tutorial picks up where you left off in the last part. If you don’t have it already, here is the project with all of the source code up to this point. You also need a copy of the resources zip.
Time for you to finish this yummy game of yours :]
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. The game should show these numbers on the screen so that the player can keep track of them.
First, add the following properties to GameViewController.swift
:
var movesLeft = 0 var score = 0 @IBOutlet weak var targetLabel: UILabel! @IBOutlet weak var movesLabel: UILabel! @IBOutlet weak var scoreLabel: UILabel! |
The movesLeft
and score
variables keep track of how well the player is doing (model data), while the outlets show this on the screen (views).
Open Main.storyboard to add these labels to the view. Design the view controller to look like this:
Make sure to set the outer Stack View‘s Distribution to be equal to Fill Equally in the Attributes Inspector. To permanently pin the outer Stack View to the top of the screen, and to have it fit all screen sizes, give it the following 3 layout constraints:
To make the labels easier to see, give the main view a gray background color. Make the font for the labels Gill Sans Bold, size 20.0 for the number labels and 14.0 for the text labels. You may also wish to set a slight drop shadow for the labels so they are easier to see.
It looks best if you set center alignment on the number labels. Connect the three number labels to their respective outlets.
Because the target score and the 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 |
In Level.swift
, add these two lines to the bottom of init(filename:)
:
init(filename: String) { ... targetScore = dictionary["targetScore"] as! Int maximumMoves = dictionary["moves"] as! Int } |
By this point, you’ve parsed the JSON into a dictionary
, so you grab the two values and store them.
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:
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(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(horizontalChains) calculateScores(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
. 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 animateScoreForChain(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.moveBy(CGVector(dx: 0, dy: 3), duration: 0.7) moveAction.timingMode = .EaseOut scoreLabel.runAction(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()
, in between the two for
loops:
for chain in chains { // Add this line: animateScoreForChain(chain) for cookie in chain.cookies { |
When using SKLabelNode
, Sprite Kit 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.
At the bottom of GameScene
‘s init(size:)
, add the following line:
let _ = SKLabelNode(fontNamed: "GillSans-BoldItalic") |
Now try it out. Build and run, and score some points!
Handle Combo Scenrios
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 |
Update calculateScores()
to:
private func calculateScores(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!
Challenge: How would you detect an L-shaped chain and make it count double the value for a row?
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.
First, though, the storyboard needs some work.
The Look of Victory or Defeat
Open Main.storyboard and drag an UIImageView into the view. In the Attributes Inspector give the view a mode of Aspect Fit. Next, give it the following 3 layout constraints (make sure to uncheck “Constrain to margins”) and center it vertically in the view.
This image view will show either the “Game Over!” or “Level Complete!” message.
Now connect this image view to a new outlet on GameViewController.swift
named gameOverPanel
.
@IBOutlet weak var gameOverPanel: UIImageView! |
Also, add a property for a gesture recognizer:
var tapGestureRecognizer: UITapGestureRecognizer! |
In viewDidLoad()
, before you present the scene, make sure to hide this image view:
gameOverPanel.hidden = true |
Now add a new method to show the game over panel:
func showGameOver() { gameOverPanel.hidden = false scene.userInteractionEnabled = false self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.hideGameOver)) view.addGestureRecognizer(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:
func hideGameOver() { view.removeGestureRecognizer(tapGestureRecognizer) tapGestureRecognizer = nil gameOverPanel.hidden = true scene.userInteractionEnabled = 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:
Likewise, when you run out of moves, you should see a “Game Over” message.
Animating the Transitions
It looks a bit messy with this banner on top of all those cookies, so add some animation here. Add these two methods to GameScene.swift
:
func animateGameOver(completion: () -> ()) { let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3) action.timingMode = .EaseIn gameLayer.runAction(action, completion: completion) } func animateBeginGame(completion: () -> ()) { gameLayer.hidden = false gameLayer.position = CGPoint(x: 0, y: size.height) let action = SKAction.moveBy(CGVector(dx: 0, dy: -size.height), duration: 0.3) action.timingMode = .EaseOut gameLayer.runAction(action, completion: completion) } |
animateGameOver()
animates the entire gameLayer
out of the way. animateBeginGame()
does the opposite and slides the gameLayer
back in from the top of the screen.
The very first time the game starts, you also want to call animateBeginGame()
to perform this same animation. It looks better if the game layer is hidden before that animation begins, so add the following line to GameScene.swift
in init(size:)
, immediately after you create the gameLayer
node:
gameLayer.hidden = true |
Now open GameViewController.swift
and call animateGameOver()
in showGameOver()
:
func showGameOver() { gameOverPanel.hidden = false scene.userInteractionEnabled = false scene.animateGameOver() { self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.hideGameOver)) self.view.addGestureRecognizer(self.tapGestureRecognizer) } } |
Note that the tap gesture recognizer is now added after the animation is complete. This prevents the player from tapping while the game is still performing the animation.
Finally, in GameViewController.swift
’s beginGame()
, just before the call to shuffle()
, call animateBeginGame()
:
scene.animateBeginGame() { } |
The completion block for this animation is currently empty, but you’ll put something there soon.
Now when you tap after game over, the cookies should drop down the screen to their starting positions. Sweet!
Whoops! Something’s not right. It appears you didn’t properly remove the old cookie sprites.
Add this new method to GameScene.swift
to perform the cleanup:
func removeAllCookieSprites() { cookiesLayer.removeAllChildren() } |
And call it as the very first thing from shuffle()
inside GameViewController.swift
:
scene.removeAllCookieSprites() |
That solves that! Build and run and your game should reset cleanly.
Manual Shuffling
There’s one more situation to manage: It may happen—though only rarely—that there is no way to swap any of the cookies to make a chain. In that case, the player is stuck.
There are different ways to handle this. For example, Candy Crush Saga automatically reshuffles the cookies. But in Cookie Crunch, you’ll give that power to the player. You will allow the player to shuffle at any time by tapping a button, but it will cost the player a move.
Add an outlet property in GameViewController.swift
:
@IBOutlet weak var shuffleButton: UIButton! |
And add an action method:
@IBAction func shuffleButtonPressed(AnyObject) { shuffle() decrementMoves() } |
Tapping the shuffle button costs a move, so this also calls decrementMoves()
.
In showGameOver()
, add the following line to hide the shuffle button:
shuffleButton.hidden = true |
Also do the same thing in viewDidLoad()
, so the button is hidden when the game first starts.
In beginGame()
, in the animation’s completion block, put the button back on the screen again:
scene.animateBeginGame() { self.shuffleButton.hidden = false } |
Now open Main.storyboard and add a button to the bottom of the screen.
Set the title to “Shuffle” and make the button 100×36 points big. To style the button, give it the font Gill Sans Bold, 20 pt. Make the text color white with a 50% opaque black drop shadow. For the background image, choose “Button”, an image you added to the asset catalog in Part One.
To pin the button to the bottom of the screen, center it horizontally, and add the following 3 layout constraints:
Finally, connect the shuffleButton
outlet to the button and its Touch Up Inside event to the shuffleButtonPressed:
action.
Try it out!
Note: When shuffling a deck of cards, you take the existing cards, change their order and deal out the same cards again in a different order. In this game, however, you simply get all new—random!—cookies. Finding a distribution of the same set of cookies that allows for at least one swap is an extremely difficult computational problem, and after all, this is only a casual game.
The shuffle is a bit abrupt, rather make the new cookies appear with a cute animation. In GameScene.swift
, go to addSpritesForCookies()
and add the following lines inside the for
loop, after the existing code:
// Give each cookie sprite a small, random delay. Then fade them in. sprite.alpha = 0 sprite.xScale = 0.5 sprite.yScale = 0.5 sprite.runAction( SKAction.sequence([ SKAction.waitForDuration(0.25, withRange: 0.5), SKAction.group([ SKAction.fadeInWithDuration(0.25), SKAction.scaleTo(1.0, duration: 0.25) ]) ])) |
This gives each cookie sprite a small, random delay and then fades them into view. It looks like this:
Adding Music
Give the player some smooth, relaxing music to listen to while crunching cookies. Add this line to the top of GameViewController.swift
to include the AVFoundation framework:
import AVFoundation |
Also add the following property:
lazy var backgroundMusic: AVAudioPlayer? = { guard let url = NSBundle.mainBundle().URLForResource("Mining by Moonlight", withExtension: "mp3") else { return nil } do { let player = try AVAudioPlayer(contentsOfURL: url) player.numberOfLoops = -1 return player } catch { return nil } }() |
What you see here is a common pattern for declaring a variable and initializing it in the same statement. The initialization code sits in a closure. It loads the background music MP3 and sets it to loop forever. Because the variable is marked lazy
, the code from the closure won’t run until backgroundMusic
is first accessed.
Finally, add this line to viewDidLoad()
, just before the call to beginGame()
:
backgroundMusic?.play() |
It gives the game a whole lot more swing!
Drawing Better Tiles
If you compare your game closely to Candy Crush Saga, you’ll notice that the tiles are drawn slightly differently. The borders in Candy Crush look much nicer:
Also, if a cookie drops across a gap, your game draws it on top of the background, but candies in Candy Crush appear to fall behind the background:
Recreating this effect isn’t too difficult but it requires a number of new sprites. You can find these in the tutorial’s Resources in the Grid.atlas folder. Drag this folder into your Xcode project. This creates a second texture atlas with just these images.
In GameScene.swift
, add two new properties:
let cropLayer = SKCropNode() let maskLayer = SKNode() |
In init(size:)
, add these lines below the code that creates the tilesLayer
:
gameLayer.addChild(cropLayer) maskLayer.position = layerPosition cropLayer.maskNode = maskLayer |
This makes two new layers: cropLayer
, which is a special kind of node called an SKCropNode
, and a mask layer. A crop node only draws its children where the mask contains pixels. This lets you draw the cookies only where there is a tile, but never on the background.
Replace this line:
gameLayer.addChild(cookiesLayer) |
With this:
cropLayer.addChild(cookiesLayer) |
Now, instead of adding the cookiesLayer
directly to the gameLayer
, you add it to this new cropLayer
.
To fill in the mask of this crop layer, make two changes to addTiles()
:
- Replace
"Tile"
with"MaskTile"
- Replace
tilesLayer
withmaskLayer
Wherever there’s a tile, the method now draws the special MaskTile sprite into the layer functioning as the SKCropNode
’s mask. The MaskTile is slightly larger than the regular tile.
Build and run. Notice how the cookies get cropped when they fall through a gap:
init(size:)
cropLayer.addChild(maskLayer) |
Don’t forget to remove it again when you’re done!
For the final step, add the following code to the bottom of addTiles()
:
for row in 0...NumRows { for column in 0...NumColumns { let topLeft = (column > 0) && (row < NumRows) && level.tileAtColumn(column - 1, row: row) != nil let bottomLeft = (column > 0) && (row > 0) && level.tileAtColumn(column - 1, row: row - 1) != nil let topRight = (column < NumColumns) && (row < NumRows) && level.tileAtColumn(column, row: row) != nil let bottomRight = (column < NumColumns) && (row > 0) && level.tileAtColumn(column, row: row - 1) != nil // The tiles are named from 0 to 15, according to the bitmask that is // made by combining these four values. let value = Int(topLeft) | Int(topRight) << 1 | Int(bottomLeft) << 2 | Int(bottomRight) << 3 // Values 0 (no tiles), 6 and 9 (two opposite tiles) are not drawn. if value != 0 && value != 6 && value != 9 { let name = String(format: "Tile_%ld", value) let tileNode = SKSpriteNode(imageNamed: name) tileNode.size = CGSize(width: TileWidth, height: TileHeight) var point = pointForColumn(column, row: row) point.x -= TileWidth/2 point.y -= TileHeight/2 tileNode.position = point tilesLayer.addChild(tileNode) } } } |
This draws a pattern of border pieces in between the level tiles. As a challenge, try to decipher for yourself how this method works. :]
Build and run, and you should now have a game that looks and acts just like Candy Crush Saga!
Going to the Next Level
You’re almost done, but wouldn’t it be cool if your game automatically switched to the next level upon completing the current one? Luckily, this is surprisingly easy to do.
First, in Level.swift
add the following global constant for keeping track of the number of levels right below NumRows:
let NumLevels = 4 // Excluding level 0 |
Next, in GameViewController.swift
add the following property for keeping track of the level the user is currently playing:
var currentLevelNum = 1 |
Now you need a way to know what level to use when loading your game scene. Still in GameViewController.swift
replace the current viewDidLoad()
method with the following:
override func viewDidLoad() { super.viewDidLoad() // Setup view with level 1 setupLevel(currentLevelNum) // Start the background music. backgroundMusic?.play() } |
And implement the setupLevel(_:)
function as follows:
func setupLevel(levelNum: Int) { let skView = view as! SKView skView.multipleTouchEnabled = false // Create and configure the scene. scene = GameScene(size: skView.bounds.size) scene.scaleMode = .AspectFill // Setup the level. level = Level(filename: "Level_\(levelNum)") scene.level = level scene.addTiles() scene.swipeHandler = handleSwipe gameOverPanel.hidden = true shuffleButton.hidden = true // Present the scene. skView.presentScene(scene) // Start the game. beginGame() } |
As you can see, this is almost the exact same code as you had in viewDidLoad()
before, except for the line that setup the actual level instance. Now you choose the level number dynamically :]
Next, in decrementMoves()
after the line:
gameOverPanel.image = UIImage(named: "LevelComplete") |
add the following to update the current level number.
currentLevelNum = currentLevelNum < NumLevels ? currentLevelNum+1 : 1 |
Notice that this is only called if the player actually completes the level. Rather than congratulating the player when all levels are complete, you simply go back to level 1. This way the game goes on forever!
Now there’s only one last change you need to make before having implemented this awesome level-changing feature to your game. In hideGameOver()
replace the line beginGame()
with:
setupLevel(currentLevelNum) |
That’s it! Build and run, and your game should now automatically go to the next level when a user completes the current one.
Where to Go From Here?
Congrats for making it to the end! This has been a long but “Swift” tutorial, and you are coming away with all the basic building blocks for making your own match-3 games.
You can download the final Xcode project here.
Here are ideas for other features you could add:
- Special cookies when the player matches a certain shape. For example, Candy Crush Saga gives you a cookie that can clear an entire row when you match a 4-in-a-row chain.
- Detection of special chains, such as L- or T-shapes, that reward the player with bonus points or special power-ups.
- Boosts, or power-ups the player can use any time they want. For example, one boost might remove all the cookies of one type from the screen at once.
- Jelly levels: On these levels, some tiles are covered in jelly. You have X moves to remove all the jelly. This is where the
Tile
class comes in handy. You can give it aBool
jelly property and if the player matches a cookie on this tile, set the jelly property to false to remove the jelly. - Hints: If the player doesn’t make a move for two seconds, light up a pair of cookies that make a valid swap.
- Shuffle the cookies automatically if there are no possible moves.
As you can see, there’s still plenty to play with. Have fun! :]
Credits: Free game art from Game Art Guppy. The music is by Kevin MacLeod. The sound effects are based on samples from freesound.org.
Some of the techniques used in this source code are based on a blog post by Emanuele Feronato.
Note: If you want to learn more about Sprite Kit, you should check out our book 2D iOS & tvOS Games by Tutorials.
In this book we’ll teach you everything you need to know to make great games for iOS & tvOS – from physics, to tile maps, to particle systems, and even how to make your games “juicy” with polish and special effects.
The post How to Make a Game Like Candy Crush with SpriteKit and Swift: Part 4 appeared first on Ray Wenderlich.