Sprite Kit Tutorial: Making a Universal App: Part 2

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 2 of 3 of this article. Click here to view the first page.

Adding Game Logic

We’re now going to add the gameplay logic into the game. The idea is a certain number of moles will appear, and you get points for each one you whack. You try to get the most number of points you can.

So we’ll need to keep track of the score, and also display it to the user. And when the moles are finished popping, we’ll need to tell the user about that as well.

So start by opening MyScene.h, and add the following instance variables under the actions you added earlier:

@property (strong, nonatomic) SKLabelNode *scoreLabel;
@property (nonatomic) NSInteger score;
@property (nonatomic) NSInteger totalSpawns;
@property (nonatomic) BOOL gameOver;

These will keep track of the score label, the current score, the number of moles popped so far, and whether the game is over or not.

Next add the following to the end of your initWithSize: method in MyScene.m:

// Add score label
float margin = 10;

self.scoreLabel = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
self.scoreLabel.text = @"Score: 0";
self.scoreLabel.fontSize = [self convertFontSize:14];
self.scoreLabel.zPosition = 4;
self.scoreLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
self.scoreLabel.position = CGPointMake(margin, margin);
[self addChild:self.scoreLabel];

This code creates a label to show the score. The label is placed in the lower left corner of the screen by using the margin of 10 points from the left and bottom. Setting the label’s horizontalAlignmentMode property to SKLabelHorizontalAlignmentModeLeft will position the label’s text so that the left side of the text is on the node’s origin.

Also note that rather than passing the font size directly, it goes through a helper function to convert the font size first. This is because the font size will need to be larger on the iPad and iPad Retina, since it has a bigger screen. So implement convertFontSize next as the following:

- (float)convertFontSize:(float)fontSize
{
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        return fontSize * 2;
    } else {
        return fontSize;
    }
}

This is very simple – on the iPad and iPad Retina the font size is doubled, otherwise it’s left alone.

Next we want to add the touch detection code to see if a touch has hit a mole. But before we can do that, we need to add a flag to the mole to the game knows whether the mole is currently tappable or not. The mole should only be able to be tapped while it’s laughing – while it’s moving or underground it’s “safe.”

We could create a subclass of SKSpriteNode for the mole to keep track of this, but because we only need to store this one piece of information, we’ll use the userData property on the SKSpriteNode instead. So modify popMole one more time to the following:

- (void)popMole:(SKSpriteNode *)mole
{
    if (self.totalSpawns > 50) return;
    self.totalSpawns++;
    
    // Reset texture of mole sprite
    mole.texture = self.moleTexture;
    
	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 *setTappable = [SKAction runBlock:^{
        [mole.userData setObject:@1 forKey:@"tappable"];
    }];
    
    SKAction *unsetTappable = [SKAction runBlock:^{
        [mole.userData setObject:@0 forKey:@"tappable"];
    }];
    
    
    SKAction *sequence = [SKAction sequence:@[easeMoveUp, setTappable, self.laughAnimation, unsetTappable, easeMoveDown]];
    [mole runAction:sequence completion:^{
        [mole removeAllActions];
    }];
}

The changes to popMole are as follows:

  • The method immediately returns if there has been 50 or more spawns, since 50 is the limit for this game.
  • It next resets the display frame of the sprite to the base image (“mole_1.png”) at the beginning of the method, since if the mole was hit last time, it will still be showing the “hit” image and will need to be reset.
  • Right before the mole laughs, it runs an action to run code specified in a block. This block sets a userData dictionary object named tappable to an NSNumber of 1, which you’ll use to indicate whether the mole is tappable.
  • Similarly, after the mole laughs, it runs code specified in the block for the unsetTappable action, which sets the tappable flag back to 0.

Ok, now that the sprite has a userData flag indicating whether it can be tapped or not, you can finally add the touchesBegan: method back to your code. Add the following to the end of your method:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint touchLocation = [touch locationInNode:self];
    
    SKNode *node = [self nodeAtPoint:touchLocation];
    if ([node.name isEqualToString:@"Mole"]) {
        SKSpriteNode *mole = (SKSpriteNode *)node;
        
        if (![[mole.userData objectForKey:@"tappable"] boolValue]) return;
        
        self.score += 10;
        
        [mole.userData setObject:@0 forKey:@"tappable"];
        [mole removeAllActions];

        SKAction *easeMoveDown = [SKAction moveToY:(mole.position.y - mole.size.height) duration:0.2f];
        easeMoveDown.timingMode = SKActionTimingEaseInEaseOut;
        
        // Slow down the animation by half
        easeMoveDown.speed = 0.5;
        
        SKAction *sequence = [SKAction sequence:@[self.hitAnimation, easeMoveDown]];
        [mole runAction:sequence];
    }
}

The touchesBegan:method get the location of a touch, then it finds the SKNode at the touch location. If the node’s name is equal to Mole, it proceeds to check if the sprite is tappable.

If the mole is hit, it sets the mole as no longer tappable, and increases the score. It then stops any running actions, plays the “hit” animation, and moves the mole immediately back down the hole.

One final step – add some code to update the score and check for the level complete condition at the beginning of update:

if (self.gameOver) return;

if (self.totalSpawns >= 50) {
    
    SKLabelNode *gameOverLabel = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
    gameOverLabel.text = @"Level Complete!";
    gameOverLabel.fontSize = 48;
    gameOverLabel.zPosition = 4;
    gameOverLabel.position = CGPointMake(CGRectGetMidX(self.frame),
                                         CGRectGetMidY(self.frame));
    
    [gameOverLabel setScale:0.1];
    
    [self addChild:gameOverLabel];
    [gameOverLabel runAction:[SKAction scaleTo:1.0 duration:0.5]];
    
    self.gameOver = YES;
    return;
}

[self.scoreLabel setText:[NSString stringWithFormat:@"Score: %d", self.score]];

That’s it! Compile and run your code, and you should be able to whack moles and increase your score! How high of a score can you get?

Showing the score in the game

Gratuitous Sound Effects

As usual, let’s add even more fun to the game with some zany sound effects. Download these sound effects Ray made with Garage Band and Audacity, unzip the file, and drag the sounds to your WhackAMole folder. Make sure that “Copy items into destination group’s folder” is selected, and click Finish.
Add the following import statement to the top of MyScene.h:

#import <AVFoundation/AVFoundation.h>

Now add the following properties before @end:

@property (strong, nonatomic) AVAudioPlayer *audioPlayer;
@property (strong, nonatomic) SKAction *laughSound;
@property (strong, nonatomic) SKAction *owSound;

Then make the following changes to MyScene.m:

// Add at the bottom of your initWithSize: method
// Preload whack sound effect
self.laughSound = [SKAction playSoundFileNamed:@"laugh.caf" waitForCompletion:NO];
self.owSound = [SKAction playSoundFileNamed:@"ow.caf" waitForCompletion:NO];

NSURL *url = [[NSBundle mainBundle] URLForResource:@"whack" withExtension:@"caf"];
NSError *error = nil;
self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];

if (!self.audioPlayer) {
    NSLog(@"Error creating player: %@", error);
}

[self.audioPlayer play];

// Add at bottom of popMole method, change the sequence action to:
SKAction *sequence = [SKAction sequence:@[easeMoveUp, setTappable, self.laughSound, self.laughAnimation, unsetTappable, easeMoveDown]];

// Add inside touchesBegan: method, change the sequence action to:
SKAction *sequence = [SKAction sequence:@[self.owSound, self.hitAnimation, easeMoveDown]];

Compile and run your code, and enjoy the groovy tunes!

Nicholas Waynik

Contributors

Nicholas Waynik

Author

Over 300 content creators. Join our team.