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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Sprite Kit Tutorial: Space Shooter
40 mins
- Create the Project
- Adding Resources
- Setting up your Project
- Switching to Landscape Orientation
- Adding a Space Ship
- Adding Parallax Scrolling
- Adding Stars
- Moving the Ship with the Accelerometer
- Pulling accelerometer data from your code
- Positioning the ship via Physics
- Adding Asteroids
- Shooting Lasers
- Basic Collision Detection
- Win/Lose Detection
- Gratuitous Music and Sound Effects
- Where To Go From Here?
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!
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!