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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Make a Game Like Candy Crush Tutorial: OS X Port
40 mins
- Getting Started
- Add an OS X Build Target
- Surveying the work to be done
- Sprite Kit on iOS and OS X
- Platform-specific UI
- Source File Organization
- Porting UIKit to Sprite Kit
- Cross-platform event handling extension
- Cross-platform GameScene class
- Removing Dependencies on UIKit
- Labels with Shadows
- A simple Sprite Kit button
- A cross-platform controller
- Getting the game running on OS X
- OS X Finishing Touches
- Where to go from here?
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:
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.
- 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: Set<NSObject>, withEvent event: UIEvent) {
userInteractionBegan(touches.first as! UITouch)
}
override public func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
userInteractionContinued(touches.first as! UITouch)
}
override public func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
userInteractionEnded(touches.first as! UITouch)
}
override public func touchesCancelled(touches: Set<NSObject>, withEvent event: UIEvent) {
userInteractionCancelled(touches.first 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: Set<NSObject>, withEvent event: UIEvent) {
// Convert the touch location to a point relative to the cookiesLayer.
let touch = touches.first 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: Set<NSObject>, withEvent event: UIEvent) {
and replace it with the following:
override func userInteractionContinued(event: CCUIEvent) {
Within that function, replace the lines
let touch = touches.first as! UITouch
let location = touch.locationInNode(cookiesLayer)
with:
let location = event.locationInNode(cookiesLayer)
Now replace:
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
with:
override func userInteractionEnded(event: CCUIEvent) {
And finally, replace:
override func touchesCancelled(touches: Set<NSObject>, 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.