Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

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

$
0
0
Level up your iOS Sprite Kit skills to cover OS X!

Level up your iOS Sprite Kit skills to cover OS X!

If you’ve successfully completed and enjoyed the two-part tutorial How to Make a Game Like Candy Crush with Swift, you’re surely hungry for more cookie adventures! Where could you go from there?

The game makes heavy use of Sprite Kit, and Sprite Kit is available on the Mac as well. Apple has done an excellent job of keeping the Sprite Kit APIs unified across both platforms so if you are planning on developing games with Sprite Kit, why not consider targeting iOS devices and Macs in one project?

This tutorial will show you how to take an existing iOS Sprite Kit game written in Swift – Cookie Crunch – and adapt it to run on OS X. Whether you worked through the Objective-C or Swift version of the iOS tutorial, you’ll find many of the concepts in this tutorial familiar. Still, the Cookie Crunch game presents its own unique challenges.

Getting Started

This tutorial assumes that you have already successfully completed the How to Make a Game Like Candy Crush with Swift Tutorial. For convenience, you can download the completed project from that tutorial here.

Unzip the project and open it in Xcode. Build and run the project and you’ll see the game running in the iOS Simulator:

Cookie Crunch OS X Starter iPhone Screenshot

Add an OS X Build Target

The first thing you need to do to kick off your OS X conversion is to create a new build target for OS X.

At the top of the Project Navigator, select the CookieCrunch project to display project settings. You’ll see that there are currently only two targets: CookieCrunch and CookieCrunch Tests:

OSX_CC_Targets

Ensure your Xcode project is the active window and select the File \ New \ Target… menu item. Xcode will prompt you to select a template for the new target. Select OS X \ Application \ Game and click Next, as shown below:

OSX_CC_CreateGameTarget

Enter CookieCrunch Mac as the product name and click Finish:

OSX_CC_MacTarget

Xcode will create two new targets for you: CookieCrunch Mac and CookieCrunch Mac Tests:

OSX_CC_AddedMacTargets

Try running your new app on OS X. First, select CookieCrunch Mac from the scheme list, then select My Mac as shown below:

OSC_CC_MacScheme

Build and run. What do you expect to see? Will it be:

  1. Nothing at all?
  2. The CookieCrunch game running on Mac?
  3. Something else?
Solution Inside: Solution SelectShow>

 

Shortly you will strip out everything from the Mac target you don’t need before adding in and adapting the code you already developed for iOS. Adding the Mac target doesn’t affect the iOS target, which will continue to work as usual.

Surveying the work to be done

Before diving in to the work of adapting CookieCrunch for OS X, it is important to first survey what needs to be done. After all, you just never dive into a project without a plan, do you? :]

Sprite Kit on iOS and OS X

Apple has done an excellent job keeping the Sprite Kit framework essentially the same on both iOS and OS X. However, there are a couple of significant differences since the iOS framework is built on top of UIKit and the OS X framework is built on top of AppKit.

On iOS, the SKNode class inherits from UIResponder, and SKView inherits from UIView. On OS X, they inherit from NSResponder and NSView respectively.

If you peruse the documentation for each of the classes, you’ll see many similarities and many more differences between UIResponder and NSResponder, and UIView and NSView. The biggest differences are in event handling. They exist because of different input methods: multi-touch on iOS versus mouse events on the Mac. Apple also took the opportunity with UIKit to learn from the lessons of the past and create much “cleaner” APIs.

Touch events received in an SKNode on iOS are UITouch objects, but on OS X events (whether mouse, keyboard, etc) come through as NSEvent objects.

Sprite Kit even helps you out here. Do you want to know the location of a mouse or touch event in an SKNode? The function locationInNode is available as an extension to both NSEvent and UITouch. This simplifies things for you somewhat, but you’ll still have to do work to make your code truly cross-platform.

Platform-specific UI

In the case of Cookie Crunch, you have another, even larger, porting challenge before you. If you worked through the original tutorial, you may remember that you created a number of UI elements (the score labels, game over images, and shuffle button) in Interface Builder. Unfortunately, these are all UIKit elements and are not available to the OS X target.

You could design a roughly similar OS X interface using AppKit, but there are several problems here:

  1. The UI elements such as NSLabel and NSButton do not have the built-in ability to do things like shadowed text.
  2. Overlaying NSView objects on top of a Sprite Kit scene is possible, but involves tricky things with layers that doesn’t seem to handle the transition between retina and non-retina screens very well.
  3. You end up with lots of similar but not-quite-reusable code in both iOS and OS X targets.

Because of this, to make your game code as platform-agnostic as possible, it will be necessary to first migrate the iOS target to use a pure Sprite Kit-based approach for the UI as well as the game elements. You will do this while keeping the ultimate goal in mind: getting the game running just as well on OS X as it currently does on iOS.

So, first things first, you will rearrange your project files a bit to clearly identify what can be shared between the iOS and OS X targets, and what will remain platform-specific.

Source File Organization

In the Project Navigator, select the CookieCrunch project. Create a new group by selecting File \ New \ Group. Name CookieCrunch Shared, as shown below:

OSX_CC_SharedGroup

This group will be the location for all code and resources common to both the OS X and iOS targets.

Now, move (drag and drop) the following source files to the group you just created:

  • Array2D.swift
  • Chain.swift
  • Cookie.swift
  • Extensions.swift
  • GameScene.swift
  • Level.swift
  • Set.swift
  • Swap.swift
  • Tile.swift

Also move the assets and subfolders Grid.atlas, Sprites.atlas, Images.xcassets, Levels and Sounds from the CookieCrunch group into the shared group as well.

When you’re done, your project navigator should look like this:

OSX_CC_FilesMigrated

Now, for all of the files in the shared group, you need to add them to the Mac target. In Project Navigator, select all the Swift files from the shared group. In the file inspector, make sure that CookieCrunch Mac is selected in Target Membership, as shown below:

CookieCrunch-OSX-TargetMembership

Do the same with Grid.atlas, Images.xcassets, Sprites.atlas, and each of the files in the Levels and Sounds groups.

While you’re dealing with the asset catalog, you should also make sure that the app has a Mac icon. Open Images.xcassets, find AppIcon, and in the Inspector, make sure Mac all sizes is checked:

CookieCrunch-OSX-AppIcon

You also need to clean up a few things that Xcode created by default as part of the CookieCrunch Mac target.

From the CookieCrunch Mac group, delete Images.xcassets, GameScene.swift (since there is already a shared GameScene class to reuse), and GameScene.sks.

Replace the contents of the OS X AppDelegate.swift file with the following:

import Cocoa
import SpriteKit
 
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
 
  @IBOutlet weak var window: NSWindow!
  @IBOutlet weak var skView: SKView!
 
  func applicationDidFinishLaunching(aNotification: NSNotification) {
  }
 
  func applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication) -> Bool {
    return true
  }
}

This is a pretty simple AppDelegate right now. But, later, you’ll come back to this code to add a few important elements. It’s a good idea at this stage to make sure that the iOS target still works. Select an iPhone target and build and run the project. If the game still launches, you are ready to move on to the next step. Otherwise, go back and carefully re-check each step you’ve performed so far.

Now, have a go at building the Mac target. What do you expect to happen?

Whoops – what is that? Xcode is reporting a bunch of errors in the GameScene class. Take a look, and you’ll see several instances of:

Use of undeclared type 'UIEvent'
Use of undeclared type 'UITouch'

Remember what I said earlier about the differences in event handling on iOS vs OS X? Now is the time to deal with that.

Porting UIKit to Sprite Kit

As mentioned earlier, the Sprite Kit framework is built on top of the platform-specific UIKit framework on iOS and AppKit on OS X. On both platforms SKScene inherits ultimately from SKNode. On iOS this is built on top of UIResponder; on OS X it’s NSResponder. UIResponder provides methods such as touchesBegan and touchesMoved. NSResponder provides functions such as mouseDown and mouseDragged.

You have several options to tackle this difference:

  1. Conditional compilation in the GameScene class.
    This has the advantage of keeping all code in a single source file. But it sacrifices code readability to do this since you’ll have one big source file with a lot of #if ande #else statements.
  2. Extract the common GameScene code into a superclass and implement OS-specific stuff in a subclass specific to its target. e.g.:
    • GameScene.swift
    • GameSceneIOS.swift
    • GameSceneMac.swift

    This has the advantage of only having platform-neutral code in the shared group, and any OS-specific stuff exists only in the relevant targets. However, the amount of platform-specific work you’ll have to do is actually quite small, so there is a third, better way.

  3. Put all event handling code inside a class extension and use conditional compilation.
    Doing this keeps code all in one place, and it is reusable by any class that derives from SKNode (something you will take advantage of shortly).

Cross-platform event handling extension

Right-click on the CookieCrunch Shared group, select New File… and create a new Swift File. Name it EventHandling.swift and make sure it is added to both the CookieCrunch and CookieCrunch Mac targets.

Replace the contents of the file with the following code:

import SpriteKit
 
// MARK: - cross-platform object type aliases
 
#if os(iOS)
typealias CCUIEvent = UITouch
#else
typealias CCUIEvent = NSEvent
#endif

The first step is to create the CCUIEvent type alias. On iOS it refers to a UITouch object; on OS X, an NSEvent. This will let you use this type in event handling code without having to worry about what platform you are developing on… within reason, of course. In your cross-platform code you will be limited to only calling methods or accessing properties that exist on both platforms.

Apple itself takes this approach for classes such as NSColor and UIColor, by creating a type alias SKColor that points to one or the other as appropriate for the platform. You’re already in good company with your cross-platform style! :]

Next, add the following code to the file:

extension SKNode {
 
  #if os(iOS)
 
  // MARK: - iOS Touch handling
 
  override public func touchesBegan(touches: NSSet, withEvent event: UIEvent)  {
    userInteractionBegan(touches.anyObject() as UITouch)
  }
 
  override public func touchesMoved(touches: NSSet, withEvent event: UIEvent)  {
    userInteractionContinued(touches.anyObject() as UITouch)
  }
 
  override public func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
    userInteractionEnded(touches.anyObject() as UITouch)
  }
 
  override public func touchesCancelled(touches: NSSet, withEvent event: UIEvent) {
    userInteractionCancelled(touches.anyObject() as UITouch)
  }
 
  #else
 
  // MARK: - OS X mouse event handling
 
  override public func mouseDown(event: NSEvent) {
    userInteractionBegan(event)
  }
 
  override public func mouseDragged(event: NSEvent) {
    userInteractionContinued(event)
  }
 
  override public func mouseUp(event: NSEvent) {
    userInteractionEnded(event)
  }
 
  #endif
 
  // MARK: - Cross-platform event handling
 
  func userInteractionBegan(event: CCUIEvent) {
  }
 
  func userInteractionContinued(event: CCUIEvent) {
  }
 
  func userInteractionEnded(event: CCUIEvent) {
  }
 
  func userInteractionCancelled(event: CCUIEvent) {
  }
 
}

This section of code defines the extension to SKNode for cross-platform behavior. You’re mapping the relevant location-based event handling methods on each platform to generic userInteractionBegan/Continued/Ended/Cancelled methods. In each call, the CCUIEvent is passed as a parameter.

Cross-platform GameScene class

Now, in any SKNode-derived class in your project, you only need to change any use of touchesBegan, etc, to userInteractionBegan, etc and it should compile and run on both platforms!

Open GameScene.swift. First, find and replace the following lines:

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
  // Convert the touch location to a point relative to the cookiesLayer.
  let touch = touches.anyObject() as UITouch
  let location = touch.locationInNode(cookiesLayer)

with:

override func userInteractionBegan(event: CCUIEvent) {
  // Convert the touch location to a point relative to the cookiesLayer.
  let location = event.locationInNode(cookiesLayer)

Similarly, find the method definition for touchesMoved:

override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {

and replace it with the following:

override func userInteractionContinued(event: CCUIEvent) {

Within that function, replace the lines

let touch = touches.anyObject() as UITouch
let location = touch.locationInNode(cookiesLayer)

with:

let location = event.locationInNode(cookiesLayer)

Now replace:

override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {

with:

override func userInteractionEnded(event: CCUIEvent) {

And finally, replace:

override func touchesCancelled(touches: NSSet, withEvent event: UIEvent) {
  touchesEnded(touches, withEvent: event)
}

with:

override func userInteractionCancelled(event: CCUIEvent) {
  userInteractionEnded(event)
}

That’s all the changes to call the cross-platform version of the methods. Thanks to the type alias, you can just implement the userInteraction methods now for both OS X and iOS.

Build and run the Mac target. It won’t do anything yet, as its AppDelegate doesn’t set up the game.

As a final check to make sure you’ve done this all correctly, build and run the iOS target. The game should still play as normal.

Removing Dependencies on UIKit

Most of the game user interaction logic is, understandably enough, located in the GameViewController class. However, while AppKit for OS X provides view controllers and views just like UIKit on iOS, the differences are large enough that it would take a lot of work to reimplement the logic in the game controller specifically for OS X.

However, if you survey the GameViewController code, you’ll notice that most of it should be reusable on both platforms. Background music is played through an AVAudioPlayer which is also available on OS X. Much of the game turn logic should be reusable. It is only the code that accesses UIKit components (UILabel, UIImageView and UIButton) that needs to be adapted for OS X.

The various labels can be replaced with Sprite Kit’s SKLabelNode. The Shuffle button and Game Over images can be implemented using SKSpriteNode objects. With these, you can create a custom controller object that is usable on both platforms, maximising code reuse and leaving the total amount of platform-specific code at an absolute minimum.

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 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:

CookieCrunch-OSX-EmtpyIOSstoryboard

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) -> Void 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) -> Void in
  self.shuffle()
  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!

CookieCrunch-iOS-Simulator-SpriteKitConversion

Getting the game running on OS X

As you might imagine, after all that work, you must be really close to getting the OS X version running. And you’d be right!

Switch the current build target to CookieCrunch Mac and invoke a build. There should only be three errors, all instances of the same problem that you’ll have to deal with now:

Swift Compiler Error ‘SKView’ does not have a member named ‘userInteractionEnabled’

Under iOS, the UIView class declares this property. Unfortunately, NSView doesn’t. But all is not lost: with a custom subclass of SKView it is possible to implement this property yourself.

Checking the documentation for the NSView class, you will find the hitTest function. From the discussion in the documentation:

This method is used primarily by an NSWindow object to determine which view should receive a mouse-down event. You’d rarely need to invoke this method, but you might want to override it to have a view object hide mouse-down events from its subviews.

Ah, that sounds like what you want! If you override this function and return nil when user interaction should be disabled, the view won’t receive any mouse events. This should emulate exactly the userInteractionEnabled behaviour of UIKit.

In the CookieCrunch Mac group, create a new Swift file CCView.swift. Only add it to the OS X target and not iOS. Replace the contents of CCView.swift with the following:

import SpriteKit
 
@objc(CCView)
class CCView: SKView {
 
  var userInteractionEnabled: Bool = true
 
  override func hitTest(aPoint: NSPoint) -> NSView? {
    if userInteractionEnabled {
      return super.hitTest(aPoint)
    }
    return nil
  }
}

That looks pretty easy, doesn’t it? If user interaction is enabled, perform the normal hit testing functionality, otherwise return nil to say “nope! don’t send any mouse down events to me!”.

Note the line @objc(CCView) – this is needed so the compiled nib from Interface Builder can find and load the class. The @objc directive instructs the compiler that the CCView class should be accessible from Objective-C classes as well.

To use this class instead of the standard SKView you need to make changes in two places.

The first is in MainMenu.xib. Select it, then in the document outline, select the SKView object, which is contained inside a parent view:

OSX_CC_CCView

In the Identity Inspector change SKView to CCView as shown below:

OSX_CC_CustomClass

In the CookieCrunch Mac AppDelegate.swift, change the line:

@IBOutlet weak var skView: SKView!

to:

@IBOutlet weak var skView: CCView!

And in GameController.swift file in the shared group, change:

let view: SKView

to:

let view: CCView

And change:

init(skView: SKView) {

to:

init(skView: CCView) {

Now Build the OS X target. The errors you had previously should have gone!

Of course, some new errors have been introduced into the iOS target: it doesn’t yet know about anything called CCView. To fix this is relatively simple.

Open GameViewController.swift and, just below the import statements, add the following line:

typealias CCView = SKView

SKView already has everything you need because it subclasses from UIView, so creating a type alias on iOS is all you need to do.

Now change the line in viewDidLoad():

let skView = view as SKView

to:

let skView = view as CCView

Build and run the iOS target to verify that it works again.

Now switch back to the OS X target and build and run. Surprised? All you see is a blank grey window. This is because no instance of the GameController is created in the CookieCrunch Mac AppDelegate!

In the CookieCrunch Mac group, open AppDelegate.swift. Add the following variable to the class:

var gameController: GameController!

This will store a strong reference to the game controller instance so it doesn’t get deallocated while the game is playing. Now, add one simple line to applicationDidFinishLaunching:

gameController = GameController(skView: skView)

Build and run, and voilà! Cookie Crunch running as a native Mac OS X application!

CookieCrunch-OSX-UnoptimisedWindow

OS X Finishing Touches

While the game runs just fine on OS X, there are a few issues you still have to deal with.

The first thing you’ll notice is that the window is much too big for the game. When you resize it, the game scales but the score labels can be cut off. To fix this, open MainMenu.xib. Select the CookieCrunch Mac window. In the Inspector area on the right, select the Size Inspector:

OSX_CC_MacMenu

Since the @2x background image is 320 x 568 points at 1x scale, set the initial window size to match. To prevent resizing, set the minimum and maximum size to the same values:

OSX_CC_MacWindowSize

Build and run. The game is now sized appropriately and does not allow resizing:

CookieCrunch-OSX-OptimisedWindow

Congratulations! Your OS X conversion of Cookie Crunch is complete!

Where to go from here?

You can grab the completed sample files for this project from here.

If you take a look over the project, you’ll see that the amount of platform-specific code is really quite low. By using Sprite Kit for the UI elements as well as the game itself, you have achieved an extremely high level of code reuse! Although developing a UI for a Sprite Kit game in a storyboard for iOS makes it easy to rapidly prototype the game, when it comes to porting to OS X it makes it very difficult, so you might want to consider going Sprite Kit-only from the start.

If you have any comments or questions, feel free to join the discussion below!

How to Make a Game Like Candy Crush Tutorial: OS X Port is a post from: Ray Wenderlich

The post How to Make a Game Like Candy Crush Tutorial: OS X Port appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>