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

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:

  1. Create a rectangular physics body the same size as the ship.
  2. Make the shape dynamic; this makes it subject to things such as collisions and other outside forces.
  3. You don’t want the ship to drop off the bottom of the screen, so you indicate that it’s not affected by gravity.
  4. 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.

Ship Moving with Accelerometer

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!

Adding asteroids to dodge

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!

  1. Pick up a laser from one of your pre-made lasers.
  2. Set the initial position of the laser to where your ship is positioned.
  3. 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
  4. Define a done action using a block that hides the laser when it hits the right edge.
  5. Define a sequence action of the move and done actions
  6. Run the sequence on the laser sprite

Build and run your code, and now you can go fire your shipboard laser!

Shooting lasers from ship

Tony Dahbura

Contributors

Tony Dahbura

Author

Over 300 content creators. Join our team.