Sprite Kit Tutorial: Making a Universal App: Part 1

Learn how to make a universal app that works on the iPhone, iPad, and retina display in this Sprite Kit tutorial! By Nicholas Waynik.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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 480x320 "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 960x640 area in the 1024x768 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

Nicholas Waynik

Contributors

Nicholas Waynik

Author

Over 300 content creators. Join our team.