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?
Positioning the ship via Physics
Next is making the ship move…Sprite Kit includes a a great capability called a physics engine.
Hold on a minute…No one said anything about getting involved with physics! Well, whenever you discuss outer space, someone always brings up physics. Anyways, the physics you are going to use is way easier than your physics book :]
Sprite Kit’s built-in physics system is based on Box 2D and can simulate a wide range of physics like forces, translation, rotation, collisions, and contact detection. Each SKNode (which includes SKScenes and SKSpriteNodes) has a SKPhysicsBody attached to it. This SKPhysicsBody represents that node in the physics simulation.
Right after the _ship.position = CGPointMake(self.frame.size.width * 0.1, CGRectGetMidY(self.frame));
line in your initWithSize
method, add:
//move the ship using Sprite Kit's Physics Engine
//1
_ship.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_ship.frame.size];
//2
_ship.physicsBody.dynamic = YES;
//3
_ship.physicsBody.affectedByGravity = NO;
//4
_ship.physicsBody.mass = 0.02;
These lines do the following:
- Create a rectangular physics body the same size as the ship.
- Make the shape dynamic; this makes it subject to things such as collisions and other outside forces.
- You don’t want the ship to drop off the bottom of the screen, so you indicate that it’s not affected by gravity.
- Give the ship an arbitrary mass so that its movement feels natural.
You are basically defining a rectangular physics body around the ship.
Because you do not want your ship to slide off the top and bottom of the galaxy (the screen) while you move it, you must also define a edge loop around the boundary of your screen. This is like a wall around the screen.
At the top of the initWithSize
method below the line self.backgroundColor = [SKColor blackColor];
add:
//Define your physics body around the screen - used by your ship to not bounce off the screen
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
Now you need to get the console output to actually move the ship for you. In the updateShipPositionFromMotionManager
method, replace the NSLog
statement with:
[_ship.physicsBody applyForce:CGVectorMake(0.0, 40.0 * data.acceleration.x)];
The applies a force 40.0*data.acceleration.x
to the ship’s physics body in the y direction. The number 40.0 is an arbitrary value to make the ship’s motion feel more natural.
Build and run the app on your device and move your device up and down to see your ship move. You can play around with the 40.0 number to get it where you like the settings.
Adding Asteroids
The game is looking good so far, but where’s the danger and excitement? Let’s spice things up by adding some wild asteroids to the scene!
The approach you’re going to take is every so often, you’ll create an asteroid offscreen to the right. Then you’ll run a Sprite Kit action to move it to the left of the screen.
You could simply create a new asteroid every time you needed to spawn, but allocating memory is a slow operation and it’s best when you can avoid it. So you’ll pre-allocate memory for a bunch of asteroids, and simply grab the next available asteroid when needed.
OK, let’s see what this looks like. At the top of the MyScene.m
file, below your imports add:
#define kNumAsteroids 15
Now, inside the @implementation
section underneath your _motionManager
variable, add:
NSMutableArray *_asteroids;
int _nextAsteroid;
double _nextAsteroidSpawn;
Inside your initWithSize
method underneath the line #pragma mark - TBD - Setup the asteroids
add:
_asteroids = [[NSMutableArray alloc] initWithCapacity:kNumAsteroids];
for (int i = 0; i < kNumAsteroids; ++i) {
SKSpriteNode *asteroid = [SKSpriteNode spriteNodeWithImageNamed:@"asteroid"];
asteroid.hidden = YES;
[asteroid setXScale:0.5];
[asteroid setYScale:0.5];
[_asteroids addObject:asteroid];
[self addChild:asteroid];
}
The above code adds all kNumAsteroids
asteroids to the array as soon as the game starts, but sets them all to invisible. If they're invisible you'll treat them as inactive.
Above your update
method, add the following:
- (float)randomValueBetween:(float)low andValue:(float)high {
return (((float) arc4random() / 0xFFFFFFFFu) * (high - low)) + low;
}
At the end of your update
method, under the [self updateShipPositionFromMotionManager];
line, add:
double curTime = CACurrentMediaTime();
if (curTime > _nextAsteroidSpawn) {
//NSLog(@"spawning new asteroid");
float randSecs = [self randomValueBetween:0.20 andValue:1.0];
_nextAsteroidSpawn = randSecs + curTime;
float randY = [self randomValueBetween:0.0 andValue:self.frame.size.height];
float randDuration = [self randomValueBetween:2.0 andValue:10.0];
SKSpriteNode *asteroid = [_asteroids objectAtIndex:_nextAsteroid];
_nextAsteroid++;
if (_nextAsteroid >= _asteroids.count) {
_nextAsteroid = 0;
}
[asteroid removeAllActions];
asteroid.position = CGPointMake(self.frame.size.width+asteroid.size.width/2, randY);
asteroid.hidden = NO;
CGPoint location = CGPointMake(-self.frame.size.width-asteroid.size.width, randY);
SKAction *moveAction = [SKAction moveTo:location duration:randDuration];
SKAction *doneAction = [SKAction runBlock:(dispatch_block_t)^() {
//NSLog(@"Animation Completed");
asteroid.hidden = YES;
}];
SKAction *moveAsteroidActionWithDone = [SKAction sequence:@[moveAction, doneAction ]];
[asteroid runAction:moveAsteroidActionWithDone withKey:@"asteroidMoving"];
}
Some things worth mentioning in the above code:
- The instance variable
_nextAsteroidSpawn
tells when to spawn an asteroid next. You always check this in the update loop. - If you're new to Sprite Kit actions, they are easy ways to get sprites to do things over time, such as move, scale, rotate, etc. Here you perform a sequence of two actions: move to the left a good bit, then call a method that will set the asteroid to invisible again. This is an example of a sequential action, one must complete before the next begins.
- The asteroids move from some x position off to the right and random Y position towards the left of the screen (towards where your ship is!) at a random speed.
At the top of the startTheGame
method, add:
_nextAsteroidSpawn = 0;
for (SKSpriteNode *asteroid in _asteroids) {
asteroid.hidden = YES;
}
Build and run your app, and now you have some asteroids flying across the screen!
Shooting Lasers
Not sure about you about you, but the first thing I think of when I see asteroids flying at me is SHOOT THEM!
So let's take care of this urge by adding the ability to fire lasers! This code will be similar to how you added asteroids, because you'll create an array of reusable laser beams and move them across the screen with actions based on when you tap to fire.
The main difference is we'll be using touch handling to detect when to shoot.
Underneath your #define kNumAsteroids
line, add:
#define kNumLasers 5
Inside the @implementation
section underneath your other variables, add:
NSMutableArray *_shipLasers;
int _nextShipLaser;
Inside your initWithSize
method underneath the line #pragma mark - TBD - Setup the lasers
add:
_shipLasers = [[NSMutableArray alloc] initWithCapacity:kNumLasers];
for (int i = 0; i < kNumLasers; ++i) {
SKSpriteNode *shipLaser = [SKSpriteNode spriteNodeWithImageNamed:@"laserbeam_blue"];
shipLaser.hidden = YES;
[_shipLasers addObject:shipLaser];
[self addChild:shipLaser];
}
In the startTheGame
method before the line [self startMonitoringAcceleration];
, add:
for (SKSpriteNode *laser in _shipLasers) {
laser.hidden = YES;
}
Finally, you need to detect touches to fire your laser. Add this new touchesBegan
method:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Called when a touch begins */
//1
SKSpriteNode *shipLaser = [_shipLasers objectAtIndex:_nextShipLaser];
_nextShipLaser++;
if (_nextShipLaser >= _shipLasers.count) {
_nextShipLaser = 0;
}
//2
shipLaser.position = CGPointMake(_ship.position.x+shipLaser.size.width/2,_ship.position.y+0);
shipLaser.hidden = NO;
[shipLaser removeAllActions];
//3
CGPoint location = CGPointMake(self.frame.size.width, _ship.position.y);
SKAction *laserMoveAction = [SKAction moveTo:location duration:0.5];
//4
SKAction *laserDoneAction = [SKAction runBlock:(dispatch_block_t)^() {
//NSLog(@"Animation Completed");
shipLaser.hidden = YES;
}];
//5
SKAction *moveLaserActionWithDone = [SKAction sequence:@[laserMoveAction,laserDoneAction]];
//6
[shipLaser runAction:moveLaserActionWithDone withKey:@"laserFired"];
}
This shows you how easy it is to receive touch events in Sprite Kit. It's identical to receiving them in a standard iOS application!
- Pick up a laser from one of your pre-made lasers.
- Set the initial position of the laser to where your ship is positioned.
- Set the end position off screen (X) and at the same Y position as it started. Define a move action to move to the edge of the screen from the initial position with a duration of a 1/2 second
- Define a done action using a block that hides the laser when it hits the right edge.
- Define a sequence action of the move and done actions
- Run the sequence on the laser sprite
Build and run your code, and now you can go fire your shipboard laser!