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
Loading Levels from JSON Files
Not all the levels in Candy Crush Saga have grids that are a simple square shape. You will now add support for loading level designs from JSON files. The five designs you’re going to load still use the same 9x9 grid, but they leave some of the squares blank.
Look in the Levels folder in the starter project and you’ll see several files.
Click on Level_1.json to look inside. You’ll see that the contents are structured as a dictionary containing three elements: tiles, targetScore and moves.
The tiles array contains nine other arrays, one for each row of the level. If a tile has a value of 1, it can contain a cookie; a 0 means the tile is empty.
Open Level.swift and add a new property and method:
private var tiles = Array2D<Tile>(columns: numColumns, rows: numRows)
func tileAt(column: Int, row: Int) -> Tile? {
precondition(column >= 0 && column < numColumns)
precondition(row >= 0 && row < numRows)
return tiles[column, row]
}
The tiles variable describes the structure of the level. This is very similar to the cookies array, except now you make it an Array2D
of Tile
objects.
Whereas the cookies array keeps track of the Cookie
objects in the level, tiles simply describes which parts of the level grid are empty and which can contain a cookie:
Wherever tiles[a, b]
is nil, the grid is empty and cannot contain a cookie.
Now that the instance variables for level data are in place, you can start adding the code to fill in the data.
Add the new init(filename:)
initializer to Level.swift:
init(filename: String) {
// 1
guard let levelData = LevelData.loadFrom(file: filename) else { return }
// 2
let tilesArray = levelData.tiles
// 3
for (row, rowArray) in tilesArray.enumerated() {
// 4
let tileRow = numRows - row - 1
// 5
for (column, value) in rowArray.enumerated() {
if value == 1 {
tiles[column, tileRow] = Tile()
}
}
}
}
Here's what this initializer does, step-by-step:
- Load the data level from a specific JSON file. Note that this function may return nil — it returns an optional — and you use
guard
to handle this situation. - Create a “tiles” array.
- Step through the rows using built-in
enumerated()
function, which is useful because it also returns the current row number. - In SpriteKit (0, 0) is at the bottom of the screen, so you have to reverse the order of the rows here. The first row you read from the JSON corresponds to the last row of the 2D grid.
- Step through the columns in the current row. Every time you find a 1, create a
Tile
object and place it into the tiles array.
You still need to put this new tiles array to good use. Inside createInitialCookies()
, add an if-clause
inside the two for-loops
, around the code that creates the Cookie object:
// This line is new
if tiles[column, row] != nil {
let cookieType = ...
...
set.insert(cookie)
}
Now the app will only create a Cookie
object if there is a tile at that spot.
One last thing remains: In GameViewController.swift’s viewDidLoad()
, replace the line that creates the level object with:
level = Level(filename: "Level_1")
Build and run, and now you should have a non-square level shape:
Making the Tiles Visible
To make the cookie sprites stand out from the background a bit more, you can draw a slightly darker “tile” sprite behind each cookie. These new tile sprites will live on their own layer, the tilesLayer. The graphics are included in the starter project, in Grid.atlas.
In GameScene.swift, add three new properties:
let tilesLayer = SKNode()
let cropLayer = SKCropNode()
let maskLayer = SKNode()
This makes three layers: tilesLayer
, cropLayer
, which is a special kind of node called an SKCropNode
, and maskLayer
. 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.
In init(size:)
, add these lines below the code that creates the layerPosition
:
tilesLayer.position = layerPosition
maskLayer.position = layerPosition
cropLayer.maskNode = maskLayer
gameLayer.addChild(tilesLayer)
gameLayer.addChild(cropLayer)
Make sure you add the children nodes in the correct order so tiles appear behind the cropLayer
(which contains your cookies.) SpriteKit nodes with the same zPosition
are drawn in the order they were added.
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
.
Add the following method to GameScene.swift, as well:
func addTiles() {
// 1
for row in 0..<numRows {
for column in 0..<numColumns {
if level.tileAt(column: column, row: row) != nil {
let tileNode = SKSpriteNode(imageNamed: "MaskTile")
tileNode.size = CGSize(width: tileWidth, height: tileHeight)
tileNode.position = pointFor(column: column, row: row)
maskLayer.addChild(tileNode)
}
}
}
// 2
for row in 0...numRows {
for column in 0...numColumns {
let topLeft = (column > 0) && (row < numRows)
&& level.tileAt(column: column - 1, row: row) != nil
let bottomLeft = (column > 0) && (row > 0)
&& level.tileAt(column: column - 1, row: row - 1) != nil
let topRight = (column < numColumns) && (row < numRows)
&& level.tileAt(column: column, row: row) != nil
let bottomRight = (column < numColumns) && (row > 0)
&& level.tileAt(column: column, row: row - 1) != nil
var value = topLeft.hashValue
value = value | topRight.hashValue << 1
value = value | bottomLeft.hashValue << 2
value = value | bottomRight.hashValue << 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 = pointFor(column: column, row: row)
point.x -= tileWidth / 2
point.y -= tileHeight / 2
tileNode.position = point
tilesLayer.addChild(tileNode)
}
}
}
}
Here's what's going on.
The code checks which tile is required, and selects the right sprite.
- Wherever there’s a tile, the method now draws the special MaskTile sprite into the layer functioning as the
SKCropNode
’s mask. - This draws a pattern of border pieces in between the level tiles.
Imagine dividing each tile into four quadrants. The four boolean variables —topLeft
,bottomLeft
,topRight
,topLeft
— indicate which quadrants need a background. For example, a tile surrounded on all sides wouldn't need any border, just a full background to fit in seamlessly to the tiles around it. But in a square level, a tile in the lower-right corner would need a background to cover the top-left only, like so:The code checks which tile is required, and selects the right sprite.
Finally, open GameViewController.swift. Add the following line to viewDidLoad()
, immediately after you present the scene:
scene.addTiles()
Build and run and notice how nice your cookies look!