Cocos2D-X Tutorial for iOS and Android: Space Game

Jean-Yves Mengant

Update 5/14/2013: Fully updated for Cocos2D-X 2.1 and iOS 6. (original post by Jean-Yves Mengant, update by Jorge Jordán).

Update 5/4/2014: You can find the code for this tutorial updated to Cocos2D-X 3.0 by @pahakorolev here.

In this Cocos2D-X tutorial, I will show you how to build a space game similar to the one was built in the How to Make a Space Shooter iPhone Game.

There will be one major difference – this will be written in beautiful cross-platform C++, using Cocos2D-X!

This means you’ll be able to run this game on both your iPhone and Android by the end of this Cocos2D-X tutorial. And with a little more work, you could get it running on other platforms as well – from Windows to Linux to the Mac!

This Cocos2D-X tutorial picks up where the Cocos2D-X Tutorial for iOS and Android: Getting Started left off. So if you haven’t already, read that project first and get your project set up the way described there.

So put on your Star Wars CD to set the mood, and let’s get started!

Getting Started

First things first – go ahead and download the space game resources ZIP file and unzip them to your hard drive.

You want to add these in a way so the same files can be used in both the Android and iOS projects, much like you did for the reusable C++ classes. To do this, you’ll add the files to the Android project’s Resources directory, and add relative links to this directory from your iOS project.

To add images and other resources into your project, you need to add them into $PROJECT_HOME\Resources (Remember that $PROJECT_HOME is the location of your Android Cocos2D-X project – samplecocos2dxandroid). (But don’t copy them yet – there’s a trick I’ll tell you about soon).

However, there’s one problem with this plan: the Eclipse project will only show files inside $PROJECT_HOME\android.

Luckily there is an easy workaround: you’ll make a link from $PROJECT_HOME\proj.android\Resources to $PROJECT_HOME\Resources – this way the files are where they need to be, but Eclipse can see them.

To do this, open up a Terminal and run the following command from within the $PROJECT_HOME\proj.android folder:

ln -s ../Resources ./Resources

If you go back to Eclipse, right click the project, and select Refresh, you should see the new folder Resources appear.

Now it’s time to copy the files to the $PROJECT_HOME\Resources folder, but let me tell you the trick I mentioned earlier.

For cross platform portability reasons, you should avoid using subdirectory hierarchies inside the $PROJECT_HOME\Resources folder. Although subdirectories work fine under iOS, they do not work well on Android. For example, if you had the Sprites.pvr.ccz file inside a SpriteSheet subdirectory, on Android the CCSpriteBatchNode::batchNodeWithFile Cocos2DX method would fail and return a null pointer.

So copy all of the individual files from the space game resources ZIP file into the $PROJECT_HOME\Resources folder. Remember not to create any subdirectories, just copy the files over. There’s also an existing Fonts subfolder inside Resources, move all the files inside Fonts into $PROJECT_HOME\Resources as well.

Additionally, there’s a Classes subfolder inside the ZIP file that you should not add to the $PROJECT_HOME\Resources – just delete that folder, or keep it for later reference when you get to the point where those particular files are created in this Cocos2D-X tutorial. When you’re done your $PROJECT_HOME\Resources folder should look like this:

Adding-resources-to-a-Cocos2D-x-Project

Next, let’s bring in these same files in to the iOS project as well. Open your Xcode project, and create a new group called SharedResources. Select the new group, and in the File Inspector click the button next to Path and browse to your $PROJECT_HOME\Resources.

Browsing-to-the-Resources-folder

Then right click the SharedResources group, select Add Files, and add all of the files from the $PROJECT_HOME\Resources folder. Make sure that the Copy items into destination group’s forder checkbox is not checked before you click Add. w00t – you’re fully set up!

Adding A Space Ship

Let’s try this out and see if it works! In your Eclipse project, open up Classes\HelloWorldScene.h and add the following to the beginning of the HelloWorldScene class (above the existing public: line):

private:
	cocos2d::CCSpriteBatchNode * _batchNode;
	cocos2d::CCSprite * _ship;

This creates two private instance variables – one for the sprite batch node, and one for the space ship sprite.

Next switch to HelloWorldScene.cpp and, inside the init() method, delete everything from the comment 2. add a menu item to the end of the method. Then add the following:

_batchNode = CCSpriteBatchNode::create("Sprites.pvr.ccz");
this->addChild(_batchNode);
CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("Sprites.plist");
 
_ship = CCSprite::createWithSpriteFrameName("SpaceFlier_sm_1.png");
CCSize winSize = CCDirector::sharedDirector()->getWinSize();
_ship->setPosition(ccp(winSize.width * 0.1, winSize.height * 0.5));    
_batchNode->addChild(_ship, 1);
return true;

Note that this code looks very similar to what you’re used to with Cocos2D in Objective-C. The API is pretty much the same, it’s just that there’s some differences in C++ language syntax.

Compile and Run in your Android simulator, and you should see your ship appear on the screen:

Space-Flier-Android

And the best part is it works on iOS as well!

Space-Flier-Xcode

Adding Parallax Scrolling

Next, let’s add the space background and make it scroll in a cool way with parallax scrolling.

First, it’s annoying to have to prefix everything with the cocos2d namespace, so add this line to HelloWorldScene.h, right before the class declaration:

USING_NS_CC;

Then add some new variables to the private section of the HelloWorldScene.h file (note you don’t need to prefix them anymore):

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

Next, add the following to the HelloWorldScene.cpp‘s init() method, right before the return statement:

// 1) Create the CCParallaxNode
    _backgroundNode = CCParallaxNode::create(); //1
    this->addChild(_backgroundNode,-1);
 
    // 2) Create the sprites will be added to the CCParallaxNode
    _spacedust1 = CCSprite::create("bg_front_spacedust.png");
    _spacedust2 = CCSprite::create("bg_front_spacedust.png");
    _planetsunrise = CCSprite::create("bg_planetsunrise.png");
    _galaxy = CCSprite::create("bg_galaxy.png");
    _spacialanomaly = CCSprite::create("bg_spacialanomaly.png");
    _spacialanomaly2 = CCSprite::create("bg_spacialanomaly2.png");
 
    // 3) Determine relative movement speeds for space dust and background
    CCPoint dustSpeed = ccp(0.1, 0.1);
    CCPoint bgSpeed = ccp(0.05, 0.05);
 
    // 4) Add children to CCParallaxNode
    _backgroundNode->addChild(_spacedust1, 0, dustSpeed, ccp(0,winSize.height/2) ); // 2
    _backgroundNode->addChild(_spacedust2, 0, dustSpeed, ccp( _spacedust1->getContentSize().width,winSize.height/2));
    _backgroundNode->addChild(_galaxy, -1, bgSpeed, ccp(0, winSize.height * 0.7));
    _backgroundNode->addChild(_planetsunrise, -1 , bgSpeed, ccp(600, winSize.height * 0));
    _backgroundNode->addChild(_spacialanomaly, -1, bgSpeed, ccp(900, winSize.height * 0.3));
    _backgroundNode->addChild(_spacialanomaly2, -1, bgSpeed, ccp(1500, winSize.height * 0.9));

Again, this is quite similar to the code in the Cocos2D space shooter tutorial, there are only minor syntax changes. You might enjoy comparing the two tutorials to see the differences in syntax.

Compile and Run in your Android simulator, and you should see the start of a space scene:

Space-Flier-Parallax-Android

And again, it works on the iPhone too!

Space-Flier-Parallax-Xcode

Now it’s time to make the background scroll. First, predeclare the update method in HelloWorldScene.h – you can add the following code to either the private or public section but since the update method is used internally, it’s probably more appropriate to add it as a private method:

// scheduled Update 
void update(float dt);

Then add the implementation at the end of HelloWorldScene.cpp:

void HelloWorld::update(float dt) {
  CCPoint backgroundScrollVert = ccp(-1000, 0);
  _backgroundNode->setPosition(ccpAdd(_backgroundNode->getPosition(), ccpMult(backgroundScrollVert, dt)));
}

Finally, call the scheduleUpdate method at the end of the init() method (but before the final return statement):

this->scheduleUpdate();

Compile and Run, and you will see the background scroll!

Continuous Scrolling

However, you will notice that after the background scrolls offscreen, it doesn’t repeat, so you need to fix that.

In the Cocos2D space shooter tutorial, it’s implemented by extending the CCParallaxNode class via an Objective-C category. Unfortunately, categories doesn’t exist in C++, so you’ll have to resort to an alternative method: inheritance.

You’re going to define a CCParallaxNode extras class that extends the standard CCParallaxNode. This is not as elegant as an Objective-C category, but sometimes it’s needed to make some sacrifices for the sake of portability.

In Xcode, right click the Classes group and select New File. Select the iOS\C and C++\C++ Class template, click Next, name it CCParallaxNodeExtras.cpp, browse to the $PROJECT_HOME\Classes directory to save it, and click Create. This step will create the CCParallaxNodeExtras.h header file automatically.

Then replace CCParallaxNodeExtras.h with the following:

#ifndef Cocos2DxFirstIosSample_CCParallaxNodeExtras_h
#define Cocos2DxFirstIosSample_CCParallaxNodeExtras_h
 
#include "cocos2d.h"
 
USING_NS_CC;
 
class CCParallaxNodeExtras : public CCParallaxNode {
 
    public :
 
    // Need to provide a constructor 
    CCParallaxNodeExtras();
 
    // just to avoid ugly later cast and also for safety
    static CCParallaxNodeExtras * node();
 
    // Facility method (it’s expected to have it soon in COCOS2DX)    
    void incrementOffset(CCPoint offset, CCNode* node);  
} ; 
 
#endif

Here the class extends CCParallaxNode to add the new method incrementOffset that will be used to update the position of a child of the parallax node. This will be used when part of the background goes offscreen to the left – incrementOffset will move it offscreen to the right for continuous wrapping.

Next replace CCParallaxNodeExtras.cpp with the following:

#include "CCParallaxNodeExtras.h" 
 
// Hack to access CCPointObject (which is not a public class)
class CCPointObject  : CCObject {
    CC_SYNTHESIZE(CCPoint, m_tRatio, Ratio)
    CC_SYNTHESIZE(CCPoint, m_tOffset, Offset)
    CC_SYNTHESIZE(CCNode *, m_pChild, Child)	// weak ref
};
 
// Need to provide a constructor 
CCParallaxNodeExtras::CCParallaxNodeExtras() {
    CCParallaxNode(); // call parent constructor
}
 
CCParallaxNodeExtras * CCParallaxNodeExtras::node() {
    return new CCParallaxNodeExtras();       
}
 
void CCParallaxNodeExtras::incrementOffset(CCPoint offset,CCNode* node){
    for( unsigned int i = 0; i < m_pParallaxArray->num; i++) {
        CCPointObject *point = (CCPointObject *)m_pParallaxArray->arr[i];
        CCNode * curNode = point->getChild();
        if( curNode->isEqual(node) ) {
            point->setOffset( ccpAdd(point->getOffset(), offset) );
            break;
        }
    }
}

Note that CCPointObject is unfortunately not a public class in Cocos2D, so you have to resort to a bit of a hack to use it (redefine it in your own class with the same signature). Although this works, the drawback is that if the CCPointObject structure ever changes, you’ll have to update it here as well or the program will crash.

The meat of the code is the incrementOffset method, and it’s implemented the same way the Cocos2D space shooter tutorial did it, just with different syntax.

Next, switch to HelloWorldScene.h and add this to the top of the file beneath the existing #include statement:

#include "CCParallaxNodeExtras.h"

Then change the _backgroundNode definition in the private section from a CCParallaxNode to a CCParallaxNodeExtras as follows:

CCParallaxNodeExtras *_backgroundNode;

Now switch over to HelloWorldScene.cpp and replace the first line in section #1 (the one where _backgroundNode is created) with the following:

_backgroundNode = CCParallaxNodeExtras::node();

Finally, add the following to the end of the update method:

CCArray *spaceDusts = CCArray::createWithCapacity(2);
spaceDusts->addObject(_spacedust1);
spaceDusts->addObject(_spacedust2);
for ( int ii = 0; ii <spaceDusts->count(); ii++ ) {
    CCSprite * spaceDust = (CCSprite *)(spaceDusts->objectAtIndex(ii));
    float xPosition = _backgroundNode->convertToWorldSpace(spaceDust->getPosition()).x;
    float size = spaceDust->getContentSize().width;
    if ( xPosition < -size/2 ) {
        _backgroundNode->incrementOffset(ccp(spaceDust->getContentSize().width*2,0),spaceDust); 
    }                                   
}
 
CCArray *backGrounds = CCArray::createWithCapacity(4);
backGrounds->addObject(_galaxy);
backGrounds->addObject(_planetsunrise);
backGrounds->addObject(_spacialanomaly);
backGrounds->addObject(_spacialanomaly2);
for ( int ii = 0; ii <backGrounds->count(); ii++ ) {
    CCSprite * background = (CCSprite *)(backGrounds->objectAtIndex(ii));
    float xPosition = _backgroundNode->convertToWorldSpace(background->getPosition()).x;
    float size = background->getContentSize().width;
    if ( xPosition < -size ) {
        _backgroundNode->incrementOffset(ccp(2000,0),background); 
    }
}

You can see that the alternative to NSArray here is CCArray, which is based on the STL C++ template library. You use the createWithCapacity constructor which will release the object automatically for you.

One final change – since you added a new file, you need to add it to the Android project’s Makefile to get it to build correctly. So open jni\Android.mk in Eclipse, and modify the LOCAL_SRC_FILES line to read as follows:

LOCAL_SRC_FILES := hellocpp/main.cpp \
	           ../../Classes/AppDelegate.cpp \
                   ../../Classes/HelloWorldScene.cpp \
                   ../../Classes/CCParallaxNodeExtras.cpp

Compile and Run, and now the background will scroll endlessly!

Adding Stars

Adding the stars is pretty straightforward. Just add this code to the init() method in HelloWorldScene.cpp, right before the return statement:

HelloWorld::addChild(CCParticleSystemQuad::create("Stars1.plist"));
HelloWorld::addChild(CCParticleSystemQuad::create("Stars2.plist"));
HelloWorld::addChild(CCParticleSystemQuad::create("Stars3.plist"));

Compile and Run, and pretty cool – it’s starting to look like a space game!

Adding-stars-Android

Adding-stars-iOS

Moving the Ship with Accelerometer

The original Cocos2D space shooter tutorial used the iOS accelerometer API to detect accelerometer input. Obviously the iOS accelerometer API isn’t cross platform, so how to solve this problem?

Luckily, Cocos2D-X has provided an abstraction layer around accelerometer input. Let’s see how it works.

First, add a new private variable to HelloWorldScene.h:

float _shipPointsPerSecY;

Then, add a new public method definition:

virtual void didAccelerate(CCAcceleration* pAccelerationValue);

Then add the following code to the bottom of init() in HelloWorldScene.cpp, before the return:

this->setAccelerometerEnabled(true);

Next, add the following new method to the bottom of the file:

void HelloWorld::didAccelerate(CCAcceleration* pAccelerationValue) {
  #define KFILTERINGFACTOR 0.1
  #define KRESTACCELX -0.6
  #define KSHIPMAXPOINTSPERSEC (winSize.height*0.5)        
  #define KMAXDIFFX 0.2
 
  double rollingX;
 
  // Cocos2DX inverts X and Y accelerometer depending on device orientation
  // in landscape mode right x=-y and y=x !!! (Strange and confusing choice)
  pAccelerationValue->x = pAccelerationValue->y;
  rollingX = (pAccelerationValue->x * KFILTERINGFACTOR) + (rollingX * (1.0 - KFILTERINGFACTOR));    
  float accelX = pAccelerationValue->x - rollingX;
  CCSize winSize = CCDirector::sharedDirector()->getWinSize();
  float accelDiff = accelX - KRESTACCELX;
  float accelFraction = accelDiff / KMAXDIFFX;
  _shipPointsPerSecY = KSHIPMAXPOINTSPERSEC * accelFraction;   
}

Finally, add the following to the bottom of update method:

CCSize winSize = CCDirector::sharedDirector()->getWinSize();
float maxY = winSize.height - _ship->getContentSize().height/2;
float minY = _ship->getContentSize().height/2;
 
float diff = (_shipPointsPerSecY * dt);
float newY = _ship->getPosition().y + diff;
newY = MIN(MAX(newY, minY), maxY);
_ship->setPosition(ccp(_ship->getPosition().x, newY));

The didAccelerate callback provides a CCAcceleration instance which contains x,y and z information about the acceleration data. For the purpose of this Cocos2D-X tutorial, you will only be interested in the x acceleration data so that you can move the spaceship when the phone rotates around its x axis.

Note: Cocos2D-X switches the accelerometer x and y values depending on whether the device is in portrait or landscape mode. In Landscape right (the tutorial case here), the received x is -y and received y is x. In Landscape left, the received x is y and the received y is -x. Confusing eh? :]

Compile and Run on your iPhone and Android devices, and you should now be able to move the ship by tilting your device! Of course, at this point, you’ll not be able to test the game on the simulator either for Android or iOS and would need to test on device.

Note: We have not supplied HD artwork, so the art might look a little odd if you’re on a retina device! We’ll talk a more about this at the end.

Adding Asteroids

Time to add some asteroids to the game! Start by adding some new private variables to HelloWorldScene.h:

CCArray* _asteroids;
int _nextAsteroid;
float _nextAsteroidSpawn;

Then, add some new public method definitions:

float randomValueBetween(float low, float high);
void setInvisible(CCNode * node);
float getTimeTick();

Next switch to HelloWorldScene.cpp, and add the implementations for the helper methods you defined above to the end of the file:

float HelloWorld::randomValueBetween(float low, float high) {
    return (((float) arc4random() / 0xFFFFFFFFu) * (high - low)) + low;
}
 
float HelloWorld::getTimeTick() {
    timeval time;
    gettimeofday(&time, NULL);
    unsigned long millisecs = (time.tv_sec * 1000) + (time.tv_usec/1000);
    return (float) millisecs;
}

randomValueBetween is a helper method to get a random number within a float range, and getTimeTick is a portable way to get the time in milliseconds (as a cross-platform alternative to CACurrentMediaTime, but with milliseconds instead of seconds).

Next, add this code to create an array of asteroids at the bottom of the init() method right before the return statement:

#define KNUMASTEROIDS 15
_asteroids = new CCArray();
for(int i = 0; i < KNUMASTEROIDS; ++i) {
    CCSprite *asteroid = CCSprite::createWithSpriteFrameName("asteroid.png");
    asteroid->setVisible(false);
    _batchNode->addChild(asteroid);
    _asteroids->addObject(asteroid);
}

Here the Cocos2D-X’s CCArray class is used to store an array of CCSprites. Note that you created the array manually with the “new” operator (rather than using arrayWithCapacity) to avoid the autorelease mechanism.

Finally, add this code to the bottom of update:

float curTimeMillis = getTimeTick();
if (curTimeMillis > _nextAsteroidSpawn) {
 
    float randMillisecs = randomValueBetween(0.20,1.0) * 1000;
    _nextAsteroidSpawn = randMillisecs + curTimeMillis;
 
    float randY = randomValueBetween(0.0,winSize.height);
    float randDuration = randomValueBetween(2.0,10.0);
 
    CCSprite *asteroid = (CCSprite *)_asteroids->objectAtIndex(_nextAsteroid);
    _nextAsteroid++;
 
    if (_nextAsteroid >= _asteroids->count())
        _nextAsteroid = 0;
 
    asteroid->stopAllActions();
    asteroid->setPosition( ccp(winSize.width+asteroid->getContentSize().width/2, randY));
    asteroid->setVisible(true);
    asteroid->runAction(CCSequence::create(CCMoveBy::create(randDuration, ccp(-winSize.width-asteroid->getContentSize().width, 0)), CCCallFuncN::create(this, callfuncN_selector(HelloWorld::setInvisible)), NULL // DO NOT FORGET TO TERMINATE WITH NULL (unexpected in C++)
     ));        
}

Again, this is vary similar to the original Cocos2D code. Note that you can even mimic calling Objective-C selectors through the callfuncN_selector call.

Note: Since the CCSequence::actions method is defined to have a variable number of arguments, you might be tempted to omit the final NULL argument, since it’s useless in C++. But for compatibility reasons, the Cocos2D-X developers decided to keep compliancy with Cocos2D and require the NULL terminator, and if you don’t provide it in your code, your app will crash.

The final step is to add the setInvisible callback method to the end of the file:

void HelloWorld::setInvisible(CCNode * node) {
    node->setVisible(false);
}

Compile and Run, and asteroids-ahoy! Here it is running on an iPhone 4:

iPhone4-run

And here running on an Android Nexus 7:

Running-on-Android-device

Shooting Lasers

It’s time to make this ship shoot! Add the following private variables to HelloWorldScene.h:

CCArray* _shipLasers;
int _nextShipLaser;

Now add a new public method definition:

virtual void ccTouchesBegan(cocos2d::CCSet* touches, cocos2d::CCEvent* event);

Then, add the following to the bottom of init() in HelloWorldScene.cpp before the return:

#define KNUMLASERS 5
_shipLasers = new CCArray();
for(int i = 0; i < KNUMLASERS; ++i) {
    CCSprite *shipLaser = CCSprite::createWithSpriteFrameName("laserbeam_blue.png");
    shipLaser->setVisible(false);
    _batchNode->addChild(shipLaser);
    _shipLasers->addObject(shipLaser);
}
this->setTouchEnabled(true);

Finally, implement ccTouchesBegan at the end of the file:

void HelloWorld::ccTouchesBegan(cocos2d::CCSet* touches, cocos2d::CCEvent* event)
{
    CCSize winSize = CCDirector::sharedDirector()->getWinSize();
 
    CCSprite *shipLaser = (CCSprite *)_shipLasers->objectAtIndex(_nextShipLaser++);
    if ( _nextShipLaser >= _shipLasers->count() )
        _nextShipLaser = 0;
    shipLaser->setPosition( ccpAdd( _ship->getPosition(), ccp(shipLaser->getContentSize().width/2, 0)));
    shipLaser->setVisible(true);
    shipLaser->stopAllActions();
    shipLaser->runAction(CCSequence::create(CCMoveBy::create(0.5,ccp(winSize.width, 0)), CCCallFuncN::create(this, callfuncN_selector(HelloWorld::setInvisible)), NULL  // DO NOT FORGET TO TERMINATE WITH NULL
    ));
}

Compile and Run, and now you can shoot lasers! Here it is running on an iPhone 4:

And on an Android Nexus 7:

Basic Collision Detection

Next, you will add some code to detect collisions between lasers and asteroids, and explode the asteroids that were hit when that occurs!

Again, the code to do this is quite similar to the Cocos2D space shooter tutorial, but with some new syntax. Notice that the update method introduce the CCARRAY_FOREACH macro to walk through the CCArray.

First, add a new private variable to HelloWorldScene.h:

int _lives;

Then, add the following code to the bottom of update in HelloWorldScene.cpp:

// Asteroids
CCObject* asteroid;
CCObject* shipLaser;
CCARRAY_FOREACH(_asteroids, asteroid){
    if (!((CCSprite *) asteroid)->isVisible() )
        continue;
    CCARRAY_FOREACH(_shipLasers, shipLaser){
        if (!((CCSprite *) shipLaser)->isVisible())
            continue;
        if (((CCSprite *) shipLaser)->boundingBox().intersectsRect(((CCSprite *)asteroid)->boundingBox()) ) {
            ((CCSprite *)shipLaser)->setVisible(false);
            ((CCSprite *)asteroid)->setVisible(false);
            continue;
        }
    }
    if (_ship->boundingBox().intersectsRect(((CCSprite *)asteroid)->boundingBox()) ) {
        ((CCSprite *)asteroid)->setVisible(false);
        _ship->runAction( CCBlink::create(1.0, 9));
        _lives--;
    }
}

Compile and Run, and now you should be able to blow up asteroids!

Of course, you’ll notice that while the asteroids disappear when the lasers hit them, there are no explosions. That’s because you haven’t added in the particle system for the explosion. Since you’ve already covered particle system addition for the stars, it should be a simple enough task to follow the original tutorial and add the explosions in. Consider it an extra credit assignment :]

Win/Lose Detection

Converting the win/lose detection code to Cocos2D-X is straightforward as well.

Switch back to HelloWorldScene.h and add an enum before the class declaration:

typedef enum {
  KENDREASONWIN,
  KENDREASONLOSE
} EndReason;

Now add a couple of private variables to the HelloWorldScene.h class:

double _gameOverTime;
bool _gameOver;

Next, add the definitions for two new private methods:

void endScene(EndReason endReason);
void restartTapped();

Then, switch to HelloWorldScene.cpp and add the following code to init(), before the return:

_lives = 3;
double curTime = getTimeTick();
_gameOverTime = curTime + 30000;

Add the following code to the end of update:

if (_lives <= 0) {
    _ship->stopAllActions();
    _ship->setVisible(false);
    this->endScene(KENDREASONLOSE);
} else if (curTimeMillis >= _gameOverTime) {
    this->endScene(KENDREASONWIN);
}

Finally, add the below method implementations to the end of the file:

void HelloWorld::restartTapped() {
    CCDirector::sharedDirector()->replaceScene
    (CCTransitionZoomFlipX::create(0.5, this->scene()));
    // reschedule
    this->scheduleUpdate();
}
 
void HelloWorld::endScene( EndReason endReason ) {
    if (_gameOver)
        return;
    _gameOver = true;
 
    CCSize winSize = CCDirector::sharedDirector()->getWinSize();
    char message[10] = "You Win";
    if ( endReason == KENDREASONLOSE)
        strcpy(message,"You Lose");
    CCLabelBMFont * label ;
    label = CCLabelBMFont::create(message, "Arial.fnt");
    label->setScale(0.1);
    label->setPosition(ccp(winSize.width/2 , winSize.height*0.6));
    this->addChild(label);
 
    CCLabelBMFont * restartLabel;
    strcpy(message,"Restart");
    restartLabel = CCLabelBMFont::create(message, "Arial.fnt");
    CCMenuItemLabel *restartItem = CCMenuItemLabel::create(restartLabel, this, menu_selector(HelloWorld::restartTapped) );
    restartItem->setScale(0.1);
    restartItem->setPosition( ccp(winSize.width/2, winSize.height*0.4));
 
    CCMenu *menu = CCMenu::create(restartItem, NULL);
    menu->setPosition(CCPointZero);
    this->addChild(menu);
 
    // clear label and menu
    restartItem->runAction(CCScaleTo::create(0.5, 1.0));
    label ->runAction(CCScaleTo::create(0.5, 1.0));
    // Terminate update callback
    this->unscheduleUpdate();
}

Note that one difference between the original version of the code and the Cocos2D-X version is that you have to schedule/unschedule the update callback when the game ends and begins.

Compile and Run, and if your ship gets hit too many times, you’ll die!

Gratuitous Music and Sound Effects

Android does not support the CAF sound file format, so the first thing you need to do is convert the original sound files to an alternate format that works on Android as well.

Open up a Terminal and issue the following commands (and don’t forget to replace $PROJECT_HOME in the first command with the actual path to the Android project – otherwise the command won’t work):

cd $PROJECT_HOME/Resources
afconvert -f WAVE -d UI8 SpaceGame.caf SpaceGame.wav
afconvert -f WAVE -d UI8 explosion_large.caf explosion_large.wav
afconvert -f WAVE -d UI8 laser_ship.caf laser_ship.wav

As you’ve probably guessed, the above commands will convert the files from CAF format to WAV format.

Once you’re done converting the files, go to your Xcode project and add the WAV converted files to your SharedResources folder (from the Android directory) – similar to how you added in the images and other resources earlier.

The hard part’s done – now to play these sounds! Add the following code to the top of HelloWorldScene.cpp:

#include "SimpleAudioEngine.h"
using namespace CocosDenshion;

Here you include the SimpleAudioEngine header file and say that you want to use symbols in its namespace.

The rest is a simple port of the original code to Cocos2D-X. Start by adding this code to the end of init(), before the return:

SimpleAudioEngine::sharedEngine()->playBackgroundMusic("SpaceGame.wav",true);
SimpleAudioEngine::sharedEngine()->preloadEffect("explosion_large.wav");
SimpleAudioEngine::sharedEngine()->preloadEffect("laser_ship.wav");

Next, add the following code to the Asteroids section in update where the intersectsRect test is made to determine if a laser collided with an asteroid. (You could also add a sound effect when a ship collides with an asteroid, which is tested for in the next intersectsRect test in the same section but since the ship doesn’t explode, you might want to make that a different sound :]):

SimpleAudioEngine::sharedEngine()->playEffect("explosion_large.wav");

Finally, add the following to the beginning of ccTouchesBegan:

SimpleAudioEngine::sharedEngine()->playEffect("laser_ship.wav");

Compile and Run, and enjoy the sound effects! Here is the final project running on an iPhone 4:

Final-work-iOS

And on an Android Nexus 7:

Final-work-Android

Platform-Specific Bugs and Tweaks

The Cocos2D-X developers do a great job, but nobody’s perfect, especially when it comes to ensuring cross-platform compatibility ;]

If you play around with the app, you’ll see that it works flawlessly on iOS. However, on Android you might find some of the following issues:

  • When closing/resuming the game, sprites set by CCParticleSystemQuad are not correctly repainted and you get white squared areas instead. This appears to be an issue with Cocos2D-X on Android.
  • Parallax scrolling is slow with visible refreshing sometimes.

Since these issues deal with Cocos2D-X itself, they are beyond the scope of this tutorial (and will likely be fixed in future version of Cocos2D-X).

Also, you might notice that the stars only show up on a limited portion of the screen. This is because when the particle systems were created with Particle Designer, they were set up assuming the iPhone’s screen dimensions.

To make them work properly on Android devices, just open up the particle systems and edit them according to the Android’s screen dimensions. These settings should do the trick for most devices: sourcePositiony 720, sourcePositionVariancey 720, and sourcePositionx 1088. However, depending on your device’s screen size, you might need to adjust those settings a bit.

Developing for Multiple Devices and Resolutions

When you develop for iOS, you have it easy – you just have four screen aspect ratios: iPhone4, iPhone5, . iPad and iPad mini, and normal/retina resolution for each.

However, it gets more complicated for the Android because there are numerous devices from multiple vendors, with varying screen sizes.

If you have a device with a different screen size than the iPhone (or a retina device), the game might not look quite right currently. But you want your game to look OK no matter what screen size the user is using!

The general idea is, you can scale the artwork based on the screen size according to the following formula:

factor = min(targetWidth / DesignWidth, targetHeight / DesignHeight)

You also should try to place artwork, when laying out scenes, based on multiples of the screen size, etc. rather than hardcoded coordinates.

Unfortunately this Cocos2D-X tutorial is long enough already and is only meant to be an intro to Cocos2D-X, so this will be left as an exercise for you guys! :]

Where To Go From Here?

Congratulations, you now have hands-on experience with Cocos2D-X and have created a simple cross-platform game!

Here is the example project from the above Cocos2D-X tutorial.

Note that I have cleaned up the project directory structure a bit so that the same root directory can be shared across iOS and Android projects. If you’re interested in more details on how to set this up, I’d be glad to discuss more in the forums!

If you have any questions on this Cocos2D-X tutorial or Cocos2D-X in general, please join the forum discussion below!

Jean-Yves Mengant is an experienced Android / IOS developer and designer.

User Comments

31 Comments

[ 1 , 2 , 3 ]
  • can I use your ART RESOURCES here for my space shooter app??? Though I'd ask first ^__^
    I'm just an aspiring dev and I dont have a graphics designer with me.
    heliosnarcissus
[ 1 , 2 , 3 ]

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 September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Tammy Coron
  • Kirill Muzykov

... 49 total!

Update Team

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Heejun Han
  • Jesus Guerra
  • David Xie

... 33 total!

Subject Matter Experts

... 4 total!