Sprite Kit Tutorial: Making a Universal App: Part 1

Nicholas Waynik
Whack this Mole!

Whack this Mole!

Note from Ray: Tutorial Team member Nick Waynik has ported this tutorial from Cocos2D to Sprite Kit as part of the iOS 7 Feast. We hope you enjoy!

In this tutorial, you’ll learn how to make your app universal so it works on both the iPhone and iPad, including retina display support.

Specifically, you will create a mole whacking game. Why? Because whacking moles is fun! :]

This tutorial builds on the following Sprite Kit tutorials:

If you have not reviewed these tutorials already (or have similar knowledge), I recommend you go through them first.

This is a two-part Sprite Kit tutorial series. In this first part, you’ll create the basics of the game – cute little moles popping out of holes. You’ll spend a lot of time thinking about how to organize the art and coordinates so that the game looks good on the iPhone, iPad, and Retina display – and be efficient too!

Planning the Art: Overview

Since you want this app to work on both the normal iPhone 3.5-inch, iPhone 4-inch (iPhone 5), and the iPad, you need to take some time to carefully plan out how the art is going to be set up before you proceed any further.

In order to understand how to properly size and set up the art, you need to understand these topics first:

  • Retina Display
  • 4-inch iPhone Display
  • iPad, iPhone, and Aspect Ratios

So let’s get started!

Retina Display

The difference between a non-retina display iPhone and a retina display iPhone is the retina display can show double the pixels. So (in landscape) instead of the display size being 480×320 px as it is on a non-retina iPhone, it’s 960×640 px on a retina display.

iPhone vs Retina Display

If you haven’t noticed, the iPad comes in non-retina and retina models as well. So the same concept of “double the pixels” also applies with the iPad. With the non-retina iPad the display size is 1024×768 px and with the retina display iPad the size is 2048×1536 px, double the size!

iPad vs Retina Display

“But wait a minute”, you may think, “wouldn’t doubling the number of pixels break all of the apps that were written assuming the 480×320 for the iPhone or 1024×768 for the iPad display size?” It would have, except when you specify sizes or coordinates in Sprite Kit, you’re actually setting these in when you specify frame sizes in UIKit, you’re actually setting the sizes in a unit called points, not pixels.

On a non-retina display iPhone or iPad, a point is defined to be exactly one pixel. But on a retina display iPhone/iPad, a point is defined to be two pixels. So when you specify a location as (10,10) in points, it will be (10,10) on a non-retina iPhone/iPad, and (20,20) on a retina display iPhone/iPad, so will appear to be at the same relative offset. Cool, eh?

When you’re using Apple’s controls or Core Graphics, Apple has already written the code to make things look nice and crisp on the Retina display.

The only trick comes into play when you use an image. Say you have a 200×200 image in an iPhone or iPad app. If you don’t do anything, on the Retina display it will just scale the image to be 2x larger – which won’t look that great because you aren’t taking advantage of the extra resolution available to you.

Zoomed image not saved in Retina resolution

So what you need to do is provide another version for all of your images: a normal version, and one that is double the size. If you name your image with double the size with an “@2x” extension, whenever you try to load an sprite with [SKSpriteNode spriteNodeWithImageNamed:…] or similar APIs, it will automatically load the @2x image instead on the Retina display.

So making a Sprite Kit game use the retina display is pretty easy – just add @2X images and you’re done for the most part.

4-inch iPhone Display

With the release of the iPhone 5, there are now more pixels on the screen. Yea for more Pixels! This is definitely a welcomed change that can benefit your games. In this tutorial you will keep it simple and just extend your grass background images to account for this extra space.

The screen pixel dimension is 1136×640 – a 16:9 aspect ratio. In keeping with the points concept mentioned above, the points dimension is 568×320.

iPad, iPhone, and Aspect Ratio

OK so dealing with the retina display is pretty easy, but what about making a universal app that runs on both the iPhone and iPad devices?

Well, it turns out that there is a very annoying thing about making a game that works on both the iPhone and the iPad – the aspect ratio between the devices is different!

The iPhone is 480×320 or 960×640 – a 1.5 aspect ratio. However, the iPad is 768×1024 or 1536×2048 – a 1.33 aspect ratio.

This means that if you have an image that fills up the entire background of the non-retina iPad (768×1024) and want to re-use it for the iPhone too, it’s not going to fit exactly. Say you scale it down so it fits the width of the iPhone (multiply by 0.9375): you’ll get 720×960, so there will be extra stuff to the side that will get cut off!

Aspect Ratio of iPhone vs. iPad

This makes things kind of annoying, because not only do you run into problems with background images, but the aspect ratio also makes it difficult to use the same coordinates across devices.

There are several strategies for how to deal with this, here are a few I’ve seen/heard/used (feel free to chime in with any of your own solutions in the comments):

  • Have a “playable area” in the middle of the screen that is the size of the 3.5-inch iPhone retina display (960×640). This will leave a little extra are around the edges – you can just cover that with a background and the player probably won’t even notice. This allows you to easily convert coordinates between devices and re-use. This is what we’ll be doing in this Sprite Kit tutorial.
  • You can make the iPad have a similar aspect ratio to the iPhone if you take 32 point margins on the left and right, and 64 point margins on the top and bottom with the “main content” centered inside 1024×768 points. If you make your content to fit within the 1024×768 points box, you can scale down images for each device from there.
  • Remember there is a difference between pixels and points. Start out by using images created for the iPad retina display (1536×2048 px). You can then resize the images by half to produce the images for the non-retina display iPad. This will ensure your images are sharp and crisp!

iOS Simulator Options

Here are the iOS simulators that run iOS 7:

  • iPhone Retina (3.5-inch) – iPhone 4 and 4S
  • iPhone Retina (4-inch) – iPhone 5, 5C, and 5S
  • iPad – iPad 1, 2, and Mini
  • iPad Retina – iPad 3 and 4

Note that there is no option for a non-retina iPhone. This is because no iPhones or iPod touches without a retina display are able to run iOS 7.

And since Sprite Kit was introduced in iOS 7, there is no iPhone/iPod Touch that will use the non-retina display art. However, you might as well include it anyway to be future proof.

Planning the Art: Conclusion

OK, so based on the above discussion, here is the plan for this Sprite Kit tutorial.

  • The art has been designed to be within a 960×640 “playable area”, used full-screen on retina-display iPhones, and centered for the 4-inch iPhone, iPad, and iPad Retina screens.
  • The art is available already scaled and structured inside texture atlas folders. The @2x identifier will be used for the iPad retina-display images.
  • Backgrounds are a special case because they always need to be fullscreen. They are made to fit the 1024×768 point size (iPad size), so the entire screen is filled. These images are also scaled down by half so they can be used on the 3.5-inch iPhone. Some of the background will be offscreen, but that doesn’t matter for this particular background because it’s close enough.
  • The 4-inch iPhone will utilize code to use the “-568” texture atlases, centering the “playable area.”
  • The iPad and iPad Retina will utilize code to use the “-ipad” texture atlases, convert coordinates to inside the “playable area”, use the appropriate font sizes, etc.

Go ahead and download the art for this Sprite Kit tutorial, made by the fabulous Vicki Wenderlich. Unzip the file and take a look at how things are set up:

  • There are three folders inside the TextureAtlases folder. Each of these folders contains artwork for each particular type of display (3.5-inch iPhone, 4-inch iPhone, and iPad).
  • The iPad texture atlas folders contain images sized for the non-retina and retina display iPads. They are the only ones that will contain @2x images.
  • In the “foreground” folder, there are two image for the foreground images (the lower part and the upper part). It’s split into two parts so you can place the mole in-between the lower and upper parts, to make him look like he’s going underground.
  • The 4-inch iPhone is another special case. For this we just had our artist create new foreground images to take advantage of the extra space. Thanks Vicki!
  • In the “background” folder, the background has the 1.33 aspect ratio of the iPad, but is actually half sized (512×384 and 1024×768). This is because the background barely shows (just through the three mole holes), so it’s not worth the cost of a large texture load. Instead a small texture is loaded and scaled up.
  • In the “sprites” folder, all sprites were sized to fit nicely within the 960×640 “playable area”. Note, there are a mole and two animations for him (the mole laughing, and the mole being hit).

Ok – enough background info – it’s time to get started!

Getting Started

Open up Xcode, select File > New > Project…, then select SpriteKit Game and click Next. Name the product WhackAMole, make sure Universal is selected for devices, and click Next. Choose a location to save your project and click Create.

When the project opens you should have the project file selected in the Project Navigator, if not select it. Make sure you have WhackAMole selected under the targets section, and the General tab selected at the top. In the Deployment Info section you will see checkboxes for Device Orientation. Since your game will only be displayed in the landscape orientation, make sure Landscape Left and Landscape Right are selected for both iPhone and iPad.

Orientation Settings Changes

There is one more change to make so that the orientations work properly. Open ViewController.m and replace the viewDidLoad method with the following viewWillLayoutSubviews: method.

- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
 
    // Configure the view.
    SKView * skView = (SKView *)self.view;
    if (!skView.scene) {
        skView.showsFPS = YES;
        skView.showsNodeCount = YES;
 
        // Create and configure the scene.
        SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
        scene.scaleMode = SKSceneScaleModeAspectFill;
 
        // Present the scene.
        [skView presentScene:scene];
    }
}

So why did you do this? View Controller views are loaded in portrait orientation by default, and are not guaranteed to be sized correctly when the viewDidLoad method is called. However the view’s size will be correct by the time the viewWillLayoutSubviews method is called. As you can see, most of the code is the same from the viewDidLoad method. The major thing to note is the if statement wrapped around the setup of skView.scene. You will need to check if skView.scene already exists, because the viewWillLayoutSubviews method to can be called more than once.

Texture Atlases

Setting up texture atlases is a very easy thing to do. The first thing you will need to do is create a folder with “.atlas” appended to the name. Next you copy the artwork to that folder. Then include this folder in your Xcode project and you’re done!

Now isn’t that simple? Xcode will generate the texture atlases from the images contained in the “.atlas” folders automagically for you when the app is compiled and ran.

Note: You must be conscious of the size of your images when adding them to your “.atlas” folder. If you add an image larger than 2048×2048 pixels you will get an error, because that is the maximum size of the auto-generated texture atlases.

To do this, find the artwork zip file you downloaded earlier. Inside that zip you will find a folder named TextureAtlases. This folder contains folders for the three types of devices the game will run on (iPad, iPhone, and WidescreeniPhone). Each of these three folders contain “.atlas” folders which have the various sized images for that device. Go ahead and drag the TextureAtlases folder into your project, and make sure to check the “Copy items into destination group’s folder (if needed) before clicking Finish.

Copy Artwork to Project

For this tutorial you will keep it simple by having a set of texture atlases for each type of device (iPhone 3.5-inch, iPhone 4-inch, and iPads). You could reuse some of the iPhone 3.5-inch texture atlases for the iPhone 4-inch, since the foreground “.atlas” folder is the only one that contains images sized specifically for the iPhone 4-inch device.

Setting the Background

To start off you will need to add a macro and a helper method before changing what’s displayed in the scene. Open up MyScene.m and at the top of the file paste the following line of code under the #import statement.

#define IS_WIDESCREEN ( fabs( ( double )[ [ UIScreen mainScreen ] bounds ].size.height - ( double )568 ) < DBL_EPSILON )

This macro will help determine if the device the app is running on has a 4-inch screen, and will be used in the helper method. If you would like to read the details about what this line of code does, then visit this StackOverflow post.

Next you are going to add a helper method to get the correct SKTextureAtlas for the device the app is being ran on. This method will accept a filename, add the correct identifier, and return a SKTextureAtlas.

- (SKTextureAtlas *)textureAtlasNamed:(NSString *)fileName
{
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
 
        if (IS_WIDESCREEN) {
            // iPhone Retina 4-inch
            fileName = [NSString stringWithFormat:@"%@-568", fileName];
        } else {
            // iPhone Retina 3.5-inch
            fileName = fileName;
        }
 
    } else {
        fileName = [NSString stringWithFormat:@"%@-ipad", fileName];
    }
 
    SKTextureAtlas *textureAtlas = [SKTextureAtlas atlasNamed:fileName];
 
    return textureAtlas;
}

What is this code doing?

  • Determine if the device is an iPhone.
  • If the device is an iPhone, then check if it is a 4-inch display using the IS_WIDESCREEN macro defined at the top of the file. If it is widescreen then add “-568” to the end of fileName.
  • If the device is an iPad or iPad Retina, then add “-iPad” to the end of fileName.
  • Create and return a new SKTextureAtlas based on fileName.

Next, find your initWithSize: method. Remove the six lines of code that set the background color and create the Hello World label, then replace those lines with the following:

// Add background
SKTextureAtlas *backgroundAtlas = [self textureAtlasNamed:@"background"];
SKSpriteNode *dirt = [SKSpriteNode spriteNodeWithTexture:[backgroundAtlas textureNamed:@"bg_dirt"]];
dirt.scale = 2.0;
dirt.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
dirt.zPosition = 0;
[self addChild:dirt];
 
// Add foreground
SKTextureAtlas *foregroundAtlas = [self textureAtlasNamed:@"foreground"];
SKSpriteNode *upper = [SKSpriteNode spriteNodeWithTexture:[foregroundAtlas textureNamed:@"grass_upper"]];
upper.anchorPoint = CGPointMake(0.5, 0.0);
upper.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
upper.zPosition = 1;
[self addChild:upper];
 
SKSpriteNode *lower = [SKSpriteNode spriteNodeWithTexture:[foregroundAtlas textureNamed:@"grass_lower"]];
lower.anchorPoint = CGPointMake(0.5, 1.0);
lower.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
lower.zPosition = 3;
[self addChild:lower];
 
// Add more here later...

OK let’s go over this section by section, since there is a good amount of new material here.

  • Add background. This section creates the background texture atlas using the helper method you previously created. Next it creates the dirt sprite from the texture found on the background texture atlas. Lastly, the sprite is scaled by two and added to the center of the scene. It does this because we made it smaller on purpose to conserve space.
  • Add foreground. This section is very similar to the background section, however the two textures for the foreground sprites are contained in the same texture atlas. As an easy way to place the image, it sets the anchor point to the middle/bottom for the top image, and the middle/top for the bottom image, and matches that anchor point up to the center of the screen. That way you don’t have to do any complicated math, and it shows up in the right place on all devices. Take note that part of the background will be off-screen for iPhones, but that is OK for this background and barely even noticeable. Also note that the images are added with different zPosition values, so the lower image appears on bottom.
  • SKSpriteNode’s zPosition property. This is used to determine what position each sprite is layered on top of each other in the scene. You can think of this as a cake you are looking down upon. The dirt sprite is at the very bottom, so you will use a smaller number. As you add another layer you will increase the number, so the upper foreground is set at 0 and the lower and topmost layer is set at 3. What happened to 2? This is reserved for the moles because they should appear above the upper but below the lower sprites.

Before you run the app, you should do a little bit more cleanup. Find the touchesBegan: method and remove it. This method creates a spaceship sprite at the screen location you touch, and besides it seems kind of odd to have spaceships in a mole whacking game!

Compile and run the code, and you should now see the background and foreground on the screen! Give it a try on the iPhone and iPad simulators to make sure that it appears OK on all of the devices.

Mole Background

Placing the Moles

For this game, you’re going to add three moles to the scene – one for each hole. The moles will usually be “underground” beneath the lower part of the grass – but occasionally they will “pop up” so you can try to whack them.

First, let’s add the moles to the level underneath each of the holes. You’ll temporarily make them appear above all the other art so you can make sure they’re in the right spot, then we’ll put them underground once we’re happy with their position.

Open up MyScene.h and change the code to look like the following:

#import <SpriteKit/SpriteKit.h>
 
@interface MyScene : SKScene
 
@property (strong, nonatomic) NSMutableArray *moles;
@property (strong, nonatomic) SKTexture *moleTexture;
 
@end

In this code you add a SKTexture and an array. The SKTexture will be used when creating the mole sprites. Each on of the moles you create will be added to the moles array, it will make it easy to loop through each of the moles later on.

Before you add the moles, scroll to the top of MyScene.m and add the following line of code above “@implementation MyScene”.

const float kMoleHoleOffset = 155.0;

This is a constant floating point variable used to position the moles.

Next, add the code to place the moles at the end of your initWithSize: method (where it says “Add more here later…”), as shown below:

// Load sprites
self.moles = [[NSMutableArray alloc] init];
SKTextureAtlas *spriteAtlas = [self textureAtlasNamed:@"sprites"];
self.moleTexture = [spriteAtlas textureNamed:@"mole_1.png"];
 
 
float center = 240.0;
 
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && IS_WIDESCREEN) {
    center = 284.0;
}
 
SKSpriteNode *mole1 = [SKSpriteNode spriteNodeWithTexture:self.moleTexture];
mole1.position = [self convertPoint:CGPointMake(center - kMoleHoleOffset, 85.0)];
mole1.zPosition = 999;
mole1.name = @"Mole";
mole1.userData = [[NSMutableDictionary alloc] init];
[self addChild:mole1];
[self.moles addObject:mole1];
 
SKSpriteNode *mole2 = [SKSpriteNode spriteNodeWithTexture:self.moleTexture];
mole2.position = [self convertPoint:CGPointMake(center, 85.0)];
mole2.zPosition = 999;
mole2.name = @"Mole";
mole2.userData = [[NSMutableDictionary alloc] init];
[self addChild:mole2];
[self.moles addObject:mole2];
 
SKSpriteNode *mole3 = [SKSpriteNode spriteNodeWithTexture:self.moleTexture];
mole3.position = [self convertPoint:CGPointMake(center + kMoleHoleOffset, 85.0)];
mole3.zPosition = 999;
mole3.name = @"Mole";
mole3.userData = [[NSMutableDictionary alloc] init];
[self addChild:mole3];
[self.moles addObject:mole3];

This first creates and loads a SKTextureAtlas for the sprites. Next it creates a SKTexture of the mole_1.png image found in the sprites texture atlas. The texture of the mole will be used when creating all three moles. Texture reuse is done to allow Sprite Kit to process and render sprites more efficiently.

Next a value for center is set. If the device is an iPhone 4-inch then the value for center will reflect the extra screen size.

Then it goes through and creates a sprite for each mole, places them in the scene, and adds them to the array of moles. Note the coordinate for each mole is determined from the center position using the constant variable defined at the top of the file. The moles are within the 480×320 “playable area” of the game (the size of the iPhone 3.5-inch). For the iPad, these points will need to be converted, so it calls a helper function convertPoint which we’ll write next.

Add the following method right below the initWithSize: method:

- (CGPoint)convertPoint:(CGPoint)point
{
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        return CGPointMake(32 + point.x*2, 64 + point.y*2);
    } else {
        return point;
    }
}

This method converts a point in the “playable area” to the appropriate screen position on the iPad. Remember that:

  • The iPad has a larger screen size, so all points are doubled.
  • You’re centering that 960×640 area in the 1024×768 iPad screen, so that leaves 32 point margins on the left and right, and 64 point margins on the top and bottom.

So this method simply does that math to give the right position on the iPad.

Compile and run your code, and you should see the three moles happily in the scene at the correct spots! You should try the code on the iPhone 3.5-inch, iPhone 4-inch, iPad, and iPad Retina to make sure that they’re in the right spot on each device.

Moles placed in correct positions

Popping the Moles

Now that we’re sure the moles are in the right place, let’s add the code to make them pop out of their holes.

First things first – switch the zPosition of 999 for the mole sprites back to 2 so the moles are underground.

Once that’s done, add the following code to your update: method:

for (SKSpriteNode *mole in self.moles) {
    if (arc4random() % 3 == 0) {
        if (!mole.hasActions) {
            [self popMole:mole];
        }
    }
}

If you haven’t seen this before, the update method is called once per frame. You want to try popping some moles out of their holes every time the method is called. The new code will loop through each mole and give it a 1 in 3 chance of popping out of its hole. But it will only pop out if it isn’t moving already – and one easy way to check for this is to check the return value of the sprite’s hasActions method. If the sprite has running actions then hasActions will return YES.
Next, add the implementation of popMoles:

- (void)popMole:(SKSpriteNode *)mole
{
	SKAction *easeMoveUp = [SKAction moveToY:mole.position.y + mole.size.height duration:0.2f];
    	easeMoveUp.timingMode = SKActionTimingEaseInEaseOut;
    	SKAction *easeMoveDown = [SKAction moveToY:mole.position.y duration:0.2f];
    	easeMoveDown.timingMode = SKActionTimingEaseInEaseOut;
    	SKAction *delay = [SKAction waitForDuration:0.5f];
 
    	SKAction *sequence = [SKAction sequence:@[easeMoveUp, delay, easeMoveDown]];
    	[mole runAction:sequence];
}

This code uses some Sprite Kit actions to make the mole pop out of it’s hole, pause for half a second, then pop back down. Let’s go through this line-by-line to make sure we’re on the same page:

  1. Creates an action to move the mole move up along the Y axis as much as the mole is tall. Since you placed the mole right below the hole, it will look right.
  2. To make the movement look more natural, it sets the timingMode of the action to SKActionTimingEaseInEaseOut. This causes the action to go slower at the beginning and end, as if the mole is accelerating/decelerating, as it naturally would.
  3. To create an action to move the mole move back down again, create an action similar to the one used to move the mole up except use the mole’s current y-axis position.
  4. Creates an action to pause for one second after the mole pops out.
  5. Now that the actions are ready to go, it runs them on the mole in a sequence: move up, delay, and finally move down.

That’s it! Compile and run the code, and you’ll see the moles happily popping out of their holes!

Moles popping out of holes

Where To Go From Here?

Here is a sample project with all of the code we’ve developed so far in this Sprite Kit tutorial series.

Next check out Part 2, where you’ll add some cute animations to the mole as he laughs and gets whacked, add gameplay so you can do the whacking and earn points, and of course add some gratuitous sound effects as usual.

Please add a comment below in if you have any thoughts, advice, or suggestions for future tutorials!

Nicholas Waynik

Nicholas Waynik is an independent iOS developer, and has done everything from network administration to web development. He started writing iOS apps when the iPhone SDK was first released. Since then he has gone on to start his own business focusing on iOS development. He loves spending his free time with his family, and sometimes playing golf.

He can be found on Twitter as @n_dubbs, or at his website: http://www.nicholaswaynik.com.

Other Items of Interest

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

Swift Team

... 16 total!

iOS Team

... 30 total!

Android Team

... 15 total!

macOS Team

... 11 total!

Apple Game Frameworks Team

... 10 total!

Unity Team

... 11 total!

Articles Team

... 11 total!

Resident Authors Team

... 11 total!