Cocos2D Tutorial for iOS: How To Make A Space Shooter iPhone Game

Ray Wenderlich

This post is also available in: Japanese

Make a Space Shooter iPhone game!

Make a Space Shooter iPhone game!

In this Cocos2D tutorial, you’ll learn how to make a space shooter game for the iPhone!

You’ll pilot a space ship with the accelerometer, and blast your way through a field of dangerous asteroids with your trusty laser gun!

If you’re a complete beginner to making iPhone games, this Cocos2D tutorial is for you! You’ll learn how to make a complete game from scratch, with no prior experience necessary!

If you’re completely new to programming in general you might want to check out this slightly easier introduction first.

This Cocos2D tutorial is also good for intermediate developers, because it covers some neat effects such as parallax scrolling, pre-allocating CCNodes, accelerometer movement, and particle systems.

Without further ado, let’s get blasting!

Install Cocos2D

To make this game, you’ll need to be a member of the iOS developer program (so you can run this game on your iPhone) and have Xcode and the Cocos2D framework installed.

If you already have Cocos2D installed, feel free to skip to the next section. Otherwise, here are some instructions for how to install Cocos2D on your Mac:

  • Download Cocos2D from this page. Be sure to pick the very latest version – at the time of this writing it is 1.0.0-rc2 in the Unstable category. Don’t worry that it says Unstable – it actually works quite well! :]
  • Double click the downloaded file to unzip it, and (optionally) store it somewhere safe.
  • Open a Terminal (Applications\Utilities\Terminal), and use the cd command to navigate to where your cocos2d folder. Then run the ./install-templates.sh command to install your Xcode templates, like the following:
$ cd Downloads
$ cd cocos2d-iphone-1.0.0-rc2
$ ./install-templates.sh -f -u

If all works well, you should see several lines saying “Installing xxx template”.

Then restart Xcode, and congrats – you’ve installed Cocos2D!

Hello, Cocos2D!

Let’s get started by creating a “Hello World” Cocos2D project.

Start up Xcode, go to File\New\New Project, choose the iOS\cocos2d template, and click Next. Name the project SpaceGame, click Next, choose a folder to save your project in, and click Create.

Compile and run your project, and you should see “Hello World” appear on the screen:

Hello, Cocos2D!

Adding Resources

To make this iPhone game, you are going to need some art and sound effects with a space theme.

But don’t whip out MS Paint quite yet – luckily my lovely wife has made some cool space game resources you can use in this project!

So go ahead and download the space game resources and unzip them to your hard drive.

Once you’ve unzipped the resources, drag the Backgrounds, Fonts, Particles, Sounds, and Spritesheets folders into the Resources group in your Xcode project. (Basically everything except the Classes folder).

Make sure that “Copy items into destination group’s folder (if needed)” is checked, and click Finish.

When you’re done your Groups and Files tree should look something like this:

Xcode Resources Group

If you’re curious, feel free to take a peek through the contents of the folders you just added to your project. Here’s what’s inside:

  • Backgrounds: Some images that you’ll use to create a side-scrolling background for the game. Includes images of a galaxy, sunrise, and spatial anomolies (that will move quite slowly), and an image of some space dust (that will go in front and move a little bit faster).
  • Fonts: A bitmap font created with Glyph Designer that we’ll use to display some text in the game later on.
  • Particles: Some special effects we’ll be using to create the effect of some stars flying by, created with Particle Designer.
  • Sounds: Some space-themed background music and sound effects, created with Garage Band and cxfr.
  • Spritesheets: Contains an image in the pvr.ccz format containing several smaller images that we’ll be using in the game, including the asteroid, space ship, etc. This was created with Texture Packer – you’ll need this if you want to look at the pvr.ccz.

Don’t worry if you don’t have any of these tools installed – you don’t need them for this tutorial, since you can use these premade files. You can always try out these tools later!

Here’s what Sprites.pvr.ccz looks like by the way:

Sprite Sheet created with Texture Packer

In case you’re wondering why we’re combining all those images into a large image like this, it’s because it helps conserve memory and improve performance while making the game.

It’s a good practice to get into, so we’re starting you out with it early! :]

Adding a Space Ship

Let’s start things out nice and simple by adding the space ship to the screen!

Start by opening HelloWorldLayer.h, and add two new instance variables inside the @interface:

CCSpriteBatchNode *_batchNode;
CCSprite *_ship;

The first variable (_batchNode) is necessary because we’re storing all of our images inside a single image, and using this helps us batch up all the drawing work.

The second variable (_ship) represents the space ship on the screen.

Next move to HelloWorldLayer.m, and replace the init method with the following:

-(id) init
{
    if( (self=[super init])) {
 
        _batchNode = [CCSpriteBatchNode batchNodeWithFile:@"Sprites.pvr.ccz"]; // 1
        [self addChild:_batchNode]; // 2
        [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"Sprites.plist"]; // 3
 
        _ship = [CCSprite spriteWithSpriteFrameName:@"SpaceFlier_sm_1.png"];  // 4
        CGSize winSize = [CCDirector sharedDirector].winSize; // 5
        _ship.position = ccp(winSize.width * 0.1, winSize.height * 0.5); // 6
        [_batchNode addChild:_ship z:1]; // 7
    }
    return self;
}

Let’s go over this step by step:

  1. Creates a CCSpriteBatchNode to batch up all of the drawing of objects from the same large image. Passes in the image name (Sprites.pvr.ccz).
  2. Adds the CCSpriteBatchNode to the layer so it will be drawn.
  3. Loads the Sprites.plist file, which contains information on where inside the large image each of the smaller images lies. This lets you easily retrieve the sub-images later with spriteWithSpriteFrameName.
  4. Creates a new Sprite using the SpaceFlier_sm_1.png image, which is a sub-image within the large image.
  5. Gets the size of the screen from the CCDirectory – we’ll need this in a second.
  6. Sets the position of the ship so that it’s 10% along the width of the screen, and 50% along the height. Note that by default the position of the ship is the center of the ship.
  7. Adds the ship to the batchNode so that the drawing of the sprite is batched up.

Compile and run your project, and you should see your ship image appear on the screen!

Added space ship sprite to scene

Adding Parallax Scrolling

We have a cool space ship on the screen, but it looks like it’s just sitting there! Let’s fix this by adding some cool parallax scrolling to the scene.

But wait a minute – what in the heck is parallax scrolling?!

Parallax scrolling is just a fancy way of saying “move some parts of the background more slowly than the other parts.” If you’ve ever played SNES games like Act Raiser, you’ll often see this in the background of the action levels.

It’s really easy to use parallax scrolling in Cocos2D. You just have to do three steps:

  1. Create a CCParallaxNode, and add it to the layer.
  2. Create items you wish to scroll, and add them to the CCParallaxNode with addChild:parallaxRatio:positionOffset.
  3. Move the CCParallaxNode to scroll the background. It will scroll the children of the CCParallaxNode more quickly or slowly based on what you set the parallaxRatio to.

Let’s see how this works. Start by opening HelloWorldLayer.h, and add the following inside the @interface:

CCParallaxNode *_backgroundNode;
CCSprite *_spacedust1;
CCSprite *_spacedust2;
CCSprite *_planetsunrise;
CCSprite *_galaxy;
CCSprite *_spacialanomaly;
CCSprite *_spacialanomaly2;

Then switch to HelloWorldLayer.m, and add the following to the bottom of your init method:

// 1) Create the CCParallaxNode
_backgroundNode = [CCParallaxNode node];
[self addChild:_backgroundNode z:-1];
 
// 2) Create the sprites we'll add to the CCParallaxNode
_spacedust1 = [CCSprite spriteWithFile:@"bg_front_spacedust.png"];
_spacedust2 = [CCSprite spriteWithFile:@"bg_front_spacedust.png"];
_planetsunrise = [CCSprite spriteWithFile:@"bg_planetsunrise.png"];
_galaxy = [CCSprite spriteWithFile:@"bg_galaxy.png"];
_spacialanomaly = [CCSprite spriteWithFile:@"bg_spacialanomaly.png"];
_spacialanomaly2 = [CCSprite spriteWithFile:@"bg_spacialanomaly2.png"];
 
// 3) Determine relative movement speeds for space dust and background
CGPoint dustSpeed = ccp(0.1, 0.1);
CGPoint bgSpeed = ccp(0.05, 0.05);
 
// 4) Add children to CCParallaxNode
[_backgroundNode addChild:_spacedust1 z:0 parallaxRatio:dustSpeed positionOffset:ccp(0,winSize.height/2)];
[_backgroundNode addChild:_spacedust2 z:0 parallaxRatio:dustSpeed positionOffset:ccp(_spacedust1.contentSize.width,winSize.height/2)];        
[_backgroundNode addChild:_galaxy z:-1 parallaxRatio:bgSpeed positionOffset:ccp(0,winSize.height * 0.7)];
[_backgroundNode addChild:_planetsunrise z:-1 parallaxRatio:bgSpeed positionOffset:ccp(600,winSize.height * 0)];        
[_backgroundNode addChild:_spacialanomaly z:-1 parallaxRatio:bgSpeed positionOffset:ccp(900,winSize.height * 0.3)];        
[_backgroundNode addChild:_spacialanomaly2 z:-1 parallaxRatio:bgSpeed positionOffset:ccp(1500,winSize.height * 0.9)];

Compile and run your project, and you should see the start of a space scene:

Parallax background but not moving

However this isn’t very interesting yet, since nothing is moving!

To move the space dust and backgrounds, all you need to do is move the parallax node itself. For every Y points we move the parallax node, the dust will move 0.1Y points, and the backgrounds will move 0.05Y points.

To move the parallax node, you’ll simply update the position every frame according to a set velocity. Try this out for yourself by making the following changes to HelloWorldLayer.m:

// Add to end of init method
[self scheduleUpdate];
 
// Add new update method
- (void)update:(ccTime)dt {
 
    CGPoint backgroundScrollVel = ccp(-1000, 0);
    _backgroundNode.position = ccpAdd(_backgroundNode.position, ccpMult(backgroundScrollVel, dt));
 
}

Compile and run your project, and things should start to scroll pretty neatly with parallax scrolling!

Parallax Scrolling Background with Cocos2D

However, after a few seconds goes by, you’ll notice a major problem: we run out of things to scroll through, and you end up with a blank screen! That would be pretty boring, so let’s see what we can do about this.

Continuous Scrolling

We want the background to keep scrolling endlessly. The strategy we’re going to take to do this is to simply move the background to the right once it has moved offscreen to the left.

One minor problem is that CCParallaxNode currently doesn’t have any way to modify the offset of a child node once it’s added. You can’t simply update the position of the child node itself, because the CCParallaxNode overwrites that each update.

However, I’ve created a category on CCParallaxNode that you can use to solve this problem, which you can find in the resources for this project in the Classes folder. Drag CCParallaxNode-Extras.h and CCParallaxNode-Extras.m into your project, make sure “Copy items into destination group’s folder” is checked, and click Finish.

Then make the following changes to HelloWorldLayer.m to implement continuous scrolling:

// Add to top of file
#import "CCParallaxNode-Extras.h"
 
// Add at end of your update method
NSArray *spaceDusts = [NSArray arrayWithObjects:_spacedust1, _spacedust2, nil];
for (CCSprite *spaceDust in spaceDusts) {
    if ([_backgroundNode convertToWorldSpace:spaceDust.position].x < -spaceDust.contentSize.width) {
        [_backgroundNode incrementOffset:ccp(2*spaceDust.contentSize.width,0) forChild:spaceDust];
    }
}
 
NSArray *backgrounds = [NSArray arrayWithObjects:_planetsunrise, _galaxy, _spacialanomaly, _spacialanomaly2, nil];
for (CCSprite *background in backgrounds) {
    if ([_backgroundNode convertToWorldSpace:background.position].x < -background.contentSize.width) {
        [_backgroundNode incrementOffset:ccp(2000,0) forChild:background];
    }
}

Compile and run your project, and now the background should scroll continuously through a cool space scene!

Continuous Repeating Background Scrolling with Cocos2D

Adding Stars

No space game would be complete without some stars flying by!

We could create another image with stars on it and add that to the parallax node like we have with the other decorations, but stars are a perfect example of when you’d want to use a particle system.

Particle systems allow you to efficiently create a large number of small objects using the same sprite. Cocos2D gives you a lot of control over configuring particle systems, and Particle Designer is a great way to visually set these up.

But for this tutorial, I’ve already set up some particle effects for some stars racing from right to left across the screen that we can use. Simply add the following code to the bottom of the init method to set these up:

NSArray *starsArray = [NSArray arrayWithObjects:@"Stars1.plist", @"Stars2.plist", @"Stars3.plist", nil];
for(NSString *stars in starsArray) {        
    CCParticleSystemQuad *starsEffect = [CCParticleSystemQuad particleWithFile:stars];        
    [self addChild:starsEffect z:1];
}

By adding the particle systems to the layer, they automatically start running. Compile and run to see for yourself, and now you should see some stars flying across the scene!

Creating scrolling stars particle system

Moving the Ship with the Accelerometer

So far so good, except this wouldn’t be much of a game unless we can move our space ship!

We’re going to take the approach of moving the space ship via the accelerometer. As the user tilts the device along the X-axis, the ship will move up and down.

This is actually pretty easy to implement, so let’s jump right into it. First, add an instance variable inside the @interface in HelloWorldLayer.h to keep track of the points per second to move the ship along the Y-axis:

float _shipPointsPerSecY;

Then, make the following changes to HelloWorldLayer.m:

// 1) Add to bottom of init
self.isAccelerometerEnabled = YES;
 
// 2) Add new method
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
 
#define kFilteringFactor 0.1
#define kRestAccelX -0.6
#define kShipMaxPointsPerSec (winSize.height*0.5)        
#define kMaxDiffX 0.2
 
    UIAccelerationValue rollingX, rollingY, rollingZ;
 
    rollingX = (acceleration.x * kFilteringFactor) + (rollingX * (1.0 - kFilteringFactor));    
    rollingY = (acceleration.y * kFilteringFactor) + (rollingY * (1.0 - kFilteringFactor));    
    rollingZ = (acceleration.z * kFilteringFactor) + (rollingZ * (1.0 - kFilteringFactor));
 
    float accelX = acceleration.x - rollingX;
    float accelY = acceleration.y - rollingY;
    float accelZ = acceleration.z - rollingZ;
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
 
    float accelDiff = accelX - kRestAccelX;
    float accelFraction = accelDiff / kMaxDiffX;
    float pointsPerSec = kShipMaxPointsPerSec * accelFraction;
 
    _shipPointsPerSecY = pointsPerSec;
 
}
 
// 4) Add to bottom of update
CGSize winSize = [CCDirector sharedDirector].winSize;
float maxY = winSize.height - _ship.contentSize.height/2;
float minY = _ship.contentSize.height/2;
 
float newY = _ship.position.y + (_shipPointsPerSecY * dt);
newY = MIN(MAX(newY, minY), maxY);
_ship.position = ccp(_ship.position.x, newY);

Let’s go over this bit-by-bit:

  1. Adding this line sets the layer up to receive the accelerometer:didAcccelerate callback.
  2. The first part of this method comes directly from Apple sample code, to filter the accelerometer values so it’s not so “jiggly”. Don’t worry if you don’t understand this, all you really need to know is that it makes things more smooth. If you’re insatiably curious here’s some info though. Anyway, after we run the filter, we test to see how much it’s tilted. A rotation of -0.6 along the x-axis is considered “baseline”, and the closer it gets to 0.2 in either direction, the faster the ship is set to move. These values were all gotten via experimentation and what “felt right”!
  3. Sets the position of the ship based on the points per second to move along the Y axis computed earlier, and the delta time since last update.

Compile and run your project (on your iPhone, the accelerometer does not work on the Simulator), and now you should be able to move the spaceship up and down by tilting your iPhone!

Moving the spaceship with the 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 we’re going to take is every so often, we’ll create an asteroid offscreen to the right of the screen. Then we’ll run a Cocos2D action to move it to the left of the screen.

We could simply create a new asteroid every time we needed to spawn, but allocating memory is a slow operation and it’s best when you can avoid it. So we’ll pre-allocate memory for a bunch of asteroids, and simply grab the next available asteroid when we need it.

OK, let’s see what this looks like. Start by adding a few new instance variables inside the @interface in HelloWorldLayer.h:

CCArray *_asteroids;
int _nextAsteroid;
double _nextAsteroidSpawn;

Then make the following changes to HelloWorldLayer.m:

// Add to top of file
#define kNumAsteroids   15
 
// Add to bottom of init
_asteroids = [[CCArray alloc] initWithCapacity:kNumAsteroids];
for(int i = 0; i < kNumAsteroids; ++i) {
    CCSprite *asteroid = [CCSprite spriteWithSpriteFrameName:@"asteroid.png"];
    asteroid.visible = NO;
    [_batchNode addChild:asteroid];
    [_asteroids addObject:asteroid];
}
 
// Add new method, above update loop
- (float)randomValueBetween:(float)low andValue:(float)high {
    return (((float) arc4random() / 0xFFFFFFFFu) * (high - low)) + low;
}
 
// Add to bottom of update loop
double curTime = CACurrentMediaTime();
if (curTime > _nextAsteroidSpawn) {
 
    float randSecs = [self randomValueBetween:0.20 andValue:1.0];
    _nextAsteroidSpawn = randSecs + curTime;
 
    float randY = [self randomValueBetween:0.0 andValue:winSize.height];
    float randDuration = [self randomValueBetween:2.0 andValue:10.0];
 
    CCSprite *asteroid = [_asteroids objectAtIndex:_nextAsteroid];
    _nextAsteroid++;
    if (_nextAsteroid >= _asteroids.count) _nextAsteroid = 0;
 
    [asteroid stopAllActions];
    asteroid.position = ccp(winSize.width+asteroid.contentSize.width/2, randY);
    asteroid.visible = YES;
    [asteroid runAction:[CCSequence actions:
                         [CCMoveBy actionWithDuration:randDuration position:ccp(-winSize.width-asteroid.contentSize.width, 0)],
                         [CCCallFuncN actionWithTarget:self selector:@selector(setInvisible:)],
                         nil]];
 
}
 
// Add new method
- (void)setInvisible:(CCNode *)node {
    node.visible = NO;
}

Some things to point out about the above code:

  • CCArray is similar to NSArray, but optimized for speed. So it’s good to use in Cocos2D when possible.
  • Notice that we add all 15 asteroids to the batch node as soon as the game starts, but set them all to invisible. If they’re invisible we treat them as inactive.
  • We use an instance variable (_nextAsteroidSpawn) to tell us the time to spawn an asteroid next. We always check this in the update loop.
  • If you’re new to Cocos2D actions, these are easy ways to get sprites to do things over time, such as move, scale, rotate, etc. Here we 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.

Compile and run your code, and now you have some asteroids flying across the screen!

Adding asteroids to dodge

Shooting Lasers

I don’t know about you, but the first thing I think of when I see asteroids is MUST SHOOT THEM!

So let’s take care of this urge by adding the ability to fire lasers! This code will be similar to how we added asteroids, because we’ll create an array of reusable laser beams and move them across the screen with actions.

The main difference is we’ll be using touch handling to detect when to shoot them.

Start by adding a few new instance variables inside the @interface in HelloWorldLayer.h:

CCArray *_shipLasers;
int _nextShipLaser;

Then make the following changes to HelloWorldLayer.m:

// Add to top of file
#define kNumLasers      5
 
// Add to bottom of init
_shipLasers = [[CCArray alloc] initWithCapacity:kNumLasers];
for(int i = 0; i < kNumLasers; ++i) {
    CCSprite *shipLaser = [CCSprite spriteWithSpriteFrameName:@"laserbeam_blue.png"];
    shipLaser.visible = NO;
    [_batchNode addChild:shipLaser];
    [_shipLasers addObject:shipLaser];
}
 
self.isTouchEnabled = YES;
 
// Add new method
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
 
    CCSprite *shipLaser = [_shipLasers objectAtIndex:_nextShipLaser];
    _nextShipLaser++;
    if (_nextShipLaser >= _shipLasers.count) _nextShipLaser = 0;
 
    shipLaser.position = ccpAdd(_ship.position, ccp(shipLaser.contentSize.width/2, 0));
    shipLaser.visible = YES;
    [shipLaser stopAllActions];
    [shipLaser runAction:[CCSequence actions:
                          [CCMoveBy actionWithDuration:0.5 position:ccp(winSize.width, 0)],
                          [CCCallFuncN actionWithTarget:self selector:@selector(setInvisible:)],
                          nil]];
 
}

So this shows you how easy it is to receive touch events in Cocos2D – just set isTouchEnabled to YES, then you can implement ccTouchesBegan (and/or ccTouchesMoved, ccTouchesEnded, etc)!

Compile and run your code, and now you can go pew-pew!

Shooting lasers from ship

Basic Collision Detection

So far things look like a game, but don’t act like a game, because nothing blows up!

And since I’m naughty by nature (and not ’cause I hate ya), it’s time to add some violence into this game!

Start by adding the following to the @interface in HelloWorldLayer.h:

int _lives;

Then add the following to the bottom of the update loop:

for (CCSprite *asteroid in _asteroids) {        
    if (!asteroid.visible) continue;
 
    for (CCSprite *shipLaser in _shipLasers) {                        
        if (!shipLaser.visible) continue;
 
        if (CGRectIntersectsRect(shipLaser.boundingBox, asteroid.boundingBox)) {                
            shipLaser.visible = NO;
            asteroid.visible = NO;                
            continue;
        }
    }
 
    if (CGRectIntersectsRect(_ship.boundingBox, asteroid.boundingBox)) {
        asteroid.visible = NO;
        [_ship runAction:[CCBlink actionWithDuration:1.0 blinks:9]];            
        _lives--;
    }
}

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 isn’t a perfect way of checking for collisions, but it’s good enough for a simple game like this.

For more info on a better way to do collision detection in Cocos2D, check out the How To Use Box2D For Just Collision Detection tutorial.

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

Win/Lose Detection

We’re almost done – just need to add a way for the player to win or lose!

We’ll make it so the player wins if he survives for 30 seconds, and loses if he gets hit 3 times.

Start by making a few changes to HelloWorldLayer.h:

// Add before @interface
typedef enum {
    kEndReasonWin,
    kEndReasonLose
} EndReason;
 
// Add inside @interface
double _gameOverTime;
bool _gameOver;

Then make the following changes to HelloWorldLayer.m:

// Add at end of init
_lives = 3;
double curTime = CACurrentMediaTime();
_gameOverTime = curTime + 30.0;
 
// Add at end of update loop
if (_lives <= 0) {
    [_ship stopAllActions];
    _ship.visible = FALSE;
    [self endScene:kEndReasonLose];
} else if (curTime >= _gameOverTime) {
    [self endScene:kEndReasonWin];
}
 
// Add new methods above update
- (void)restartTapped:(id)sender {
    [[CCDirector sharedDirector] replaceScene:[CCTransitionZoomFlipX transitionWithDuration:0.5 scene:[HelloWorldLayer scene]]];   
}
 
- (void)endScene:(EndReason)endReason {
 
    if (_gameOver) return;
    _gameOver = true;
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
 
    NSString *message;
    if (endReason == kEndReasonWin) {
        message = @"You win!";
    } else if (endReason == kEndReasonLose) {
        message = @"You lose!";
    }
 
    CCLabelBMFont *label;
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        label = [CCLabelBMFont labelWithString:message fntFile:@"Arial-hd.fnt"];
    } else {
        label = [CCLabelBMFont labelWithString:message fntFile:@"Arial.fnt"];
    }
    label.scale = 0.1;
    label.position = ccp(winSize.width/2, winSize.height * 0.6);
    [self addChild:label];
 
    CCLabelBMFont *restartLabel;
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        restartLabel = [CCLabelBMFont labelWithString:@"Restart" fntFile:@"Arial-hd.fnt"];    
    } else {
        restartLabel = [CCLabelBMFont labelWithString:@"Restart" fntFile:@"Arial.fnt"];    
    }
 
    CCMenuItemLabel *restartItem = [CCMenuItemLabel itemWithLabel:restartLabel target:self selector:@selector(restartTapped:)];
    restartItem.scale = 0.1;
    restartItem.position = ccp(winSize.width/2, winSize.height * 0.4);
 
    CCMenu *menu = [CCMenu menuWithItems:restartItem, nil];
    menu.position = CGPointZero;
    [self addChild:menu];
 
    [restartItem runAction:[CCScaleTo actionWithDuration:0.5 scale:1.0]];
    [label runAction:[CCScaleTo actionWithDuration:0.5 scale:1.0]];
 
}

Don’t worry if you don’t understand how the endScene method works – that’s some code I’ve used for a bunch of games for a quick win/lose menu in the past.

The important part is just that you understand the rest of the code – every update loop, you just check to see if the player has won or lost, and call that method if so.

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

Adding game over detection

Gratuitous Music and Sound Effects

As you know, I can’t leave you guys without some awesome sound effects and music to add in!

You’ve already added the sounds to your project, so just add a bit of code to play them in HelloWorldLayer.m:

// Add to top of file
#import "SimpleAudioEngine.h"
 
// Add to bottom of init
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"SpaceGame.caf" loop:YES];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"explosion_large.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"laser_ship.caf"];
 
// Add inside BOTH CGRectIntersectsRect tests
[[SimpleAudioEngine sharedEngine] playEffect:@"explosion_large.caf"];
 
// Add inside ccTouchBegan
[[SimpleAudioEngine sharedEngine] playEffect:@"laser_ship.caf"];

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

You Win!

Want More?

I had a lot of fun working on this Cocos2D tutorial, so I actually spent a bit more time to polish this up and add some cool new features. Check out the following video to see what I mean:

I’m planning on making this (or something like it) into an actual iPhone game at some point, because I think the core concept is pretty fun.

But I was wondering if maybe you guys might also like me to turn this into a “Space Game Starter Kit” for sale on this site?

The starter kit would include the source code for the game in the video, plus a Cocos2D tutorial that shows how to make it from scratch, with the following features and instructions:

  • Using Box2D for Collision Detection
  • Using Physics Editor to define shapes
  • Resizing Box2D shapes based on Cocos2D sprite scaling
  • Detecting Box2D collision points
  • Playing animations for the ship and efficient preloading
  • Creating the alien ship and movement
  • Having enemies with various amounts of health
  • Shaking the screen
  • Adding Retina-display graphics
  • Making it a Universal App for iPhone + iPad
  • Creating powerups
  • Creating the cool zoom effect

But before I decide to do this, I want to make sure this is something you guys want. If not I won’t mind, I have plenty of other stuff to work on lol!

So I’d really appreciate it if you could let me know if you’d like this (or not) with the following poll:

UPDATE: Thank you to everyone who voted! I removed the poll, because I got enough feedback, and by an overwhelming response you guys said you wanted it!

You guys asked for it, you got it! The Space Game Starter Kit is now complete, and available on the new raywenderlich.com store!

Where To Go From Here?

Here is the sample project with all of the code from the above tutorial.

And that concludes the How To Make A Space Shooter iPhone Game tutorial! If you want to learn more about Cocos2D, there are a bunch of other cocos2D tutorials on this site you can check out!

Thank you to @whereswayne from the forums for suggesting this idea! If you have an idea, feel free to suggest one yourself!

I know I still owe you guys a tutorial about OpenGL from a few weeks back, I’m still working on that LOL. It’s quite a big subject!

Please join in the forum discussion below if you have any questions or comments!

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

201 Comments

[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 ]
  • I am having the same trouble that a few others are having, namely when I add parallax scrolling the code crashes on startup. The only suggestion I see on this forum is to make sure that CCParallaxNode-Extras.m has been added to the project, which it is. The offending code appears to be:

    // Add at end of your update method
    NSArray *spaceDusts = [NSArray arrayWithObjects:_spacedust1, _spacedust2, nil];
    for (CCSprite *spaceDust in spaceDusts)
    {
    if ([_backgroundNode convertToWorldSpace:spaceDust.position].x < -spaceDust.contentSize.width)
    {
    [_backgroundNode incrementOffset:ccp(2*spaceDust.contentSize.width,0) forChild:spaceDust];
    }
    }


    NSArray *backgrounds = [NSArray arrayWithObjects:_planetsunrise, _galaxy, _spacialanomaly, _spacialanomaly2, nil];
    for (CCSprite *background in backgrounds)
    {
    if ([_backgroundNode convertToWorldSpace:background.position].x < -background.contentSize.width)
    {
    [_backgroundNode incrementOffset:ccp(2000,0) forChild:background];
    }
    }

    Does anyone have a solution for this?
    dgoulian
  • I have tried to get the shooting portion of this sample added to my game with moderate success, i would like my final results to have the projectile fired towards where the use has touched, i have combined this code along with the code from http://www.raywenderlich.com/25736/how-to-make-a-simple-iphone-game-with-cocos2d-2-x-tutorial. and i have thus far come up with the following

    Code: Select all
    - (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
     
      // Choose one of the touches to work with
      UITouch *touch = [touches anyObject];
      CGPoint location = [self convertTouchToNodeSpace:touch];
     
      CGSize winSize = [CCDirector sharedDirector].winSize;
     
      CCSprite *shipLaser = [_shipLasers objectAtIndex:_nextShipLaser];
      _nextShipLaser++;
      if (_nextShipLaser >= _shipLasers.count) _nextShipLaser = 0;
     
      shipLaser.position = ccpAdd(bird_pos, ccp(shipLaser.contentSize.height/2, 0));
      shipLaser.visible = YES;
     
      // Determine offset of location to projectile
      CGPoint offset = ccpSub(location, shipLaser.position);
     
      // Bail out if you are shooting down or backwards
      //if (offset.x <= 0) return;
     
      int realX = winSize.width + (shipLaser.contentSize.width/2);
      float ratio = (float) offset.y / (float) offset.x;
      int realY = (realX * ratio) + shipLaser.position.y;
      CGPoint realDest = ccp(realX, realY);
     
      [self playEffect:@"phaserUp7.mp3"];
      [shipLaser stopAllActions];
      [shipLaser runAction:[CCSequence actions:
                            [CCMoveBy actionWithDuration:1 position:realDest],
                            [CCCallFuncN actionWithTarget:self selector:@selector(setInvisible:)],
                            nil]];
     
    }


    it works quite well but with one flaw which i am hoping someone on here can help with, it will only fire to the right of the player, can anyone provide a solution to this that will allow full 360 degree shooting or at least 180 above the player
    sbigaming
  • hello
    First of all, great tutorial, really easy to follow.

    I have just started to learn objective-c i got the game to work and all, but i dont have an iphone to test it out, so is it to much work to change the steering from accelerometer to button based? So it is possible to try it out in the iphone simulator within xcode.
    onkelvalle
  • well after finding an answer on SO i have now got a working version as follows

    Code: Select all
    - (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
      if (!paused) {
      // Choose one of the touches to work with
      UITouch *touch = [touches anyObject];
      CGPoint location = [self convertTouchToNodeSpace:touch];
     
        float scalar = 1.0f;
       
       
      CGSize winSize = [CCDirector sharedDirector].winSize;
     
      CCSprite *shipLaser = [_shipLasers objectAtIndex:_nextShipLaser];
      _nextShipLaser++;
      if (_nextShipLaser >= _shipLasers.count) _nextShipLaser = 0;
     
      shipLaser.position = ccpAdd(bird_pos, ccp(shipLaser.contentSize.height/2, 0));
      shipLaser.visible = YES;
     
       
        int offX = location.x - shipLaser.position.x;
       // int offY = location.y - shipLaser.position.y;
       
        if (offX < 0.0f) scalar = -1.0f;
       
      // Determine offset of location to projectile
      CGPoint offset = ccpSub(location, shipLaser.position);
     
      // Bail out if you are shooting down or backwards
      //if (offset.x <= 0) return;
     
      int realX = scalar * (winSize.width + (shipLaser.contentSize.width/2));
      //int realX = winSize.width + (shipLaser.contentSize.width/2);
      float ratio = (float) offset.y / (float) offset.x;
      int realY = (realX * ratio) + shipLaser.position.y;
      CGPoint realDest = ccp(realX, realY);
     
      [self playEffect:@"phaserUp7.mp3"];
      [shipLaser stopAllActions];
      [shipLaser runAction:[CCSequence actions:
                            [CCMoveBy actionWithDuration:1 position:realDest],
                            [CCCallFuncN actionWithTarget:self selector:@selector(setInvisible:)],
                            nil]];
      }
    }


    this allows me to fire in all directions now, i hope this will help anyone else having a similar problem
    sbigaming
  • I was able to make a continuous scrolling background on the iPad but not on the iPhone. On the iPhone the background is usually not in the correct starting position. I am having positioning issues on the iPhone. Can anyone help with positioning sprites in the parallax on the iPhone and iPhone 5? Here is my code so far:
    Code: Select all

    -(void)setUpBackground{
    // 1) Create the CCParallaxNode
        background = [CCParallaxNode node];
        [self addChild:background z:-1];

    // 2) Create the sprites we'll add to the CCParallaxNode
        colorBackground = [CCSprite spriteWithFile:@"backgroundcolor.png"];
        colorBackground2 = [CCSprite spriteWithFile:@"backgroundcolor.png"];
        birdBackground= [CCSprite spriteWithFile:@"birdbackground.png"];
        birdBackground.color = ccBLACK;
        birdBackground.opacity = birdBackground.opacity/2;
        treeBackground = [CCSprite spriteWithFile:@"treebackground.png"];
        treeBackground.color = ccGRAY;
        treeBackground2 = [CCSprite spriteWithFile:@"treebackground.png"];
        treeBackground2.color = ccGRAY;
        canopyBackground = [CCSprite spriteWithFile:@"canopybackground.png"];
        canopyBackground2 = [CCSprite spriteWithFile:@"canopybackground.png"];

    // 3) Determine relative movement speeds
        CGPoint birdSpeed = ccp(0.1, 0.1);
        CGPoint treeAndColorBackgroundSpeed = ccp(0.05, 0.05);
        CGPoint canopyBackgroundSpeed = ccp(0.15, 0.15);

    // 4) Add children to CCParallaxNode
        [background addChild:colorBackground z:-5 parallaxRatio:treeAndColorBackgroundSpeed positionOffset:ccp(screenWidth/2, screenHeight/2)];
        [background addChild:colorBackground2 z:-5 parallaxRatio:treeAndColorBackgroundSpeed positionOffset:ccp(colorBackground.contentSize.width + colorBackground2.contentSize.width/2, screenHeight/2)];
        [background addChild:birdBackground z:-4 parallaxRatio:birdSpeed positionOffset:ccp(screenWidth/2, screenHeight/2)];
        [background addChild:treeBackground z:-3 parallaxRatio:treeAndColorBackgroundSpeed positionOffset:ccp(screenWidth/2, screenHeight/2)];
        [background addChild:treeBackground2 z:-3 parallaxRatio:treeAndColorBackgroundSpeed positionOffset:ccp(treeBackground.contentSize.width + treeBackground2.contentSize.width/2, screenHeight/2)];
        [background addChild:canopyBackground z:-2 parallaxRatio:canopyBackgroundSpeed positionOffset:ccp(screenWidth/2, screenHeight/2)];
        [background addChild:canopyBackground2 z:-2 parallaxRatio:canopyBackgroundSpeed positionOffset:ccp(canopyBackground.contentSize.width + canopyBackground2.contentSize.width/2, screenHeight/2)];

    //[self scheduleUpdate];
        [self schedule:@selector(updateTheBackground:)interval:1.0f/60.0f];
    }

    - (void)updateTheBackground:(ccTime)delta{

        CGPoint backgroundScrollVel = ccp(backgroundSpeed, 0);
        background.position = ccpAdd(background.position, ccpMult(backgroundScrollVel, delta));

        if (screenWidth == 1024 && screenHeight == 768) {

            NSArray *treeAndColor = [NSArray arrayWithObjects:treeBackground,treeBackground2, colorBackground,colorBackground2, nil];
            for (CCSprite *treeAndColors in treeAndColor) {
                if ([background convertToWorldSpace:treeAndColors.position].x < -treeAndColors.contentSize.width/2 - 80) {
                    [background incrementOffset:ccp(treeAndColors.contentSize.width * 2, 0) forChild:treeAndColors];
                }
            }

            NSArray *bird = [NSArray arrayWithObjects:birdBackground, nil];
            for (CCSprite *birds in bird) {
                if ([background convertToWorldSpace:birds.position].x < -birds.contentSize.width) {
                    [background incrementOffset:ccp(screenWidth * 2, 0) forChild:birds];
                }
            }

            NSArray *canopy = [NSArray arrayWithObjects:canopyBackground,canopyBackground2, nil];
            for (CCSprite* canopies in canopy) {
                if ([background convertToWorldSpace:canopies.position].x < -canopies.contentSize.width/2 - 50) {
                    [background incrementOffset:ccp(canopies.contentSize.width * 2, 0) forChild:canopies];
                }
            }

        }else if (screenWidth == 480 && screenHeight == 320){
            NSArray *treeAndColor = [NSArray arrayWithObjects:treeBackground,treeBackground2, colorBackground,colorBackground2, nil];
            for (CCSprite *treeAndColors in treeAndColor) {
                if ([background convertToWorldSpace:treeAndColors.position].x < -treeAndColors.contentSize.width/2 - 30) {
                    [background incrementOffset:ccp(treeAndColors.contentSize.width * 2, 0) forChild:treeAndColors];
                }
            }

            NSArray *bird = [NSArray arrayWithObjects:birdBackground, nil];
            for (CCSprite *birds in bird) {
                if ([background convertToWorldSpace:birds.position].x < -birds.contentSize.width) {
                    [background incrementOffset:ccp(screenWidth * 2, 0) forChild:birds];
                }
            }

            NSArray *canopy = [NSArray arrayWithObjects:canopyBackground,canopyBackground2, nil];
            for (CCSprite* canopies in canopy) {
                if ([background convertToWorldSpace:canopies.position].x < -canopies.contentSize.width/2 - 25) {
                    [background incrementOffset:ccp(canopies.contentSize.width * 2, 0) forChild:canopies];
                }
            }
        }
    }

    All of the code after the if statement:
    Code: Select all

    }else if (screenWidth == 480 && screenHeight == 320){

    is for the positioning of the background on the iPhone.
    pokobros
  • hi!!!! I am trying to follow this example and i am stuck in the "category on CCParallaxNode" class that you had done for cocos2d 2.X...

    I Am trying to do it with the cocos2d 3.0 RC version. Can you update that helper class that you had designed for repeat the background??

    Thatk you very much !
    streak22
[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in May: Procedural Level Generation in Games with Kim Pedersen.

Sign Up - May

Coming up in June: WWDC Keynote - Podcasters React! with the podcasting team.

Sign Up - June

Vote For Our Next Book!

Help us choose the topic for our next book we write! (Choose up to three topics.)

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Matt Galloway

... 55 total!

Editorial Team

  • Ryan Nystrom

... 22 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

  • Lin Ma
  • Heejun Han

... 38 total!

Subject Matter Experts

... 4 total!