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.
A little while back, I wrote an Objective-C tutorial about how to make a game like the Candy Crush Saga, which is a very popular casual match-3 game.
But I thought it would be great to make a Swift version as well, hence this post!
In this four-part “How to” tutorial with SpriteKit and Swift series, you’ll learn how to make a game like Candy Crush named Cookie Crunch Adventure. Yum, that sounds even better than candy!
- (You’re here) In the first part, you’ll put some of the foundation in place. You’ll setup the gameplay view, the sprites, and the logic for loading levels.
- The second part will continue expanding on the foundation of the game. You’ll focus on detecting swipes and swapping cookies, and will create some nice visual effects in the process.
- In the third part, you’ll work on finding and removing chains and refilling the level with new yummy cookies after successful swipes.
- Finally, in the fourth part, you’ll complete the gameplay by adding support for scoring points, winning and losing, shuffling the cookies, and more.
In the process of going through this tutorial, you’ll get some excellent practice with Swift techniques such as enums, generics, subscripting, closures, and extensions. You’ll also learn a lot about game architecture and best practices.
There’s a lot to cover, so let’s get you started!
Note: This Swift tutorial assumes you have working knowledge of Sprite Kit and Swift. If you’re new to Sprite Kit, check out the Sprite Kit for beginners tutorial or our book, 2D iOS & tvOS Games by Tutorials. For an introduction to Swift, see our Swift tutorial.
Getting Started
Before you continue, download the resources for this Swift tutorial and unpack the zip file. You’ll have a folder containing all the images and sound effects you’ll need later on.
Start up Xcode, go to File\New\Project…, choose the iOS\Application\Game template and click Next. Fill out the options as follows:
- Product Name: CookieCrunch
- Language: Swift
- Game Technology: SpriteKit
- Devices: iPhone
Click Next, choose a folder for your project and click Create.
This is a portrait-only game, so open the Target Settings screen and in the General tab, make sure only Portrait is checked in the Device Orientation section:
To start importing the graphics files, go to the Resources folder you just downloaded and drag the Sprites.atlas folder into Xcode’s Project Navigator. Make sure Destination: Copy items if needed is checked.
You should now have a blue folder in your project:
Xcode will automatically pack the images from this folder into a texture atlas when it builds the game. Using a texture atlas as opposed to individual images will dramatically improve your game’s drawing performance.
Note: To learn more about texture atlases and performance, check out Chapter 25 in iOS Games by Tutorials, “Sprite Kit Performance: Texture Atlases”.
There are a few more images to import, but they don’t go into a texture atlas. This is because they are either large full-screen background images (which are more efficient to keep outside of the texture atlas) or images that you will later use from UIKit controls (UIKit controls cannot access images inside texture atlases).
From the Resources/Images folder, drag each of the individual images into the asset catalog:
Delete the Spaceship image from the asset catalog. This is a sample image that came with the template but you won’t need any spaceships while crunching those tasty cookies! :]
Outside of the asset catalog in the Project Navigator, delete GameScene.sks
. You won’t be using Xcode’s built-in level editor for this game.
Great! It’s time to write some code. Replace the contents of GameViewController.swift
with the following:
import UIKit import SpriteKit class GameViewController: UIViewController { var scene: GameScene! override func prefersStatusBarHidden() -> Bool { return true } override func shouldAutorotate() -> Bool { return true } override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { return [UIInterfaceOrientationMask.Portrait, UIInterfaceOrientationMask.PortraitUpsideDown] } override func viewDidLoad() { super.viewDidLoad() // Configure the view. let skView = view as! SKView skView.multipleTouchEnabled = false // Create and configure the scene. scene = GameScene(size: skView.bounds.size) scene.scaleMode = .AspectFill // Present the scene. skView.presentScene(scene) } } |
This is mostly boilerplate code that creates the Sprite Kit scene and presents it in the SKView
.
For the final piece of setup, replace the contents of GameScene.swift
with this:
import SpriteKit class GameScene: SKScene { required init?(coder aDecoder: NSCoder) { fatalError("init(coder) is not used in this app") } override init(size: CGSize) { super.init(size: size) anchorPoint = CGPoint(x: 0.5, y: 0.5) let background = SKSpriteNode(imageNamed: "Background") background.size = size addChild(background) } } |
This loads the background image from the asset catalog and places it in the scene. Because the scene’s anchorPoint
is (0.5, 0.5), the background image will always be centered on the screen on all iPhone screen sizes.
Build and run to see what you’ve got so far. Excellent!
The Cookie Class
This game’s playing field will consist 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 Sprite Kit’s coordinate system, it makes sense to have everything else “upside down”—at least compared to the rest of UIKit. :]
Note: Wondering why Sprite Kit’s coordinate system is different than UIKit’s? This is because OpenGL ES’s coordinate system has (0, 0) at the bottom-left, and Sprite Kit is built on top of OpenGL ES and Metal since iOS 9 on supported devices.
To learn more about OpenGL ES, we have a video tutorial series for that.
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 enum CookieType: Int { case Unknown = 0, Croissant, Cupcake, Danish, Donut, Macaroon, SugarCookie } class Cookie { 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 } } |
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 toward the end of this part of the tutorial.
Each cookie type number corresponds to a sprite image:
In Swift, an enum isn’t just useful 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()
(an UInt32) must first be converted to an Int, and 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 model-view-controller (or 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.
Printing Cookies
If you were to use print
to print out a Cookie
at the moment, it wouldn’t look very nice.
What you’d like is to customize the output of when you print a cookie. You can do this by making the Cookie
conform to the CustomStringConvertible
protocol.
To do this, modify the declaration of Cookie
as follows:
class Cookie: CustomStringConvertible { |
Then add a computed property named description
:
var description: String { return "type:\(cookieType) square:(\(column),\(row))" } |
Now print()
will print out something helpful: the type of cookie and its column and row in the level grid. You’ll use this in practice later.
Let’s also make the CookieType
enum printable. Add the CustomStringConvertible
protocol to the enum definition and have the description
property return the sprite name, which is a pretty good description of the cookie type:
enum CookieType: Int, CustomStringConvertible { ... var description: String { return spriteName } } |
Working with 2D Arrays
Now you need something to hold that 9×9 grid of cookies. The Objective-C version of this tutorial did this,
Cookie *_cookies[9][9]; |
to create a two-dimensional array of 81 elements. Then you could simply do myCookie = _cookies[3][6];
to find the cookie at column 3, row 6.
Swift arrays, however, work quite differently from plain old C arrays and the above is not possible. Fortunately, you can create your own type that acts like a 2D array and that is just as convenient to use.
Go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Array2D.swift
and click Create.
Replace the contents of Array2D.swift
with the following:
struct Array2D<T> { let columns: Int let rows: Int private var array: Array<T?> init(columns: Int, rows: Int) { self.columns = columns self.rows = rows array = Array<T?>(count: rows*columns, repeatedValue: nil) } subscript(column: Int, row: Int) -> T? { get { return array[row*columns + column] } set { array[row*columns + column] = newValue } } } |
The notation Array2D<T>
means that this struct is a generic; it can hold elements of any type T
. You’ll use Array2D
to store Cookie
objects, but later on in the tutorial you’ll use another Array2D
to store a different type of object, Tile
.
Array2D
‘s initializer creates a regular Swift Array
with a count of rows × columns and sets all these elements to nil. When you want a value to be nil in Swift, it needs to be declared optional, which is why the type of the array
property is Array<T?>
and not just Array<T>
.
What makes Array2D
easy to use is that is supports subscripting. If you know the column and row numbers of a specific item, you can index the array as follows: myCookie = cookies[column, row]
. Sweet!
The Level Class
That’s the preliminaries out of the way. Let’s put Array2D
to use.
First, however, a minor change to the Cookie.swift
class. Cookies will later be used in a Set
and the objects that you put into the set must conform to the Hashable
protocol. That’s a requirement from Swift. Right now, Cookie
does not conform to Hashable
.
Switch to Cookie.swift
and change the class declaration to include Hashable
:
class Cookie: CustomStringConvertible, Hashable { |
Add the following property inside the class:
var hashValue: Int { return row*10 + column } |
The Hashable
protocol requires that you add a hashValue
property to the object. This should return an Int value that is as unique as possible for your object. Its position in the 2D grid is enough to identify each cookie, and you’ll use that to generate the hash value.
Also add the following function outside the Cookie
class:
func ==(lhs: Cookie, rhs: Cookie) -> Bool { return lhs.column == rhs.column && lhs.row == rhs.row } |
Whenever you add the Hashable
protocol to an object, you also need to supply the ==
comparison operator for comparing two objects of the same type. That’s it! The Cookie
class is now ready to be used within a Set
.
Next, 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 Level.swift
:
func cookieAtColumn(column: Int, row: Int) -> Cookie? { assert(column >= 0 && column < NumColumns) assert(row >= 0 && row < NumRows) return cookies[column, row] } |
Using cookieAtColumn(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 (they may be nil).
Notice the use of assert()
to verify that the specified column and row numbers are within the valid range of 0-8.
Note: New to assert
? The idea behind assert
is you give it a condition, and if the condition fails the app will crash with a log message.
“Wait a minute,” you may think, “why would I want to crash my app on purpose?!”
Crashing your app on purpose is actually a good thing if you have a condition that you don’t expect to ever happen in your app like this one. assert
will help you because when the app crashes, the backtrace will point exactly to this unexpected condition, making it nice and easy to resolve the source of the problem.
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 Level.swift
:
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 var 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<Cookie>
object. A Set is a collection, like an array, but it allows each element to appear only once, and it does not store the elements in any particular order.
The shuffle
method 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 the method picks a random cookie type. This uses the
random()
function you added to theCookieType
enum earlier. - Next, the method creates a new
Cookie
object and adds it to the 2D array. - Finally, the method adds the new
Cookie
object to aSet
.shuffle
returns this set of cookie objects 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
object and call shuffle
to fill it up with cookies, the Level
replies, “Here is a set with all the new Cookie
objects I just added.” You can take that set and, for example, create new sprites for all the cookie objects it contains. In fact, that’s exactly what you’ll do in the next section.
Press Command+B to build the app and make sure you’re not getting any compilation errors.
The Scene and the View Controller
In many Sprite Kit games, the “scene” is the main object for the game. In Cookie Crunch, however, you’ll make the view controller play that role.
Why? The game will include UIKit elements, such as labels, and it makes sense for the view controller to manage them. You’ll still have a scene object—GameScene
from the template—but this will only be responsible for drawing the sprites; it won’t handle any of the game logic.
Cookie Crunch will use an architecture that is very much like the model-view-controller or MVC pattern that you may know from non-game apps:
- The data model will consist of
Level
,Cookie
and a few other classes. The models will contain the data, such as the 2D grid of cookie objects, and handle most of the gameplay logic. - The views will be
GameScene
and theSKSpriteNodes
on the one hand, andUIViews
on the other. The views will be responsible for showing things on the screen and for handling touches on those things. The scene in particular will draw the cookie sprites and detect swipes. - The view controller will play the same role here as in a typical MVC app: it will sit between the models and the views and coordinate the whole shebang.
All of these objects will communicate with each other, mostly by passing arrays and sets of objects to be modified. This separation will give each object only one job that it can do, totally independent of the others, which will keep the code clean and easy to manage.
Note: Putting the game data and rules in separate model objects is especially useful for unit testing. This tutorial doesn’t cover unit testing but, for a game such as this, it’s a good idea to have a comprehensive set of tests for the game rules. To learn more about unit testing, check out our Unit Testing Basics video series.
If game logic and sprites are all mixed up, then it’s hard to write such tests, but in this case you can test Level
separate from the other components. This kind of testing lets you add new game rules with confidence you didn’t break any of the existing ones.
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. This variable is marked as Level!
with an exclamation point because it will not initially have a value.
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 Sprite Kit 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, 0), and the position
of the scene also defaults to (0, 0). This means (0, 0) is in the center of the screen. Therefore, when you add these layers as children of the scene, the point (0, 0) in layer coordinates will also be in the center of the screen.
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.
Note: Because NumColumns
and NumRows
are of type Int but CGPoint
‘s x
and y
fields are of type CGFloat
, you have to convert these values by writing CGFloat(NumColumns)
. You’ll see this sort of thing a lot in Swift code.
Adding the sprites to the scene happens in addSpritesForCookies()
. Add it below:
func addSpritesForCookies(cookies: Set<Cookie>) { for cookie in cookies { let sprite = SKSpriteNode(imageNamed: cookie.cookieType.spriteName) sprite.size = CGSize(width: TileWidth, height: TileHeight) sprite.position = pointForColumn(cookie.column, row:cookie.row) cookiesLayer.addChild(sprite) cookie.sprite = sprite } } func pointForColumn(column: Int, row: Int) -> CGPoint { return CGPoint( x: CGFloat(column)*TileWidth + TileWidth/2, y: CGFloat(row)*TileHeight + TileHeight/2) } |
addSpritesForCookies()
iterates through the set of cookies and adds a corresponding SKSpriteNode
instance to the cookie layer. This uses a helper method, pointForColumn(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
.
Hop over to 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.addSpritesForCookies(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.
Note: The reason you declared the var level
property as Level!
, with an exclamation point, is that all properties must have a value by the time the class is initialized. But you can’t give level
a value in init()
yet; that doesn’t happen until viewDidLoad
. With the !
you tell Swift that this variable won’t have a value until later (but once it’s set, it will never become nil again).
Finally, make sure you call beginGame()
at the end of viewDidLoad()
to set things in motion:
override func viewDidLoad() { ... beginGame() } |
Build and run, and you should finally see some cookies:
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 9×9 grid, but they leave some of the squares blank.
Drag the Levels folder from the tutorial’s Resources folder into your Xcode project. As always, make sure Destination: Copy items if needed is checked. This folder contains five JSON 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.
You’ll load this data in Level
, but first you need to add a new class, Tile
, to represent a single tile in the 2D level grid. Note that a tile is different than a cookie — think of tiles as “slots”, and of cookies as the things inside the slots. I’ll discuss more about this in a bit.
Add a new Swift File to the project. Name it Tile.swift
. Replace the contents of this file with:
class Tile { } |
You can leave this new class empty right now. Later on, I’ll give you some hints for how to use this class to add additional features to the game, such as “jelly” tiles.
Open Level.swift
and add a new property and method:
private var tiles = Array2D<Tile>(columns: NumColumns, rows: NumRows) func tileAtColumn(column: Int, row: Int) -> Tile? { assert(column >= 0 && column < NumColumns) assert(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. The top-level item in the JSON file is a dictionary, so it makes sense to add the code for loading the JSON file to Swift’s Dictionary
.
Go to File\New\File…, choose the iOS\Source\Swift File template and click Next. Name the file Extensions.swift
and click Create.
Replace the contents of Extensions.swift
with the following:
import Foundation extension Dictionary { static func loadJSONFromBundle(filename: String) -> Dictionary <String, AnyObject>? { var dataOK: NSData var dictionaryOK: NSDictionary = NSDictionary() if let path = NSBundle.mainBundle().pathForResource(filename, ofType: "json") { let _: NSError? do { let data = try NSData(contentsOfFile: path, options: NSDataReadingOptions()) as NSData! dataOK = data } catch { print("Could not load level file: \(filename), error: \(error)") return nil } do { let dictionary = try NSJSONSerialization.JSONObjectWithData(dataOK, options: NSJSONReadingOptions()) as AnyObject! dictionaryOK = (dictionary as! NSDictionary as? Dictionary <String, AnyObject>)! } catch { print("Level file '\(filename)' is not valid JSON: \(error)") return nil } } return dictionaryOK as? Dictionary <String, AnyObject> } } |
Using Swift’s extension mechanism you can add new methods to existing types. Here you have added loadJSONFromBundle()
to load a JSON file from the app bundle, into a new dictionary of type Dictionary<String, AnyObject>
. This means the dictionary’s keys are always strings but the associated values can be any type of object.
The method simply loads the specified file into an NSData
object and then converts that to a Dictionary
using the NSJSONSerialization
API. This is mostly boilerplate code that you’ll find in any app that deals with JSON files.
Note: To learn more about JSON and parsing it in iOS, check out our Working with JSON Tutorial.
Next, add the new init(filename:)
initializer to Level.swift
:
init(filename: String) { // 1 guard let dictionary = Dictionary<String, AnyObject>.loadJSONFromBundle(filename) else { return } // 2 guard let tilesArray = dictionary["tiles"] as? [[Int]] else { return } // 3 for (row, rowArray) in tilesArray.enumerate() { // 4 let tileRow = NumRows - row - 1 // 5 for (column, value) in rowArray.enumerate() { if value == 1 { tiles[column, tileRow] = Tile() } } } } |
Here’s what this initializer does, step-by-step:
- Load the named file into a
Dictionary
using theloadJSONFromBundle()
helper function that you just added. Note that this function may return nil — it returns an optional — and here you use a guard to handle this situation. - The dictionary has an array named “tiles”. This array contains one element for each row of the level. Each of those row elements is itself an array containing the columns for that row. The type of
tilesArray
is therefore array-of-array-of-Int, or[[Int]]
. - Step through the rows using built-in
enumerate()
function, which is useful because it also returns the current row number. - In Sprite Kit (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 it finds a 1, it creates a
Tile
object and places it into thetiles
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 { var 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. The texture atlas already contains an image for this (Tile.png). These new tile sprites will live on their own layer, the tilesLayer
.
To do this, first add a new private property to GameScene.swift
:
let tilesLayer = SKNode() |
Then add this code to init(size:)
, right above where you add the cookiesLayer
:
tilesLayer.position = layerPosition gameLayer.addChild(tilesLayer) |
It needs to be done first so the tiles appear behind the cookies (Sprite Kit nodes with the same zPosition
are drawn in order of how they were added).
Add the following method to GameScene.swift
, as well:
func addTiles() { for row in 0..<NumRows { for column in 0..<NumColumns { if level.tileAtColumn(column, row: row) != nil { let tileNode = SKSpriteNode(imageNamed: "Tile") tileNode.size = CGSize(width: TileWidth, height: TileHeight) tileNode.position = pointForColumn(column, row: row) tilesLayer.addChild(tileNode) } } } } |
This loops through all the rows and columns. If there is a tile at that grid square, then it creates a new tile sprite and adds it to the tiles layer.
Next, open GameViewController.swift
. Add the following line to viewDidLoad()
, immediately after you set scene.level
:
scene.addTiles() |
Build and run, and you can clearly see where the tiles are:
You can switch to another level design by specifying a different file name in viewDidLoad()
. Simply change the filename:
parameter to “Level_2”, “Level_3” or “Level_4” and build and run. Does Level 3 remind you of anything? :]
Feel free to make your own designs, too! Just remember that the “tiles” array should contain nine arrays (one for each row), with nine numbers each (one for each column).
Where to Go From Here?
Here is the sample project with all of the code from the Swift tutorial up to this point.
Your game is shaping up nicely, but there’s still a way to go before it’s finished. For now give yourself a cookie for making it through part one!
In the next part, you’ll work on detecting swipes and swapping cookies. You’re in for a treat ;]
While you eat your cookie, take a moment to let us hear from you in the forums!
Credits: Free game art from Game Art Guppy.
Portions of the source code were inspired by Gabriel Nica‘s Swift port of the game.
The post How to Make a Game Like Candy Crush with SpriteKit and Swift: Part 1 appeared first on Ray Wenderlich.