How to Make a Game Like Candy Crush Tutorial: OS X Port

Learn how to take an existing iOS Sprite Kit game and level it up to work on OS X too! By Gabriel Hauber.

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

Labels with Shadows

If you review the API for SKLabelNode, you’ll notice that there is nothing there about shadows. (Incidentally, the NSLabel component in AppKit doesn’t have the ability to add shadows, either.) Hence, if you want to keep your text readable, you’ll need to implement your own custom ShadowedLabelNode class.

Create a new Swift file in the shared group, naming it ShadowedLabelNode.swift (make sure you add it to both iOS and OS X targets!). Replace its auto-generated contents with the following code:

import SpriteKit

class ShadowedLabelNode: SKNode {

  // 1
  private let label: SKLabelNode
  private let shadowLabel: SKLabelNode

  // 2
  var text: String {
    get {
      return label.text
    }
    set {
      label.text = newValue
      shadowLabel.text = newValue
    }
  }

  // 3
  var verticalAlignmentMode: SKLabelVerticalAlignmentMode {
    get {
      return label.verticalAlignmentMode
    }
    set {
      label.verticalAlignmentMode = newValue
      shadowLabel.verticalAlignmentMode = newValue
    }
  }

  var horizontalAlignmentMode: SKLabelHorizontalAlignmentMode {
    get {
      return label.horizontalAlignmentMode
    }
    set {
      label.horizontalAlignmentMode = newValue
      shadowLabel.horizontalAlignmentMode = newValue
    }
  }

  required init(coder: NSCoder) {
    fatalError("NSCoding not supported")
  }

  // 4
  init(fontNamed fontName: String, fontSize size: CGFloat, color: SKColor, shadowColor: SKColor) {
    label = SKLabelNode(fontNamed: fontName)
    label.fontSize = size
    label.fontColor = color

    shadowLabel = SKLabelNode(fontNamed: fontName)
    shadowLabel.fontSize = size
    shadowLabel.fontColor = shadowColor

    super.init()

    shadowLabel.position = CGPoint(x: 1, y: -1)
    addChild(shadowLabel)
    addChild(label)
  }
}

Let’s walk through this class step-by-step:

  1. As you can see, a shadowed label is constructed by using two labels of different colors, one offset from the other by a point in the vertical and horizontal directions.
  2. The text is a computed property that passes through to the child labels, ensuring both labels text are set correctly.
  3. Likewise the verticalAlignmentMode and horizontalAlignmentMode properties pass through to the two labels as well.
  4. Finally, the initializer sets up the two labels, ensuring that they are slightly offset from each other to create the shadow effect.

You could create a more comprehensive wrapper matching the SKLabelNode API; but this is all that is needed for the Cookie Crunch game.

A simple Sprite Kit button

You’ll also need a Sprite Kit-based button to replace the UIButton currently used in the iOS target.

In the shared group, create a new file, ButtonNode.swift, adding it to both the iOS and OS X targets. Replace its contents with the following code:

import SpriteKit

class ButtonNode: SKSpriteNode {

  // 1 - action to be invoked when the button is tapped/clicked on
  var action: ((ButtonNode) -> Void)?
  
  // 2
  var isSelected: Bool = false {
    didSet {
      alpha = isSelected ? 0.8 : 1
    }
  }
    
  // MARK: - Initialisers
    
  required init(coder: NSCoder) {
    fatalError("NSCoding not supported")
  }
    
  // 3
  init(texture: SKTexture) {
    super.init(texture: texture, color: SKColor.whiteColor(), size: texture.size())
    userInteractionEnabled = true
  }
    
  // MARK: - Cross-platform user interaction handling

  // 4
  override func userInteractionBegan(event: CCUIEvent) {
    isSelected = true
  }

  // 5
  override func userInteractionContinued(event: CCUIEvent) {
    let location = event.locationInNode(parent)

    if CGRectContainsPoint(frame, location) {
      isSelected = true
    } else {
      isSelected = false
    }
  }

  // 6
  override func userInteractionEnded(event: CCUIEvent) {
    isSelected = false

    let location = event.locationInNode(parent)

    if CGRectContainsPoint(frame, location) {
      // 7
      action?(self)
    }
  }
}

The class is deliberately kept nice and simple: only implementing the things absolutely needed for this game. Note the following (numbers reference the corresponding comment in the code):

  1. An action property holds a reference to the closure that will be invoked when the user taps or clicks on the button.
  2. You will want to visually indicate when the button is being pressed. A simple way to do this is to change the alpha value when the button is selected.
  3. Most of the initialization is handled by the SKSpriteNode superclass. All you need to do is pass in a texture to use, and make sure that the node is enabled for user interaction!
  4. When user interaction begins (either a touch down or mouse down event), the button is marked as selected.
  5. If, during the course of user interaction, the mouse or touch moves outside the node’s bounds, the button is no longer shown to be selected.
  6. If when the mouse click finishes or the user lifts their finger and the event location is within the bounds of the button, the action is triggered.
  7. Note the use of optional chaining as indicated by the ?. This indicates that nothing should happen if no action is set (that is, when action == nil)

You are also going to need one more thing before you can begin your controller conversion in earnest. The iOS controller uses a UITapGestureRecognizer to trigger the beginning of a new game. OS X has good gesture recognizer support as well, but in this case you will need an NSClickGestureRecognizer.

In EventHandling.swift, add the following type alias to the iOS section below the definition of CCUIEvent = UITouch:

typealias CCTapOrClickGestureRecognizer = UITapGestureRecognizer

Similarly below the line CCUIEvent = NSEvent:

typealias CCTapOrClickGestureRecognizer = NSClickGestureRecognizer

The APIs on these classes are similar enough that you can type alias them as you did with UITouch and NSEvent.

A cross-platform controller

Now it’s time to get your hands really dirty, and gut the iOS GameViewController class to create the cross-platform controller. But before you do that, you need somewhere to put the shared controller code.

Create a new Swift file GameController.swift in the Shared group, and add it to both the iOS and OS X targets.

Replace its contents with the following:

import SpriteKit
import AVFoundation

class GameController: NSObject {
    
  let view: SKView

  // The scene draws the tiles and cookie sprites, and handles swipes.
  let scene: GameScene

  // 1 - levels, movesLeft, score

  // 2 - labels, buttons and gesture recognizer

  // 3 - backgroundMusic player


  init(skView: SKView) {
    view = skView
    scene = GameScene(size: skView.bounds.size)

    super.init()

    // 4 - create and configure the scene

    // 5 - create the Sprite Kit UI components

    // 6 - begin the game
  }

  // 7 - beginGame(), shuffle(), handleSwipe(), handleMatches(), beginNextTurn(), updateLabels(), decrementMoves(), showGameOver(), hideGameOver()
}

Move the following code out of GameViewController.swift into the marked locations in GameController.swift.

  • At 1, insert the declarations for level, movesLeft and score.
  • At 3 (you’ll come back to 2 later), put the code for creating the backgroundMusic AVAudioPlayer instance.
  • At 4, take everything from viewDidLoad from the “create and configure the scene” comment to the line that assigns the swipe handler (scene.swipeHandler = handleSwipe) and move it into the initializer for the GameController. Delete the duplicate assignment scene = GameScene(size: skView.bounds.size) (the one after super.init()).
  • Delete the lines that hide the gameOverPanel and shuffleButton – you’ll do things slightly differently when you create the labels, buttons, etc, in a moment, below, at 5.
  • At 6, move the lines from skView.presentScene(scene) to beginGame().
  • At 7, move the functions beginGame(), shuffle(), handleSwipe(), handleMatches(), beginNextTurn(), updateLabels(), decrementMoves(), showGameOver(), hideGameOver().

The old iOS GameViewController class should be looking a lot slimmer now! You’re not yet done gutting it, however. Delete the following lines from the GameViewController class:

// The scene draws the tiles and cookie sprites, and handles swipes.
var scene: GameScene!

@IBOutlet weak var targetLabel: UILabel!
@IBOutlet weak var movesLabel: UILabel!
@IBOutlet weak var scoreLabel: UILabel!
@IBOutlet weak var gameOverPanel: UIImageView!
@IBOutlet weak var shuffleButton: UIButton!

var tapGestureRecognizer: UITapGestureRecognizer!

You’ll deal with the shuffleButtonPressed() method in a moment.

Before that, you’ll need a reference to the new GameController object within the GameViewController. Add the following property declaration to the GameViewController class:

var gameController: GameController!

Create an instance of this class at the end of viewDidLoad() with the following code:

gameController = GameController(skView: skView)

There’s now some housekeeping to do in the iOS storyboard. Open Main.storyboard and delete all the labels, the image view and the shuffle button so the game view controller becomes a blank canvas:

The new Sprite Kit-based components will be created in the GameController class.

In GameController.swift, at the // 2 comment, paste this code:

let targetLabel = ShadowedLabelNode(fontNamed: "GillSans-Bold", fontSize: 22, color: SKColor.whiteColor(), shadowColor: SKColor.blackColor())
let movesLabel = ShadowedLabelNode(fontNamed: "GillSans-Bold", fontSize: 22, color: SKColor.whiteColor(), shadowColor: SKColor.blackColor())
let scoreLabel = ShadowedLabelNode(fontNamed: "GillSans-Bold", fontSize: 22,  color: SKColor.whiteColor(), shadowColor: SKColor.blackColor())

var shuffleButton: ButtonNode!
var gameOverPanel: SKSpriteNode!

var tapOrClickGestureRecognizer: CCTapOrClickGestureRecognizer!

This code creates the three labels that display the level target, remaining moves and current score. It then declares properties for the shuffleButton, gameOverPanel and the tapOrClickGestureRecognizer which will handle the rest of the user interaction.

To create the labels for target, moves and score, paste the following into GameController.swift at the // 5 comment:

let nameLabelY = scene.size.height / 2 - 30
let infoLabelY = nameLabelY - 34

let targetNameLabel = ShadowedLabelNode(fontNamed: "GillSans-Bold", fontSize: 16, color: SKColor.whiteColor(), shadowColor: SKColor.blackColor())
targetNameLabel.text = "Target:"
targetNameLabel.position = CGPoint(x: -scene.size.width / 3, y: nameLabelY)
scene.addChild(targetNameLabel)

let movesNameLabel = ShadowedLabelNode(fontNamed: "GillSans-Bold", fontSize: 16, color: SKColor.whiteColor(), shadowColor: SKColor.blackColor())
movesNameLabel.text = "Moves:"
movesNameLabel.position = CGPoint(x: 0, y: nameLabelY)
scene.addChild(movesNameLabel)

let scoreNameLabel = ShadowedLabelNode(fontNamed: "GillSans-Bold", fontSize: 16, color: SKColor.whiteColor(), shadowColor: SKColor.blackColor())
scoreNameLabel.text = "Score:"
scoreNameLabel.position = CGPoint(x: scene.size.width / 3, y: nameLabelY)
scene.addChild(scoreNameLabel)

This code first determines the y location for the name (“Target:”, “Moves:” and “Score:”) and value labels. Since the scene’s anchor point is the center, the y location is determined by adding half the scene height and then subtracting a small value, putting the labels just below the top of the view. The labels displaying the score, etc, are set to display 34 points below the heading labels.

For each label that is created, its text and position (the x position is relative to the center of the scene) are set, and the label is added to the scene.

Add the following code just below the code you just added:

targetLabel.position = CGPoint(x: -scene.size.width / 3, y: infoLabelY)
scene.addChild(targetLabel)
movesLabel.position = CGPoint(x: 0, y: infoLabelY)
scene.addChild(movesLabel)
scoreLabel.position = CGPoint(x: scene.size.width / 3, y: infoLabelY)
scene.addChild(scoreLabel)

This code sets the positions of the value labels and adds them to the scene.

To create the shuffle button, add the following code just below the code you just added:

shuffleButton = ButtonNode(texture: SKTexture(imageNamed: "Button"))
shuffleButton.position = CGPoint(x: 0, y:  -scene.size.height / 2 + shuffleButton.size.height)

let nameLabel = ShadowedLabelNode(fontNamed: "GillSans-Bold", fontSize: 20, color: SKColor.whiteColor(), shadowColor: SKColor.blackColor())
nameLabel.text = "Shuffle"
nameLabel.verticalAlignmentMode = .Center

shuffleButton.addChild(nameLabel)
scene.addChild(shuffleButton)
shuffleButton.hidden = true

This creates the button node, positions it just above the bottom of the scene, and adds the text “Shuffle” by using another ShadowedLabelNode as a child of the button. By setting center vertical alignment on the label it will be rendered properly centered on its parent button node. (By default, labels are aligned on its text’s baseline.) The button is added to the scene; but is initially hidden.

To set up the button’s action, add the following code just below the code you just added:

shuffleButton.action = { (button) in
  // shuffle button pressed!
}

Ok, what should go here? That’s right – the contents of the shuffleButtonPressed() method from the GameViewController class. Move the contents of that method into the shuffleButton action closure (you will need to prefix each method call with self as well), so it looks like this:

shuffleButton.action = { (button) in
  self.shuffle()

  // Pressing the shuffle button costs a move.
  self.decrementMoves()
}

As it is no longer needed, delete the shuffleButtonPressed() method from the GameViewController class. That class is looking rather svelte now, don’t you think?

Ok, just the game over panel and starting a new game left to do before the iOS version of the game is running again.

Above, you changed the gameOverPanel to be an SKSpriteNode. Find the decrementMoves() function in the GameController class and replace:

gameOverPanel.image = UIImage(named: "LevelComplete")

with:

gameOverPanel = SKSpriteNode(imageNamed: "LevelComplete")

Likewise, replace:

gameOverPanel.image = UIImage(named: "GameOver")

with:

gameOverPanel = SKSpriteNode(imageNamed: "GameOver")

In showGameOver(), you need to add the gameOverPanel to the scene instead of unhiding it. So, replace:

gameOverPanel.hidden = false

with:

scene.addChild(gameOverPanel!)

To use the cross-platform CCTapOrClickGestureRecognizer to handle starting a new game, replace the contents of the animateGameOver() closure in showGameOver() with the following:

self.tapOrClickGestureRecognizer = CCTapOrClickGestureRecognizer(target: self, action: "hideGameOver")
self.view.addGestureRecognizer(self.tapOrClickGestureRecognizer)

In hideGameOver(), replace all references to tapGestureRecognizer with tapOrClickGestureRecognizer. And, instead of hiding the gameOverPanel you need to remove it from the scene and clean up by setting it to nil. Replace:

gameOverPanel.hidden = true

With:

gameOverPanel.removeFromParent()
gameOverPanel = nil

Build and run the iOS target. If everything went well, you should be able to play the game just as before. But now you’ll be doing it entirely using Sprite Kit-based UI components!

Gabriel Hauber

Contributors

Gabriel Hauber

Author

Over 300 content creators. Join our team.