Have you ever wished you could port your iOS Sprite Kit game to OS X? Surprisingly, it’s easier than you think.
Apple developed Sprite Kit with the goal of keeping iOS and Mac OS X development as identical as possible – which makes it extremely easy to develop for both environments simultaneously.
This tutorial will show you how to take an existing iOS Sprite Kit game — the finished project from Sprite Kit Tutorial for Beginners — and adapt it to Mac OS X.
You’ll learn how to maintain both versions in a single Xcode project and how to continue development of your game without copying and pasting heaps of code to keep both versions up to date.
It’s recommended that you have some familiarity with Mac OS X Development if you choose to work through this tutorial. However, one of the wonderful features of Sprite Kit is that you don’t need to have a lot of prior experience to turn out a great app.
If do want to learn more about Mac development, you can check out this three part tutorial on making a simple Mac app.
Getting Started
Download the starter project for this tutorial here, and build and run on your iPhone to try it out.
The starter project has a few changes from the original in order to showcase several differences between the iOS and OS X versions of the game.
Accelerometer Support
This project borrows accelerometer support from the Space Game Starter Kit which lets the player move their character up and down the screen by tilting the device, like so:
More Destructive Power
The ninja star has now been imbued with magical ninja powers of epic destruction!
Ok, maybe not that epic, but this version uses a nice particle effect to give the ninja star some visual impact.
Small Technical Improvements
Vicki Wenderlich lent her talents to the game and added a new tiled background which makes the game more aesthetically pleasing. As well, the status bar is hidden and the app now supports iPad resolutions.
Working with Projects and Targets
A project file ties all of your working files together, but what determines how your project is compiled is a target.
A single project can contain multiple targets. This lets you compile your project in several different ways, depending on which files are associated with a specific target and the specific build settings of the target. This should give you a clue as to how you’re going to set up this project to compile for OS X!
Let’s take a look at this project’s targets. Open the SpriteKitSimpleGame project, and at the top of the left panel, select your project to show the project settings. Then select SpriteKitSimpleGame in the project window.
Right now there are two targets, one for iOS and the other for iOS unit tests, as shown below:
Alternatively, you can select SpriteKitSimpleGame from the Target list if the project and target list are both expanded:
When you build an application, you build it for the device or devices supported by the target. To see which devices the current target supports, click the SpriteKitSimpleGame drop down next to the Stop button at the top-left of the window, as shown below:
This is where you’ll add a build target for Mac OS X.
Adding a Build Target for Mac OS X
Ensure your Xcode project is the active window and select File \ New \ Target as shown below:
Xcode prompts you to select a template for your new target.
Select the OS X\Application\SpriteKit Game template and click Next, as shown below:
Finally, type in the product name as SpriteKitSimpleGameMac and click Finish, like so:
Note: If you plan to release your app on the Mac App Store, your bundle identifier must be registered separately on developer.apple.com. Profiles and provisioning are arranged separately for iOS and Mac OS X developer programs.
To try running your app on OS X, select SpriteKitSimpleGameMac from the scheme list, then select My Mac 64-bit as shown below:
What do you think will happen when you try to build and run your project using this new target?
- A — The game will run perfectly — and that’s the end of the tutorial!
- B — The game will fail to compile with numerous errors.
- C — The game will compile but crash on launch.
- D — The game will compile and run but won’t be the game you expected.
Structuring Your Files for Multiple Targets
Now that you have a Mac OS X build target, you’ll be modifying and adding files to make the app work under OS X. Keeping track of these files can be difficult as time goes on, so it’s best to set up a system right now to help keep all the files organized.
Minimize all of your groups and add a new group named SharedResources as shown below:
This group will be the main location for your game and will contain resources that are common to both Mac OS X and iOS targets.
While you’re at it, create a separate group named Testing and move the test modules into it, like so:
Keeping the unit tests in a separate folder helps avoid visual clutter.
Now that you have your new organized structure for the various files in your project, you’ll need to move the files into the appropriate locations.
Expand SharedResources and SpriteKitSimpleGame. Click and drag the Particles and Sounds groups from SpriteKitSimpleGame to SharedResources.
Next, drag over the sprites.atlas folder, MyScene.h, MyScene.m, GameOverScene.h and GameOverScene.m. Your file structure should look like the one shown below:
Delete the Spaceship.png file — you won’t need that any longer. This is just a boilerplate file that is automatically added when you create a game with the Sprite Kit template.
All the shared files of your Sprite Kit game now reside in the SharedResources group. Everything left in the SpriteKitSimpleGame relates to launching and managing the game on iOS.
Expand the SpriteKitSimpleGameMac group. You’ll need to remove the example game files from this group before you progress any further.
Delete MyScene.h, MyScene.m and Spaceship.png file from the SpriteKitSimpleGameMac group and select Move to Trash. Your file list should look like so:
Note: You may have noticed that the Mac version of your game does not contain a ViewController class; instead, it only has an AppDelegate. The UIViewController
class is part of UIKit which is not available on Mac OS X. Instead, the AppDelegate creates an instance of NSWindow
which will present your Sprite Kit scene.
As a final check, your fully-expanded file list should look like the following:
You’ve removed unnecessary files from the project and organized it neatly. Now it’s time to modify your targets to let the compiler know which files to include with each target.
Adding Target Membership
Expand the Frameworks group and select UIKit.framework
, like so:
Expand the right Utilities Panel select File Inspector, like so:
About halfway down the File Inspector you’ll see the Target Membership section. This is where you select the targets that will use this file.
UIKit is only available on the iOS platform, so leave it unchecked on your Mac OS X targets, as shown below:
The Cocoa Framework is only available on Mac OS X so ensure it’s checked for your Mac targets and unchecked for your iOS targets like so:
The Sprite Kit Framework is available to iOS and Mac OS X so set the target membership as below:
Each individual file in your project has its own target membership with the exception of texture atlases. The atlas itself is the only place you need to set a target membership and all contained textures will be automatically included.
However, classes work a little differently. You can’t set a target membership on a .h file — instead, you must set the target membership on the .m file.
Armed with your new-found understanding of target membership, work through each file in your SharedResources group and make sure both SpriteKitSimpleGame and SpriteKitSimpleGameMac are ticked on each file. In total, you should need eight ticks to get the job done.
Next, work through each file in your SpriteKitSimpleGame group and make sure that only SpriteKitSimpleGame is ticked for each — they should all be set correctly at this point, but it’s good to check.
Finally, work through each file in SpriteKitSimpleGameMac group and make sure that only SpriteKitSimpleGameMac files are ticked. Again, you shouldn’t have to change any but it never hurts to check.
Now that your project is properly set up for your iOS and Mac targets, you can get down to what you’re good at — writing code!
Getting the Game to Build and Run
As it stands right now, your project will still build and run without issue for iOS. The changes you just made have no real effect on the existing game. However, if you build and run the Mac target, you’ll see a bunch of errors. That’s because you haven’t yet accounted for the differences between iOS and OS X.
Build and run your project using the SpriteKitSimpleGameMac target; what do you see?
You’ll receive the error Module ‘CoreMotion’ not found
. Mac OS X doesn’t have a CoreMotion
class or its equivalent; you’ll have to work around this issue and use the keyboard to control player movement. However, your primary goal is to get the project to a buildable state before you worry about implementation details like that.
But how will you fix this? You can’t just remove the line of code referring to CoreMotion
, otherwise the iOS version will break. You can’t work around this by using an if
statment, since the compiler will still check each line of the code and throw an error if it doesn’t recognize something.
Open MyScene.m and replace:
@import CoreMotion; |
with the following code:
#if TARGET_OS_IPHONE @import CoreMotion; #endif |
Unlike a regular if
statement, an #if
is performed by the preprocessor. TARGET_OS_IPHONE
returns TRUE
if the current target is iOS.
Note: If you are plan to use an #if
statement to check if the target OS is Mac OS X then the preferred method to check this is !TARGET_OS_IPHONE
.
TARGET_OS_MAC
seems to work — but the problem is that it also returns TRUE
for iOS.
This might seem odd, but Apple uses !TARGET_OS_IPHONE
in their example projects that contain multiple targets, so if it is a glitch, it’s most likely one they don’t plan to fix.
Now you will need to find the remaining code related to CoreMotion
and surround it with #if
statements.
Find the following code in the instance variables for MyScene.m:
CMMotionManager *_motionManager; |
…and replace it with:
#if TARGET_OS_IPHONE CMMotionManager *_motionManager; #endif |
Scroll down to the init
method and find the following code:
_motionManager = [[CMMotionManager alloc] init]; _motionManager.accelerometerUpdateInterval = 0.05; [_motionManager startAccelerometerUpdates]; |
Replace the above code with the following:
#if TARGET_OS_IPHONE _motionManager = [[CMMotionManager alloc] init]; _motionManager.accelerometerUpdateInterval = 0.05; [_motionManager startAccelerometerUpdates]; #endif |
Now, find the following code [TODO: FPE: Again, are we in the same file?]:
[self updatePlayerWithTimeSinceLastUpdate:timeSinceLast]; |
…and replace it with the following:
#if TARGET_OS_IPHONE [self updatePlayerWithTimeSinceLastUpdate:timeSinceLast]; #endif |
Last but not least, locate the method named updatePlayerWithTimeSinceLastUpdate:
and wrap the entire method with the following code:
#if TARGET_OS_IPHONE - (void)updatePlayerWithTimeSinceLastUpdate:(CFTimeInterval)timeSinceLast . . . } #endif |
If you build this against an iOS target, all of the above #if
statements will return TRUE
, so the app compiles just as it did before. In contrast, if you build this against a Mac OS X target all of the above #if
statements will return FALSE
and none of the above blocks will be compiled.
Take a look at the touchesEnded:withEvents:
method in MyScene.m. Since the current version of Mac OS X doesn’t support touch screens, this method is meaningless. The Mac OS X version of this game will use mouse clicks instead as a perfectly adequate substitute for screen touches.
To avoid adding a bunch of boilerplate code, you’ll now create a class that inherits from SKScene
to help you handle both screen touches and mouse clicks!
Adding Event Handlers
Select your SharedResources group.
On the menu bar select File \ New \ File… as shown below:
Select Objective-C Class from either the iOS or OS X category and click Next.
Name the file SKMScene and make it a subclass of SKScene.
Place the file directly under your project folder, make sure both iOS and Mac targets are ticked in the target area and click Create.
Open SKMScene.h and replace its contents with the following code:
@import SpriteKit; @interface SKMScene : SKScene //Screen Interactions -(void)screenInteractionStartedAtLocation:(CGPoint)location; -(void)screenInteractionEndedAtLocation:(CGPoint)location; @end |
You’ll override the above two screen interaction methods using subclasses of SKMScene.
Add the following code to SKMScene.m directly under the line @implementation SKMScene
:
#if TARGET_OS_IPHONE -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint positionInScene = [touch locationInNode:self]; [self screenInteractionStartedAtLocation:positionInScene]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint positionInScene = [touch locationInNode:self]; [self screenInteractionEndedAtLocation:positionInScene]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint positionInScene = [touch locationInNode:self]; [self screenInteractionEndedAtLocation:positionInScene]; } #else -(void)mouseDown:(NSEvent *)theEvent { CGPoint positionInScene = [theEvent locationInNode:self]; [self screenInteractionStartedAtLocation:positionInScene]; } - (void)mouseUp:(NSEvent *)theEvent { CGPoint positionInScene = [theEvent locationInNode:self]; [self screenInteractionEndedAtLocation:positionInScene]; } - (void)mouseExited:(NSEvent *)theEvent { CGPoint positionInScene = [theEvent locationInNode:self]; [self screenInteractionEndedAtLocation:positionInScene]; } #endif -(void)screenInteractionStartedAtLocation:(CGPoint)location { /* Overridden by Subclass */ } -(void)screenInteractionEndedAtLocation:(CGPoint)location { /* Overridden by Subclass */ } |
That’s a fair bit of code, but if you read through from the top, it makes a lot of sense. Touching the screen or the end of a touch event calls the methods in the #if TARGET_OS_IPHONE
block. You then create a CGPoint
containing the pixel location of the touch and calla the relevant screenInteraction
method.
Pressing a mouse button or releasing the mouse button calls the methods in the #else
section. Similar to above, you create a CGPoint
containing the pixel location of the touch and call the relevant screenInteraction
method.
The advantage of using this subclass is that both touch and click events call a screenInteraction
method. The screenInteraction
methods have no code as you’ll override these in your subclass.
Open MyScene.h and add the following class declaration just under #import
:
#import "SKMScene.h" |
Next, update the superclass in the @interface
line to SKMScene
as shown below:
@interface MyScene : SKMScene |
This ensures your game scene inherits from your SKMScene subclass. You can now substitute your subclasses for the touch events.
In MyScene.m find the following line:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { |
…and replace it with the following:
-(void)screenInteractionEndedAtLocation:(CGPoint)location { |
Next, delete the following lines from the method as you won’t need them any longer:
UITouch *touch = [touches anyObject]; CGPoint location = [touch locationInNode:self]; |
Build and run your project using the Mac OS X target; it should compile and run without too many issues:
Congratulations — you’re now running your Sprite Kit game on your Mac! You’ll notice that it has a few bugs:
- Some Macs have a noticeable pause the first time you click on the screen.
- The particle effect appears to be a little broken on some Macs.
- Not everything is sized correctly on the screen.
- The background music has gone noticeably silent.
You’ll tackle each of these bugs in turn — and you’ll learn about a few of the common bugs you’ll encounter when you convert apps between platforms.
Correcting Pre-loading Issues
This bug may not raise its head on all systems, but when it does it definitely points to a performance issue. “First-time-through” bugs like this usually stem from the initial load of resources into memory.
Texture atlases are the first resource type that springs to mind, but since this app doesn’t contain animations or large complex images, it’s safe to assume the problem is somewhere else.
The sound effects are the next most likely candidate as the sound files don’t get loaded until the user clicks on the screen.
To fix this, add the following instance variable to MyScene.m:
SKAction *_playPewPew; |
Next, add the following line to initWithSize:
inside the if
statement:
_playPewPew = [SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]; |
This modifies your app to preload the sound file when the scene initializes.
Find the following line in screenInteractionEndedAtLocation:
:
[self runAction:[SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]]; |
…and replace it with the following:
[self runAction:_playPewPew]; |
Build and run your app; click the mouse and ensure that the delay has been eradicated.
If your system didn’t expose this bug, then at least the changes above will ensure that it won’t happen on someone else’s system.
Correcting SKS Issues
At the time of this writing the bug in the particle effect seems to be an issue with Xcode 5. You’ll override the file reference to the texture in your sks
file.
Technically, there isn’t anything wrong with your sks
file – and you won’t experience this issue on all systems – but you should fix it nonetheless.
Find the following line in projectile:dideCollideWithMonster:
of MyScene.m:
SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:@"SmallExplosion" ofType:@"sks"]]; |
Add the following code directly under the line you found above:
emitter.particleTexture = [SKTexture textureWithImageNamed:@"spark"]; |
All you have done above is to tell Xcode where to find the particular texture.
Build and run your app; now you can admire your epic glitch-free particle effect.
Correcting Image Resizing Issues
Navigate to your SpriteKitSimpleGameMac group and then to AppDelegate.m. Take a look at the screen size set in applicationDidFinishLaunching:
.
It’s set to 1024 x 768 — this is the resolution of the non-Retina iPad.
Now take a look at the contents of sprites.atlas. As expected, all iPad versions of images are suffixed with ~ipad
so that your app knows to use these images when it runs on an iPad.
Unfortunately, there is no ~mac
suffix you can use here; instead, you’ll need to create a separate texture atlas for the Mac version of your app.
In order to keep your build as small as possible, you should use a texture atlas with only the resolutions your app will actually use.
Right-click on sprites.atlas and select Show in Finder to take you to the images folder.
Create a copy of sprites.atlas and delete all images from the copied folder that don’t have ~ipad
in their name.
Next, remove the ~ipad
designator from the file names but leave the @2x
designator intact.
Note: The @2x
files have been left in the project to support the Retina screens on the Macbook Pro.
Rename the folder to spritesMac.atlas and drag the renamed folder into your project.
In the Choose options for adding these files dialog, make sure only the SpriteKitSimpleGameMac target is ticked in the Add to targets section as shown below:
Click Finish. Now that the folder has been imported, select sprites.atlas and turn off target membership for Macs. This ensures that each texture atlas works separately of the other.
Keeping with the spirit of staying organized, move the iOS texture atlas into the iOS group and the Mac texture atlas into the Mac group, as shown below:
Next, go to Project\Clean. This will remove any old files from your build directory (if you forget to do this it might not work, as sprites.atlas may still exist).
Build and run your app; you should see that everything loads at the proper size, as shown below:
At this point your app supports iPhone, iPad and Mac OS X resolutions — and Retina-compatible to boot.
Correcting Soundtrack Issues
Finally, you’ll need to deal with the missing soundtrack to your game.
Look at ViewController.m in the SpriteKitSimpleGame group. viewWillLayoutSubviews
has a small section of code which instantiates AVAudioPlayer
and sets it to repeat forever.
NSError *error; NSURL *backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"]; self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error]; self.backgroundMusicPlayer.numberOfLoops = -1; [self.backgroundMusicPlayer prepareToPlay]; [self.backgroundMusicPlayer play]; |
Aha — you don’t have a ViewController
in Mac OS X. Therefore, you’ll need to call this code from AppDelegate
instead.
Find the following line in AppDelegate.m of the SpriteKitSimpleGameMac group:
@implementation AppDelegate |
..and replace it with the following:
@import AVFoundation; @implementation AppDelegate { AVAudioPlayer *backgroundMusicPlayer; } |
Next, add the following code to the top of applicationDidFinishLaunching:
:
NSError *error; NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"]; backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error]; backgroundMusicPlayer.numberOfLoops = -1; [backgroundMusicPlayer prepareToPlay]; [backgroundMusicPlayer play]; |
Build and run your app; the music plays on!
You’ve resolved all of the bugs from the Mac conversion, but you still haven’t solved the issue of game controls in the Mac version of the game.
Using the Keyboard
The ninja’s movements in the iOS version of the app are controlled by tilting the device. These movements are processed by CoreMotion
, and the game loop calls updatePlayerWithTimeSinceLastUpdate:
to calculate the new player location for the current frame.
Responding to key presses requires a slightly different approach using the available methods to listen for keypress events.
Add the following code to updatePlayerWithTimeSinceLastUpdate:
in MyScene.m just before the #endif
statement:
#else -(void)keyDown:(NSEvent *)theEvent { } |
This updates to the method to respond to a keypress as well. Note that there’s a corresponding keyUp
to handle the release of the key to handle events that only last for the duration of the keypress.
You don’t want to respond to just any keypress; you can find out which key was pressed using the passed-in NSEvent
.
Add the following code between the curly braces of the keyDown:
method you just added:
-(void)keyDown:(NSEvent *)theEvent { NSString *keyPressed = [theEvent charactersIgnoringModifiers]; if ([keyPressed length] == 1) { NSLog(@"Key: %c",[keyPressed characterAtIndex:0]); } } |
Here you extract the pressed characters from the event without any modifier keys. This means key combinations like Command + S will be ignored. As well, you check that the keypress is only one character in length to filter out any other unwanted events. You’ll dump the key pressed out to the console.
Build and run your project; press a few keys while the game is running and you’ll see the keys pressed show up in the debug area, similar to the following example:
Since you’ll use the up and down arrow keys to move your player sprite, press those keys and see what you get in the console:
Hmm, that looks a little odd. The arrow keys are part of the group known as function keys, so they don’t have a proper character representation. But don’t fret: there’s an easy way to detect when function keys are pressed.
NSEvent
is your best friend when it comes to managing keyboard and mouse inputs on the Mac. This tutorial merely introduces NSEvent
; it’s highly recommended that you check out the full NSEvent class reference.
For now, take a quick look at the section of NSEvent documentation that deals with the function keys enum. The keys you’re concerned with are NSUpArrowFunctionKey
and NSDownArrowFunctionKey
.
Go back to MyScene.m and find the keyDown:
method you just added.
Comment out the NSLog
statement and paste the following code immediately below that:
unichar charPressed = [keyPressed characterAtIndex:0]; switch (charPressed) { case NSUpArrowFunctionKey: [_player runAction:[SKAction moveByX:0.0f y:50.0f duration:0.3]]; break; case NSDownArrowFunctionKey: [_player runAction:[SKAction moveByX:0.0f y:-50.0f duration:0.3]]; break; default: break; } |
Here you store the pressed character as Unicode character and compare it to the up and down function keys. You then use an SKAction to move the character up and down accordingly.
Build and run your project; press the up and down arrow keys and you should see your character moving up and down the screen like so:
You’ve spent all this time modifying the game to be played on a Mac, but you still need to check that you haven’t affected any portion of the iOS version of the game!
Build and run your project using the iOS target and play around with it a bit to make sure none of the game functions have been affected by your changes.
Where to Go From Here?
You can grab the completed sample files for this project from here.
Now that you have a good understanding of the work required to convert your iOS targeted project to a iOS / Mac targeted project, you’ll definitely want to create any new Sprite Kit games with cross-platform capabilities right from the start. I have made a cross-platform Sprite Kit project starter template on Github that might be useful – you can clone and use for your own games.
To learn more about making games that work on both iOS and OS X (especially related to scene sizes, image sizes, and aspect ratios, and UIKit vs. Cocoa Touch considerations), check out the upcoming second edition of our book iOS Games by Tutorials, where we go into more detail.
If you have any comments or questions, feel free to join the discussion below!
How to Port Your Sprite Kit Game from iOS to OS X is a post from: Ray Wenderlich
The post How to Port Your Sprite Kit Game from iOS to OS X appeared first on Ray Wenderlich.