How to Make a Game Like Candy Crush With SpriteKit and Swift: Part 1
In the first part of this tutorial on how to make a Candy Crush-like mobile game using Swift and SpriteKit, you’ll learn how to start building your game including creating your storyboard, adding your cookies, and more. 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 1
30 mins
Other items of note:
-
LevelData.swift uses Swift 4’s new
Decodable
API to make parsing the JSON files a snap. You’ll use it to create levels. See this JSON Parsing screencast for more. - Array2D.swift is a helper file which makes it easier to create two-dimensional arrays.
- Tile.swift is empty now. But contains some hints for adding jelly.
That does it for the starter project tour!
Add Your Cookies
Enough with the pre-heating, let’s start baking! Your next steps are to:
- Create the
Cookie
class. - Create the
Level
class. - Load levels from JSON files.
- Serve up your cookies atop background tiles — mom taught you to always use a plate, after all!
The Cookie Class
This game’s playing field consists of a grid, 9 columns by 9 rows. Each square of this grid can contain a cookie.
Column 0, row 0 is in the bottom-left corner of the grid. Since the point (0,0) is also at the bottom-left of the screen in SpriteKit’s coordinate system, it makes sense to have everything else “upside down” — at least compared to the rest of UIKit. :]
To begin implementing this, you need to create the class representing a cookie object. Go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Cookie.swift and click Create.
Replace the contents of Cookie.swift with the following:
import SpriteKit
// MARK: - CookieType
enum CookieType: Int {
case unknown = 0, croissant, cupcake, danish, donut, macaroon, sugarCookie
}
// MARK: - Cookie
class Cookie: CustomStringConvertible, Hashable {
var hashValue: Int {
return row * 10 + column
}
static func ==(lhs: Cookie, rhs: Cookie) -> Bool {
return lhs.column == rhs.column && lhs.row == rhs.row
}
var description: String {
return "type:\(cookieType) square:(\(column),\(row))"
}
var column: Int
var row: Int
let cookieType: CookieType
var sprite: SKSpriteNode?
init(column: Int, row: Int, cookieType: CookieType) {
self.column = column
self.row = row
self.cookieType = cookieType
}
}
You use two protocols that will pay dividends later:
-
CustomStringConvertible
: This will make your print statements a lot easier to read. -
Hashable
: Cookies will later be used in aSet
and the objects that you put into a set must conform toHashable
. That’s a requirement from Swift.
The column and row properties let Cookie
keep track of its position in the 2D grid.
The sprite
property is optional, hence the question mark after SKSpriteNode
, because the cookie object may not always have its sprite set.
The cookieType
property describes the — wait for it — type of the cookie, which takes a value from the CookieType
enum. The type is really just a number from 1 to 6, but wrapping it in an enum allows you to work with easy-to-remember names instead of numbers.
You will deliberately not use cookie type Unknown (value 0). This value has a special meaning, as you’ll learn later on.
Each cookie type number corresponds to a sprite image:
In Swift, an enum isn’t useful only for associating symbolic names with numbers; you can also add functions and computed properties to an enum. Add the following code inside the enum CookieType
:
var spriteName: String {
let spriteNames = [
"Croissant",
"Cupcake",
"Danish",
"Donut",
"Macaroon",
"SugarCookie"]
return spriteNames[rawValue - 1]
}
var highlightedSpriteName: String {
return spriteName + "-Highlighted"
}
The spriteName
property returns the filename of the corresponding sprite image in the texture atlas. In addition to the regular cookie sprite, there is also a highlighted version that appears when the player taps on the cookie.
The spriteName
and highlightedSpriteName
properties simply look up the name for the cookie sprite in an array of strings. To find the index, you use rawValue
to convert the enum’s current value to an integer. Recall that the first useful cookie type, croissant, starts at 1 but arrays are indexed starting at 0, so you need to subtract 1 to find the correct array index.
Every time a new cookie gets added to the game, it will get a random cookie type. It makes sense to add that as a function on CookieType
. Add the following to the enum as well:
static func random() -> CookieType {
return CookieType(rawValue: Int(arc4random_uniform(6)) + 1)!
}
This calls arc4random_uniform(_:)
to generate a random number between 0 and 5, then adds 1 to make it a number between 1 and 6. Because Swift is very strict, the result from arc4random_uniform(_:)
— UInt32
— must first be converted to Int
, then you can convert this number into a proper CookieType
value.
Now, you may wonder why you’re not making Cookie
a subclass of SKSpriteNode
. After all, the cookie is something you want to display on the screen. If you’re familiar with the MVC pattern, think of Cookie
as a model object that simply describes the data for the cookie. The view is a separate object, stored in the sprite property.
This kind of separation between data models and views is something you’ll use consistently throughout this tutorial. The MVC pattern is more common in regular apps than in games but, as you’ll see, it can help keep the code clean and flexible.
The Level Class
Now let start building levels. Go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Level.swift and click Create.
Replace the contents of Level.swift with the following:
import Foundation
let numColumns = 9
let numRows = 9
class Level {
private var cookies = Array2D<Cookie>(columns: numColumns, rows: numRows)
}
This declares two constants for the dimensions of the level, numColumns
and numRows
, so you don’t have to hardcode the number 9 everywhere.
The property cookies is the two-dimensional array that holds the Cookie
objects, 81 in total, 9 rows of 9 columns.
The cookies
array is private
, so Level
needs to provide a way for others to obtain a cookie object at a specific position in the level grid.
Add the code for this method to end of Level
:
func cookie(atColumn column: Int, row: Int) -> Cookie? {
precondition(column >= 0 && column < numColumns)
precondition(row >= 0 && row < numRows)
return cookies[column, row]
}
Using cookie(atColumn: 3, row: 6)
you can ask the Level
for the cookie at column 3, row 6. Behind the scenes this asks the Array2D
for the cookie and then returns it. Note that the return type is Cookie?
, an optional, because not all grid squares will necessarily have a cookie.
Notice the use of precondition
to verify that the specified column and row numbers are within the valid range of 0-8.
Now to fill up that cookies array with some cookies! Later on you will learn how to read level designs from a JSON file but for now, you’ll fill up the array yourself, just so there is something to show on the screen.
Add the following two methods to the end of Level
:
func shuffle() -> Set<Cookie> {
return createInitialCookies()
}
private func createInitialCookies() -> Set<Cookie> {
var set: Set<Cookie> = []
// 1
for row in 0..<numRows {
for column in 0..<numColumns {
// 2
let cookieType = CookieType.random()
// 3
let cookie = Cookie(column: column, row: row, cookieType: cookieType)
cookies[column, row] = cookie
// 4
set.insert(cookie)
}
}
return set
}
Both methods return a Set
. A Set
is a collection, like an array, but it allows each element to appear only once, and it doesn't store the elements in any particular order.
shuffle()
fills up the level with random cookies. Right now it just calls createInitialCookies()
, where the real work happens. Here's what it does, step by step:
- The method loops through the rows and columns of the 2D array. This is something you’ll see a lot in this tutorial. Remember that column 0, row 0 is in the bottom-left corner of the 2D grid.
- Then it picks a random cookie type using the method you added earlier.
- Next, it creates a new
Cookie
and adds it to the 2D array. - Finally, it adds the new
Cookie
to aSet
.shuffle()
returns this set of cookies to its caller.
One of the main difficulties when designing your code is deciding how the different objects will communicate with each other. In this game, you often accomplish this by passing around a collection of objects, usually a Set
or Array
.
In this case, after you create a new Level
and call shuffle()
to fill it with cookies, the Level
replies, “Here is a set with all the new Cookie
s I just added.” You can take that set and, for example, create new sprites for all the cookies it contains. In fact, that’s exactly what you’ll do in the next section.
Build the app and make sure you're not getting any compilation errors.
Open GameScene.swift and add the following properties to the class:
var level: Level!
let tileWidth: CGFloat = 32.0
let tileHeight: CGFloat = 36.0
let gameLayer = SKNode()
let cookiesLayer = SKNode()
The scene has a public property to hold a reference to the current level.
Each square of the 2D grid measures 32 by 36 points, so you put those values into the tileWidth
and tileHeight
constants. These constants will make it easier to calculate the position of a cookie sprite.
To keep the SpriteKit node hierarchy neatly organized, GameScene
uses several layers. The base layer is called gameLayer
. This is the container for all the other layers and it’s centered on the screen. You’ll add the cookie sprites to cookiesLayer
, which is a child of gameLayer
.
Add the following lines to init(size:)
to add the new layers. Put this after the code that creates the background node:
addChild(gameLayer)
let layerPosition = CGPoint(
x: -tileWidth * CGFloat(numColumns) / 2,
y: -tileHeight * CGFloat(numRows) / 2)
cookiesLayer.position = layerPosition
gameLayer.addChild(cookiesLayer)
This adds two empty SKNode
s to the screen to act as layers. You can think of these as transparent planes you can add other nodes in.
Remember that earlier you set the anchorPoint
of the scene to (0.5, 0.5)? This means that when you add children to the scene their starting point (0, 0) will automatically be in the center of the scene.
However, because column 0, row 0 is in the bottom-left corner of the 2D grid, you want the positions of the sprites to be relative to the cookiesLayer’s
bottom-left corner, as well. That’s why you move the layer down and to the left by half the height and width of the grid.
Adding the sprites to the scene happens in addSprites(for:)
. Add it after init(size:)
:
func addSprites(for cookies: Set<Cookie>) {
for cookie in cookies {
let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName)
sprite.size = CGSize(width: tileWidth, height: tileHeight)
sprite.position = pointFor(column: cookie.column, row: cookie.row)
cookiesLayer.addChild(sprite)
cookie.sprite = sprite
}
}
private func pointFor(column: Int, row: Int) -> CGPoint {
return CGPoint(
x: CGFloat(column) * tileWidth + tileWidth / 2,
y: CGFloat(row) * tileHeight + tileHeight / 2)
}
addSprites(for:)
iterates through the set of cookies and adds a corresponding SKSpriteNode
instance to the cookie layer. This uses a helper method, pointFor(column:, row:)
, that converts a column and row number into a CGPoint
that is relative to the cookiesLayer
. This point represents the center of the cookie’s SKSpriteNode
.
Open GameViewController.swift and add a new property to the class:
var level: Level!
Next, add these two new methods:
func beginGame() {
shuffle()
}
func shuffle() {
let newCookies = level.shuffle()
scene.addSprites(for: newCookies)
}
beginGame()
kicks off the game by calling shuffle()
. This is where you call Level
's shuffle()
method, which returns the Set
containing new Cookie
objects. Remember that these cookie objects are just model data; they don’t have any sprites yet. To show them on the screen, you tell GameScene
to add sprites for those cookies.
The only missing piece is creating the actual Level
instance. Add the following lines in viewDidLoad()
, just before the code that presents the scene:
level = Level()
scene.level = level
After creating the new Level
instance, you set the level property on the scene to tie together the model and the view.
Finally, make sure you call beginGame()
at the end of viewDidLoad()
to set things in motion:
beginGame()
Build and run, and you should finally see some cookies: