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 2 of 4 of this article. Click here to view the first page.

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 a Set and the objects that you put into a set must conform to Hashable. 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:

  1. 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.
  2. Then it picks a random cookie type using the method you added earlier.
  3. Next, it creates a new Cookie and adds it to the 2D array.
  4. Finally, it adds the new Cookie to a Set. 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 Cookies 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 SKNodes 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:

finally, some cookies!

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.