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:
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
:
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:
Enter CookieCrunch Mac as the product name and click Finish:
Xcode will create two new targets for you: CookieCrunch Mac and CookieCrunch Mac Tests:
Try running your new app on OS X. First, select CookieCrunch Mac from the scheme list, then select My Mac as shown below:
Build and run. What do you expect to see? Will it be:
- Nothing at all?
- The CookieCrunch game running on Mac?
- Something else?
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:
- The UI elements such as
NSLabel
andNSButton
do not have the built-in ability to do things like shadowed text. - 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. - 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:
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:
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:
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:
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:
- 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. - 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.
- 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:
- 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.
- The
text
is a computed property that passes through to the child labels, ensuring both labels text are set correctly. - Likewise the
verticalAlignmentMode
andhorizontalAlignmentMode
properties pass through to the two labels as well. - 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):
- An
action
property holds a reference to the closure that will be invoked when the user taps or clicks on the button. - 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.
- 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! - When user interaction begins (either a touch down or mouse down event), the button is marked as selected.
- 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.
- 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. - Note the use of optional chaining as indicated by the
?
. This indicates that nothing should happen if noaction
is set (that is, whenaction == 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
andscore
. - 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 theGameController
. - Delete the lines that hide the
gameOverPanel
andshuffleButton
– 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)
tobeginGame()
. - 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) -> 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!
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:
In the Identity Inspector change SKView to CCView as shown below:
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!
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:
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:
Build and run. The game is now sized appropriately and does not allow resizing:
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.