Sprite Kit Tutorial: Space Shooter

A Sprite Kit tutorial that teaches you how to make a space shooter game. In the process, you’ll learn about accelerometers, textures, and scrolling too! By Tony Dahbura.

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

Basic Collision Detection

So far things look like a game, but don't act like a game, because nothing blows up! It's time to add some violence into this game!

Starting in the @implementation variables block underneath the _nextShipLaser declarataion, add:

int _lives;

At the bottom of the update method, add:

//check for laser collision with asteroid
for (SKSpriteNode *asteroid in _asteroids) {
    if (asteroid.hidden) {
        continue;
    }
    for (SKSpriteNode *shipLaser in _shipLasers) {
        if (shipLaser.hidden) {
            continue;
        }
            
        if ([shipLaser intersectsNode:asteroid]) {
            shipLaser.hidden = YES;
            asteroid.hidden = YES;

            NSLog(@"you just destroyed an asteroid");
            continue;
        }
    }
    if ([_ship intersectsNode:asteroid]) {
            asteroid.hidden = YES;
            SKAction *blink = [SKAction sequence:@[[SKAction fadeOutWithDuration:0.1],
                                                   [SKAction fadeInWithDuration:0.1]]];
            SKAction *blinkForTime = [SKAction repeatAction:blink count:4];
            [_ship runAction:blinkForTime];
            _lives--;
            NSLog(@"your ship has been hit!");
        }
    }

This is a very basic method of collision detection that just checks the bounding box of the sprites to see if they collide. Note that this counts transparency, so it isn't a perfect way of checking for collisions, but it's good enough for a simple game like this.

The first thing that happens is the for loop goes through each of the asteroids. If they are hidden it skips to the next asteroid. Once it gets an asteroid that is not hidden it checks to see if a laser has intersected with it (again making sure the laser is not hidden).

If there is an intersect (the laser has hit the asteroid) both are hidden and the check continues to the next asteroid. If the check with the laser fails then a similar check is done against your ship.

The ship indicates it has been hit by the blink action that is repeated 4 times. If a hit happens the lives are reduced.

You are checking if the laser has hit the asteroid first, and clearing it before the check underneath can see if the ship has been hit. In this regard you are giving the player a little better chance!

Build and run your code, and now things should blow up!

Win/Lose Detection

You're almost done - just need to add a way for the player to win or lose!

In this game, the player wins if he survives for 30 seconds, and loses if he gets hit 3 times.

Start by making the following changes to MyScene.m:

Before your @implementation line, underneath the #defines add the following:

typedef enum {
    kEndReasonWin,
    kEndReasonLose
} EndReason;

Underneath your _lives variable declaration, add:

double _gameOverTime;
bool _gameOver;

At the top of the startTheGame method, add:

_lives = 3;
double curTime = CACurrentMediaTime();
_gameOverTime = curTime + 30.0;
_gameOver = NO;

At the end of the update method, add:

// Add at end of update loop
if (_lives <= 0) {
    NSLog(@"you lose...");
    [self endTheScene:kEndReasonLose];
} else if (curTime >= _gameOverTime) {
    NSLog(@"you won...");
    [self endTheScene:kEndReasonWin];
}

Underneath the update method, add this new method:

- (void)endTheScene:(EndReason)endReason {
    if (_gameOver) {
        return;
    }
    
    [self removeAllActions];
    [self stopMonitoringAcceleration];
    _ship.hidden = YES;
    _gameOver = YES;
    
    NSString *message;
    if (endReason == kEndReasonWin) {
        message = @"You win!";
    } else if (endReason == kEndReasonLose) {
        message = @"You lost!";
    }
    
    SKLabelNode *label;
    label = [[SKLabelNode alloc] initWithFontNamed:@"Futura-CondensedMedium"];
    label.name = @"winLoseLabel";
    label.text = message;
    label.scale = 0.1;
    label.position = CGPointMake(self.frame.size.width/2, self.frame.size.height * 0.6);
    label.fontColor = [SKColor yellowColor];
    [self addChild:label];
    
    SKLabelNode *restartLabel;
    restartLabel = [[SKLabelNode alloc] initWithFontNamed:@"Futura-CondensedMedium"];
    restartLabel.name = @"restartLabel";
    restartLabel.text = @"Play Again?";
    restartLabel.scale = 0.5;
    restartLabel.position = CGPointMake(self.frame.size.width/2, self.frame.size.height * 0.4);
    restartLabel.fontColor = [SKColor yellowColor];
    [self addChild:restartLabel];
    
    SKAction *labelScaleAction = [SKAction scaleTo:1.0 duration:0.5];
    
    [restartLabel runAction:labelScaleAction];
    [label runAction:labelScaleAction];
    
}

Don't worry if you don't understand how the endTheScene method works - it's some code used for a bunch of games for a quick win/lose menu text on the screen that uses the Sprite Kit SKLabelNode. SKLabelNodes are just like the sprites you've been using, but allow displaying text.

Finally, at the top of your touchesBegan method, add:

//check if they touched your Restart Label
for (UITouch *touch in touches) {
    SKNode *n = [self nodeAtPoint:[touch locationInNode:self]];
    if (n != self && [n.name isEqual: @"restartLabel"]) {
        [[self childNodeWithName:@"restartLabel"] removeFromParent];
        [[self childNodeWithName:@"winLoseLabel"] removeFromParent];
        [self startTheGame];
        return;
    }
}

//do not process anymore touches since it's game over
if (_gameOver) {
    return;
}

The additional code added to the touchesBegan method checks if the player has tapped the node for wanting to play again. This uses a very handy Sprite Kit method called nodeAtPoint. When you detect a tap on this label you clear out the labels and call the startTheGame method to replay.

Build and run the code, and see if you can lose!

Adding game over detection

Gratuitous Music and Sound Effects

As you know, this game needs some awesome sound effects to make it complete. There were some sounds as part of the resources you downloaded as well as some cool outer space type background music.

You just need a bit of code to play these sounds in the right places. In MyScene.m file, make the following changes:

//Add to top of file underneath @import CoreMotion;
@import AVFoundation;

//Add the following variable underneath the bool _gameOver; declaration
AVAudioPlayer *_backgroundAudioPlayer;

//Add above [self startTheGame] in initWithSize
[self startBackgroundMusic];

Add the following method underneath the updateShipPositionFromMotionManager:

- (void)startBackgroundMusic
{
    NSError *err;
    NSURL *file = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"SpaceGame.caf" ofType:nil]];
    _backgroundAudioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:file error:&err];
    if (err) {
        NSLog(@"error in audio play %@",[err userInfo]);
        return;
    }
    [_backgroundAudioPlayer prepareToPlay];
    
    // this will play the music infinitely
    _backgroundAudioPlayer.numberOfLoops = -1;
    [_backgroundAudioPlayer setVolume:1.0];
    [_backgroundAudioPlayer play];
}

The above method uses the AVAudioPlayer to play the background music continuously during the game.

Now you need to get the explosions and firing working. There are three areas for sounds: when the laser fires, when an asteroid is hit, and when your ship gets destroyed.

In the touchesBegan method, above the SKAction *laserMoveAction = [SKAction moveTo:location duration:0.5]; line add:

SKAction *laserFireSoundAction = [SKAction playSoundFileNamed:@"laser_ship.caf" waitForCompletion:NO];

Replace the SKAction *moveLaserActionWithDone line with:

SKAction *moveLaserActionWithDone = [SKAction sequence:@[laserFireSoundAction, laserMoveAction,laserDoneAction]];

The above line adds one more sequence to your existing laserFire action.

The next area to fix is the asteroids being destroyed by lasers.

Inside the update method add the following inside the if ([shipLaser intersectsNode:asteroid]) { block above the shipLaser.hidden = YES; line, add:

SKAction *asteroidExplosionSound = [SKAction playSoundFileNamed:@"explosion_small.caf" waitForCompletion:NO];
[asteroid runAction:asteroidExplosionSound];

The last area to fix is when your ship gets destroyed.

In the update method replace the [_ship runAction:blinkForTime]; line with:

 SKAction *shipExplosionSound = [SKAction playSoundFileNamed:@"explosion_large.caf" waitForCompletion:NO];
 [_ship runAction:[SKAction sequence:@[shipExplosionSound,blinkForTime]]];

The above lines create a new sound action and insert a Sprite Kit sequence of actions, replacing the single blinkAction, with a sound effect and then the blink.

Does the sound being a sequence mean the ship will delay blinking till the sound finishes?
[spoiler title="Tell Me!"]The answer is no, the SKAction that plays the sound set the waitForCompletion flag to NO[/spoiler]

Although Sprite Kit can play music as an action, it is better to utilize the AVAudioPlayer for longer playing stuff like background music.

And that's it - congratulations, you've made a complete space game for the iPhone from scratch!

You Win!

Tony Dahbura

Contributors

Tony Dahbura

Author

Over 300 content creators. Join our team.