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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.

Level JSON

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:

  1. 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.
  2. Create a “tiles” array.
  3. Step through the rows using built-in enumerated() function, which is useful because it also returns the current row number.
  4. 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.
  5. 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:

Who are you calling a square?

level 1 rendered on screen

Who are you calling a square?

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.

  1. Wherever there’s a tile, the method now draws the special MaskTile sprite into the layer functioning as the SKCropNode’s mask.
  2. 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!

Looks good enough to eat!

Looks good enough to eat!

Kevin Colligan

Contributors

Kevin Colligan

Author

Alex Curran

Tech Editor

Jean-Pierre Distler

Final Pass Editor

Richard Critz

Team Lead

Over 300 content creators. Join our team.