Cocos2D-X Tutorial for iOS and Android: Space Game

In this Cocos2D-X tutorial, you will learn 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! By .

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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!