35 min read

This article is written by Roger Engelbert, the author of Cocos2d-x by Example: Beginner’s Guide – Second Edition.

Time to build our second game! This time, you will get acquainted with the power of actions in Cocos2d-x. I’ll show you how an entire game could be built just by running the various action commands contained in Cocos2d-x to make your sprites move, rotate, scale, fade, blink, and so on. And you can also use actions to animate your sprites using multiple images, like in a movie. So let’s get started.

In this article, you will learn:

  • How to optimize the development of your game with sprite sheets
  • How to use bitmap fonts in your game
  • How easy it is to implement and run actions
  • How to scale, rotate, swing, move, and fade out a sprite
  • How to load multiple .png files and use them to animate a sprite
  • How to create a universal game with Cocos2d-x

(For more resources related to this topic, see here.)

The game – sky defense

Meet our stressed-out city of…your name of choice here. It’s a beautiful day when suddenly the sky begins to fall. There are meteors rushing toward the city and it is your job to keep it safe.

The player in this game can tap the screen to start growing a bomb. When the bomb is big enough to be activated, the player taps the screen again to detonate it. Any nearby meteor will explode into a million pieces. The bigger the bomb, the bigger the detonation, and the more meteors can be taken out by it. But the bigger the bomb, the longer it takes to grow it.

But it’s not just bad news coming down. There are also health packs dropping from the sky and if you allow them to reach the ground, you’ll recover some of your energy.

The game settings

This is a universal game. It is designed for the iPad retina screen and it will be scaled down to fit all the other screens. The game will be played in landscape mode, and it will not need to support multitouch.

The start project

The command line I used was:

cocos new SkyDefense -p com.rengelbert.SkyDefense -l cpp -d /Users/rengelbert/Desktop/SkyDefense

In Xcode you must set the Devices field in Deployment Info to Universal, and the Device Family field is set to Universal. And in RootViewController.mm, the supported interface orientation is set to Landscape.

The game we are going to build requires only one class, GameLayer.cpp, and you will find that the interface for this class already contains all the information it needs.

Also, some of the more trivial or old-news logic is already in place in the implementation file as well. But I’ll go over this as we work on the game.

Adding screen support for a universal app

Now things get a bit more complicated as we add support for smaller screens in our universal game, as well as some of the most common Android screen sizes.

So open AppDelegate.cpp. Inside the applicationDidFinishLaunching method, we now have the following code:

auto screenSize = glview->getFrameSize();
auto designSize = Size(2048, 1536);
glview->setDesignResolutionSize(designSize.width, designSize.height, ResolutionPolicy::EXACT_FIT);
std::vector<std::string> searchPaths;
if (screenSize.height > 768) {
   searchPaths.push_back("ipadhd");
   director->setContentScaleFactor(1536/designSize.height);
} else if (screenSize.height > 320) {
   searchPaths.push_back("ipad");
   director->setContentScaleFactor(768/designSize.height);
} else {
   searchPaths.push_back("iphone");
   director->setContentScaleFactor(380/designSize.height);
}
auto fileUtils = FileUtils::getInstance();
fileUtils->setSearchPaths(searchPaths);

Once again, we tell our GLView object (our OpenGL view) that we designed the game for a certain screen size (the iPad retina screen) and once again, we want our game screen to resize to match the screen on the device (ResolutionPolicy::EXACT_FIT).

Then we determine where to load our images from, based on the device’s screen size. We have art for iPad retina, then for regular iPad which is shared by iPhone retina, and for the regular iPhone.

We end by setting the scale factor based on the designed target.

Adding background music

Still inside AppDelegate.cpp, we load the sound files we’ll use in the game, including a background.mp3 (courtesy of Kevin MacLeod from incompetech.com), which we load through the command:

auto audioEngine = SimpleAudioEngine::getInstance();
audioEngine->preloadBackgroundMusic(fileUtils->fullPathForFilename("background.mp3").c_str());

We end by setting the effects’ volume down a tad:

//lower playback volume for effects
audioEngine->setEffectsVolume(0.4f);

For background music volume, you must use setBackgroundMusicVolume. If you create some sort of volume control in your game, these are the calls you would make to adjust the volume based on the user’s preference.

Initializing the game

Now back to GameLayer.cpp. If you take a look inside our init method, you will see that the game initializes by calling three methods: createGameScreen, createPools, and createActions.

We’ll create all our screen elements inside the first method, and then create object pools so we don’t instantiate any sprite inside the main loop; and we’ll create all the main actions used in our game inside the createActions method.

And as soon as the game initializes, we start playing the background music, with its should loop parameter set to true:

SimpleAudioEngine::getInstance()-   >playBackgroundMusic("background.mp3", true);

We once again store the screen size for future reference, and we’ll use a _running Boolean for game states.

If you run the game now, you should only see the background image:

Cocos2d-x by Example: Beginner's Guide - Second Edition

Using sprite sheets in Cocos2d-x

A sprite sheet is a way to group multiple images together in one image file. In order to texture a sprite with one of these images, you must have the information of where in the sprite sheet that particular image is found (its rectangle).

Sprite sheets are often organized in two files: the image one and a data file that describes where in the image you can find the individual textures.

I used TexturePacker to create these files for the game. You can find them inside the ipad, ipadhd, and iphone folders inside Resources. There is a sprite_sheet.png file for the image and a sprite_sheet.plist file that describes the individual frames inside the image.

This is what the sprite_sheet.png file looks like:

Cocos2d-x by Example: Beginner's Guide - Second Edition

Batch drawing sprites

In Cocos2d-x, sprite sheets can be used in conjunction with a specialized node, called SpriteBatchNode. This node can be used whenever you wish to use multiple sprites that share the same source image inside the same node. So you could have multiple instances of a Sprite class that uses a bullet.png texture for instance. And if the source image is a sprite sheet, you can have multiple instances of sprites displaying as many different textures as you could pack inside your sprite sheet.

With SpriteBatchNode, you can substantially reduce the number of calls during the rendering stage of your game, which will help when targeting less powerful systems, though not noticeably in more modern devices.

Let me show you how to create a SpriteBatchNode.

Time for action – creating SpriteBatchNode

Let’s begin implementing the createGameScreen method in GameLayer.cpp. Just below the lines that add the bg sprite, we instantiate our batch node:

void GameLayer::createGameScreen() {
 
//add bg
auto bg = Sprite::create("bg.png");
...
 
SpriteFrameCache::getInstance()->
addSpriteFramesWithFile("sprite_sheet.plist");
_gameBatchNode = SpriteBatchNode::create("sprite_sheet.png");
this->addChild(_gameBatchNode);

In order to create the batch node from a sprite sheet, we first load all the frame information described by the sprite_sheet.plist file into SpriteFrameCache. And then we create the batch node with the sprite_sheet.png file, which is the source texture shared by all sprites added to this batch node. (The background image is not part of the sprite sheet, so it’s added separately before we add _gameBatchNode to GameLayer.)

Now we can start putting stuff inside _gameBatchNode.

  1. First, the city:
    for (int i = 0; i < 2; i++) {
    auto sprite = Sprite::createWithSpriteFrameName   ("city_dark.png");
       sprite->setAnchorPoint(Vec2(0.5,0));
    sprite->setPosition(_screenSize.width * (0.25f + i * 0.5f),0));
    _gameBatchNode->addChild(sprite, kMiddleground);
    sprite = Sprite::createWithSpriteFrameName ("city_light.png");
    sprite->setAnchorPoint(Vec2(0.5,0));
    sprite->setPosition(Vec2(_screenSize.width * (0.25f + i * 0.5f),
    _screenSize.height * 0.1f));
    _gameBatchNode->addChild(sprite, kBackground);
    }
  2. Then the trees:
    //add trees
    for (int i = 0; i < 3; i++) {
    auto sprite = Sprite::createWithSpriteFrameName("trees.png");
    sprite->setAnchorPoint(Vec2(0.5f, 0.0f));
    sprite->setPosition(Vec2(_screenSize.width * (0.2f + i * 0.3f),0));
    _gameBatchNode->addChild(sprite, kForeground);
     
    }

    Notice that here we create sprites by passing it a sprite frame name. The IDs for these frame names were loaded into SpriteFrameCache through our sprite_sheet.plist file.

  3. The screen so far is made up of two instances of city_dark.png tiling at the bottom of the screen, and two instances of city_light.png also tiling. One needs to appear on top of the other and for that we use the enumerated values declared in GameLayer.h:
    enum {
    kBackground,
    kMiddleground,
    kForeground
    };
  4. We use the addChild( Node, zOrder) method to layer our sprites on top of each other, using different values for their z order.

    So for example, when we later add three sprites showing the trees.png sprite frame, we add them on top of all previous sprites using the highest value for z that we find in the enumerated list, which is kForeground.

Why go through the trouble of tiling the images and not using one large image instead, or combining some of them with the background image? Because I wanted to include the greatest number of images possible inside the one sprite sheet, and have that sprite sheet to be as small as possible, to illustrate all the clever ways you can use and optimize sprite sheets. This is not necessary in this particular game.

What just happened?

We began creating the initial screen for our game. We are using a SpriteBatchNode to contain all the sprites that use images from our sprite sheet. So SpriteBatchNode behaves as any node does—as a container. And we can layer individual sprites inside the batch node by manipulating their z order.

Bitmap fonts in Cocos2d-x

The Cocos2d-x Label class has a static create method that uses bitmap images for the characters.

The bitmap image we are using here was created with the program GlyphDesigner, and in essence, it works just as a sprite sheet does. As a matter of fact, Label extends SpriteBatchNode, so it behaves just like a batch node.

You have images for all individual characters you’ll need packed inside a PNG file (font.png), and then a data file (font.fnt) describing where each character is. The following screenshot shows how the font sprite sheet looks like for our game:

Cocos2d-x by Example: Beginner's Guide - Second Edition

The difference between Label and a regular SpriteBatchNode class is that the data file also feeds the Label object information on how to write with this font. In other words, how to space out the characters and lines correctly.

The Label objects we are using in the game are instantiated with the name of the data file and their initial string value:

_scoreDisplay = Label::createWithBMFont("font.fnt", "0");

And the value for the label is changed through the setString method:

_scoreDisplay->setString("1000");

Just as with every other image in the game, we also have different versions of font.fnt and font.png in our Resources folders, one for each screen definition. FileUtils will once again do the heavy lifting of finding the correct file for the correct screen.

So now let’s create the labels for our game.

Time for action – creating bitmap font labels

Creating a bitmap font is somewhat similar to creating a batch node.

  1. Continuing with our createGameScreen method, add the following lines to the score label:
    _scoreDisplay = Label::createWithBMFont("font.fnt", "0");
    _scoreDisplay->setAnchorPoint(Vec2(1,0.5));
    _scoreDisplay->setPosition(Vec2   (_screenSize.width * 0.8f, _screenSize.height * 0.94f));
    this->addChild(_scoreDisplay);

    And then add a label to display the energy level, and set its horizontal alignment to Right:

    _energyDisplay = Label::createWithBMFont("font.fnt", "100%", TextHAlignment::RIGHT);
    _energyDisplay->setPosition(Vec2   (_screenSize.width * 0.3f, _screenSize.height * 0.94f));
    this->addChild(_energyDisplay);
  2. Add the following line for an icon that appears next to the _energyDisplay label:
    auto icon = Sprite::createWithSpriteFrameName ("health_icon.png");
    icon->setPosition( Vec2(_screenSize.   width * 0.15f, _screenSize.height * 0.94f) );
    _gameBatchNode->addChild(icon, kBackground);

What just happened?

We just created our first bitmap font object in Cocos2d-x. Now let’s finish creating our game’s sprites.

Time for action – adding the final screen sprites

The last sprites we need to create are the clouds, the bomb and shockwave, and our game state messages.

  1. Back to the createGameScreen method, add the clouds to the screen:
    for (int i = 0; i < 4; i++) {
    float cloud_y = i % 2 == 0 ? _screenSize.height * 0.4f : _screenSize.height * 0.5f;
    auto cloud = Sprite::createWithSpriteFrameName("cloud.png");
    cloud->setPosition(Vec2 (_screenSize.width * 0.1f + i * _screenSize.width * 0.3f, cloud_y));
    _gameBatchNode->addChild(cloud, kBackground);
    _clouds.pushBack(cloud);
    }
  2. Create the _bomb sprite; players will grow when tapping the screen:
    _bomb = Sprite::createWithSpriteFrameName("bomb.png");
    _bomb->getTexture()->generateMipmap();
    _bomb->setVisible(false);
     
    auto size = _bomb->getContentSize();
     
    //add sparkle inside bomb sprite
    auto sparkle = Sprite::createWithSpriteFrameName("sparkle.png");
    sparkle->setPosition(Vec2(size.width * 0.72f, size.height *   0.72f));
    _bomb->addChild(sparkle, kMiddleground, kSpriteSparkle);
     
    //add halo inside bomb sprite
    auto halo = Sprite::createWithSpriteFrameName   ("halo.png");
    halo->setPosition(Vec2(size.width * 0.4f, size.height *   0.4f));
    _bomb->addChild(halo, kMiddleground, kSpriteHalo);
    _gameBatchNode->addChild(_bomb, kForeground);
  3. Then create the _shockwave sprite that appears after the _bomb goes off:
    _shockWave = Sprite::createWithSpriteFrameName ("shockwave.png");
    _shockWave->getTexture()->generateMipmap();
    _shockWave->setVisible(false);
    _gameBatchNode->addChild(_shockWave);
  4. Finally, add the two messages that appear on the screen, one for our intro state and one for our gameover state:
    _introMessage = Sprite::createWithSpriteFrameName ("logo.png");
    _introMessage->setPosition(Vec2   (_screenSize.width * 0.5f, _screenSize.height * 0.6f));
    _introMessage->setVisible(true);
    this->addChild(_introMessage, kForeground);
     
    _gameOverMessage = Sprite::createWithSpriteFrameName   ("gameover.png");
    _gameOverMessage->setPosition(Vec2   (_screenSize.width * 0.5f, _screenSize.height * 0.65f));
    _gameOverMessage->setVisible(false);
    this->addChild(_gameOverMessage, kForeground);

What just happened?

There is a lot of new information regarding sprites in the previous code. So let’s go over it carefully:

  • We started by adding the clouds. We put the sprites inside a vector so we can move the clouds later. Notice that they are also part of our batch node.
  • Next comes the bomb sprite and our first new call:
    _bomb->getTexture()->generateMipmap();
  • With this we are telling the framework to create antialiased copies of this texture in diminishing sizes (mipmaps), since we are going to scale it down later. This is optional of course; sprites can be resized without first generating mipmaps, but if you notice loss of quality in your scaled sprites, you can fix that by creating mipmaps for their texture.

    The texture must have size values in so-called POT (power of 2: 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, and so on). Textures in OpenGL must always be sized this way; when they are not, Cocos2d-x will do one of two things: it will either resize the texture in memory, adding transparent pixels until the image reaches a POT size, or stop the execution on an assert. With textures used for mipmaps, the framework will stop execution for non-POT textures.

  • I add the sparkle and the halo sprites as children to the _bomb sprite. This will use the container characteristic of nodes to our advantage. When I grow the bomb, all its children will grow with it.
  • Notice too that I use a third parameter to addChild for halo and sparkle:
    bomb->addChild(halo, kMiddleground, kSpriteHalo);
  • This third parameter is an integer tag from yet another enumerated list declared in GameLayer.h. I can use this tag to retrieve a particular child from a sprite as follows:
    auto halo = (Sprite *)   bomb->getChildByTag(kSpriteHalo);

We now have our game screen in place:

Cocos2d-x by Example: Beginner's Guide - Second Edition

Next come object pools.

Time for action – creating our object pools

The pools are just vectors of objects. And here are the steps to create them:

  1. Inside the createPools method, we first create a pool for meteors:
    void GameLayer::createPools() {
    int i;
    _meteorPoolIndex = 0;
    for (i = 0; i < 50; i++) {
    auto sprite = Sprite::createWithSpriteFrameName("meteor.png");
    sprite->setVisible(false);
    _gameBatchNode->addChild(sprite, kMiddleground, kSpriteMeteor);
    _meteorPool.pushBack(sprite);
    }
  2. Then we create an object pool for health packs:
    _healthPoolIndex = 0;
    for (i = 0; i < 20; i++) {
    auto sprite = Sprite::createWithSpriteFrameName("health.png");
    sprite->setVisible(false);
    sprite->setAnchorPoint(Vec2(0.5f, 0.8f));
    _gameBatchNode->addChild(sprite, kMiddleground, kSpriteHealth);
    _healthPool.pushBack(sprite);
    }
  3. We’ll use the corresponding pool index to retrieve objects from the vectors as the game progresses.

What just happened?

We now have a vector of invisible meteor sprites and a vector of invisible health sprites. We’ll use their respective pool indices to retrieve these from the vector as needed as you’ll see in a moment. But first we need to take care of actions and animations.

With object pools, we reduce the number of instantiations during the main loop, and it allows us to never destroy anything that can be reused. But if you need to remove a child from a node, use ->removeChild or ->removeChildByTag if a tag is present.

Actions in a nutshell

If you remember, a node will store information about position, scale, rotation, visibility, and opacity of a node. And in Cocos2d-x, there is an Action class to change each one of these values over time, in effect animating these transformations.

Actions are usually created with a static method create. The majority of these actions are time-based, so usually the first parameter you need to pass an action is the time length for the action. So for instance:

auto fadeout = FadeOut::create(1.0f);

This creates a fadeout action that will take one second to complete. You can run it on a sprite, or node, as follows:

mySprite->runAction(fadeout);

Cocos2d-x has an incredibly flexible system that allows us to create any combination of actions and transformations to achieve any effect we desire.

You may, for instance, choose to create an action sequence (Sequence) that contains more than one action; or you can apply easing effects (EaseIn, EaseOut, and so on) to your actions. You can choose to repeat an action a certain number of times (Repeat) or forever (RepeatForever); and you can add callbacks to functions you want called once an action is completed (usually inside a Sequence action).

Time for action – creating actions with Cocos2d-x

Creating actions with Cocos2d-x is a very simple process:

  1. Inside our createActions method, we will instantiate the actions we can use repeatedly in our game. Let’s create our first actions:
    void GameLayer::createActions() {
    //swing action for health drops
    auto easeSwing = Sequence::create(
    EaseInOut::create(RotateTo::create(1.2f, -10), 2),
    EaseInOut::create(RotateTo::create(1.2f, 10), 2),
    nullptr);//mark the end of a sequence with a nullptr
    _swingHealth = RepeatForever::create( (ActionInterval *) easeSwing );
    _swingHealth->retain();
  2. Actions can be combined in many different forms. Here, the retained _swingHealth action is a RepeatForever action of Sequence that will rotate the health sprite first one way, then the other, with EaseInOut wrapping the RotateTo action. RotateTo takes 1.2 seconds to rotate the sprite first to -10 degrees and then to 10. And the easing has a value of 2, which I suggest you experiment with to get a sense of what it means visually. Next we add three more actions:
    //action sequence for shockwave: fade out, callback when //done
    _shockwaveSequence = Sequence::create(
    FadeOut::create(1.0f),
    CallFunc::create(std::bind(&GameLayer::shockwaveDone, this)), nullptr);
    _shockwaveSequence->retain();
     
    //action to grow bomb
    _growBomb = ScaleTo::create(6.0f, 1.0);
    _growBomb->retain();
     
    //action to rotate sprites
    auto rotate = RotateBy::create(0.5f , -90);
    _rotateSprite = RepeatForever::create( rotate );
    _rotateSprite->retain();
  3. First, another Sequence. This will fade out the sprite and call the shockwaveDone function, which is already implemented in the class and turns the _shockwave sprite invisible when called.
  4. The last one is a RepeatForever action of a RotateBy action. In half a second, the sprite running this action will rotate -90 degrees and will do that again and again.

What just happened?

You just got your first glimpse of how to create actions in Cocos2d-x and how the framework allows for all sorts of combinations to accomplish any effect.

It may be hard at first to read through a Sequence action and understand what’s happening, but the logic is easy to follow once you break it down into its individual parts.

But we are not done with the createActions method yet. Next come sprite animations.

Animating a sprite in Cocos2d-x

The key thing to remember is that an animation is just another type of action, one that changes the texture used by a sprite over a period of time.

In order to create an animation action, you need to first create an Animation object. This object will store all the information regarding the different sprite frames you wish to use in the animation, the length of the animation in seconds, and whether it loops or not.

With this Animation object, you then create a Animate action. Let’s take a look.

Time for action – creating animations

Animations are a specialized type of action that require a few extra steps:

  1. Inside the same createActions method, add the lines for the two animations we have in the game. First, we start with the animation that shows an explosion when a meteor reaches the city. We begin by loading the frames into an Animation object:
    auto animation = Animation::create();
    int i;
    for(i = 1; i <= 10; i++) {
    auto name = String::createWithFormat("boom%i.png", i);
    auto frame = SpriteFrameCache::getInstance()->getSpriteFrameByName(name->getCString());
    animation->addSpriteFrame(frame);
    }
  2. Then we use the Animation object inside a Animate action:
    animation->setDelayPerUnit(1 / 10.0f);
    animation->setRestoreOriginalFrame(true);
    _groundHit =
    Sequence::create(
       MoveBy::create(0, Vec2(0,_screenSize.height * 0.12f)),
       Animate::create(animation),
       CallFuncN::create(CC_CALLBACK_1(GameLayer::animationDone, this)), nullptr);
    _groundHit->retain();
  3. The same steps are repeated to create the other explosion animation used when the player hits a meteor or a health pack.
    animation = Animation::create();
    for(int i = 1; i <= 7; i++) {
    auto name = String::createWithFormat("explosion_small%i.png", i);
    auto frame = SpriteFrameCache::getInstance()->getSpriteFrameByName(name->getCString());
    animation->addSpriteFrame(frame);
    }
     
    animation->setDelayPerUnit(0.5 / 7.0f);
    animation->setRestoreOriginalFrame(true);
    _explosion = Sequence::create(
         Animate::create(animation),
       CallFuncN::create(CC_CALLBACK_1(GameLayer::animationDone, this)), nullptr);
    _explosion->retain();

What just happened?

We created two instances of a very special kind of action in Cocos2d-x: Animate. Here is what we did:

  • First, we created an Animation object. This object holds the references to all the textures used in the animation. The frames were named in such a way that they could easily be concatenated inside a loop (boom1, boom2, boom3, and so on). There are 10 frames for the first animation and seven for the second.
  • The textures (or frames) are SpriteFrame objects we grab from SpriteFrameCache, which as you remember, contains all the information from the sprite_sheet.plist data file. So the frames are in our sprite sheet.
  • Then when all frames are in place, we determine the delay of each frame by dividing the total amount of seconds we want the animation to last by the total number of frames.
  • The setRestoreOriginalFrame method is important here. If we set setRestoreOriginalFrame to true, then the sprite will revert to its original appearance once the animation is over. For example, if I have an explosion animation that will run on a meteor sprite, then by the end of the explosion animation, the sprite will revert to displaying the meteor texture.
  • Time for the actual action. Animate receives the Animation object as its parameter. (In the first animation, we shift the position of the sprite just before the explosion appears, so there is an extra MoveBy method.)
  • And in both instances, I make a call to an animationDone callback already implemented in the class. It makes the calling sprite invisible:
    void GameLayer::animationDone (Node* pSender) {
    pSender->setVisible(false);
    }

We could have used the same method for both callbacks (animationDone and shockwaveDone) as they accomplish the same thing. But I wanted to show you a callback that receives as an argument, the node that made the call and one that did not. Respectively, these are CallFuncN and CallFunc, and were used inside the action sequences we just created.

Time to make our game tick!

Okay, we have our main elements in place and are ready to add the final bit of logic to run the game. But how will everything work?

We will use a system of countdowns to add new meteors and new health packs, as well as a countdown that will incrementally make the game harder to play.

On touch, the player will start the game if the game is not running, and also add bombs and explode them during gameplay. An explosion creates a shockwave.

On update, we will check against collision between our _shockwave sprite (if visible) and all our falling objects. And that’s it. Cocos2d-x will take care of all the rest through our created actions and callbacks!

So let’s implement our touch events first.

Time for action – handling touches

Time to bring the player to our party:

  1. Time to implement our onTouchBegan method. We’ll begin by handling the two game states, intro and game over:
    bool GameLayer::onTouchBegan (Touch * touch, Event * event){
     
    //if game not running, we are seeing either intro or //gameover
    if (!_running) {
       //if intro, hide intro message
       if (_introMessage->isVisible()) {
         _introMessage->setVisible(false);
     
         //if game over, hide game over message
       } else if (_gameOverMessage->isVisible()) {
         SimpleAudioEngine::getInstance()->stopAllEffects();
         _gameOverMessage->setVisible(false);
        
       }
      
       this->resetGame();
       return true;
    }
  2. Here we check to see if the game is not running. If not, we check to see if any of our messages are visible. If _introMessage is visible, we hide it. If _gameOverMessage is visible, we stop all current sound effects and hide the message as well. Then we call a method called resetGame, which will reset all the game data (energy, score, and countdowns) to their initial values, and set _running to true.
  3. Next we handle the touches. But we only need to handle one each time so we use ->anyObject() on Set:
    auto touch = (Touch *)pTouches->anyObject();
     
    if (touch) {
    //if bomb already growing...
    if (_bomb->isVisible()) {
       //stop all actions on bomb, halo and sparkle
       _bomb->stopAllActions();
       auto child = (Sprite *) _bomb->getChildByTag(kSpriteHalo);
       child->stopAllActions();
       child = (Sprite *) _bomb->getChildByTag(kSpriteSparkle);
       child->stopAllActions();
      
       //if bomb is the right size, then create shockwave
       if (_bomb->getScale() > 0.3f) {
         _shockWave->setScale(0.1f);
         _shockWave->setPosition(_bomb->getPosition());
         _shockWave->setVisible(true);
         _shockWave->runAction(ScaleTo::create(0.5f, _bomb->getScale() * 2.0f));
         _shockWave->runAction(_shockwaveSequence->clone());
         SimpleAudioEngine::getInstance()->playEffect("bombRelease.wav");
     
       } else {
         SimpleAudioEngine::getInstance()->playEffect("bombFail.wav");
       }
       _bomb->setVisible(false);
       //reset hits with shockwave, so we can count combo hits
       _shockwaveHits = 0;
    //if no bomb currently on screen, create one
    } else {
       Point tap = touch->getLocation();
       _bomb->stopAllActions();
       _bomb->setScale(0.1f);
       _bomb->setPosition(tap);
       _bomb->setVisible(true);
       _bomb->setOpacity(50);
       _bomb->runAction(_growBomb->clone());
      
         auto child = (Sprite *) _bomb->getChildByTag(kSpriteHalo);
         child->runAction(_rotateSprite->clone());
         child = (Sprite *) _bomb->getChildByTag(kSpriteSparkle);
         child->runAction(_rotateSprite->clone());
    }
    }
  4. If _bomb is visible, it means it’s already growing on the screen. So on touch, we use the stopAllActions() method on the bomb and we use the stopAllActions() method on its children that we retrieve through our tags:
    child = (Sprite *) _bomb->getChildByTag(kSpriteHalo);
    child->stopAllActions();
    child = (Sprite *) _bomb->getChildByTag(kSpriteSparkle);
    child->stopAllActions();
  5. If _bomb is the right size, we start our _shockwave. If it isn’t, we play a bomb failure sound effect; there is no explosion and _shockwave is not made visible.
  6. If we have an explosion, then the _shockwave sprite is set to 10 percent of the scale. It’s placed at the same spot as the bomb, and we run a couple of actions on it: we grow the _shockwave sprite to twice the scale the bomb was when it went off and we run a copy of _shockwaveSequence that we created earlier.
  7. Finally, if no _bomb is currently visible on screen, we create one. And we run clones of previously created actions on the _bomb sprite and its children. When _bomb grows, its children grow. But when the children rotate, the bomb does not: a parent changes its children, but the children do not change their parent.

What just happened?

We just added part of the core logic of the game. It is with touches that the player creates and explodes bombs to stop meteors from reaching the city. Now we need to create our falling objects. But first, let’s set up our countdowns and our game data.

Time for action – starting and restarting the game

Let’s add the logic to start and restart the game.

  1. Let’s write the implementation for resetGame:
    void GameLayer::resetGame(void) {
       _score = 0;
       _energy = 100;
      
       //reset timers and "speeds"
       _meteorInterval = 2.5;
       _meteorTimer = _meteorInterval * 0.99f;
       _meteorSpeed = 10;//in seconds to reach ground
       _healthInterval = 20;
       _healthTimer = 0;
       _healthSpeed = 15;//in seconds to reach ground
      
       _difficultyInterval = 60;
       _difficultyTimer = 0;
      
       _running = true;
      
       //reset labels
       _energyDisplay->setString(std::to_string((int) _energy) + "%");
       _scoreDisplay->setString(std::to_string((int) _score));
    }
  2. Next, add the implementation of stopGame:
    void GameLayer::stopGame() {
      
       _running = false;
      
       //stop all actions currently running
       int i;
       int count = (int) _fallingObjects.size();
      
       for (i = count-1; i >= 0; i--) {
           auto sprite = _fallingObjects.at(i);
           sprite->stopAllActions();
           sprite->setVisible(false);
           _fallingObjects.erase(i);
       }
       if (_bomb->isVisible()) {
           _bomb->stopAllActions();
           _bomb->setVisible(false);
           auto child = _bomb->getChildByTag(kSpriteHalo);
           child->stopAllActions();
           child = _bomb->getChildByTag(kSpriteSparkle);
           child->stopAllActions();
       }
       if (_shockWave->isVisible()) {
           _shockWave->stopAllActions();
           _shockWave->setVisible(false);
       }
       if (_ufo->isVisible()) {
           _ufo->stopAllActions();
           _ufo->setVisible(false);
           auto ray = _ufo->getChildByTag(kSpriteRay);
          ray->stopAllActions();
           ray->setVisible(false);
       }
    }

What just happened?

With these methods we control gameplay. We start the game with default values through resetGame(), and we stop all actions with stopGame().

Already implemented in the class is the method that makes the game more difficult as time progresses. If you take a look at the method (increaseDifficulty) you will see that it reduces the interval between meteors and reduces the time it takes for meteors to reach the ground.

All we need now is the update method to run the countdowns and check for collisions.

Time for action – updating the game

We already have the code that updates the countdowns inside the update. If it’s time to add a meteor or a health pack we do it. If it’s time to make the game more difficult to play, we do that too.

It is possible to use an action for these timers: a Sequence action with a Delay action object and a callback. But there are advantages to using these countdowns. It’s easier to reset them and to change them, and we can take them right into our main loop.

So it’s time to add our main loop:

  1. What we need to do is check for collisions. So add the following code:
    if (_shockWave->isVisible()) {
    count = (int) _fallingObjects.size();
    for (i = count-1; i >= 0; i--) {
       auto sprite = _fallingObjects.at(i);
       diffx = _shockWave->getPositionX() - sprite->getPositionX();
       diffy = _shockWave->getPositionY() - sprite->getPositionY();
       if (pow(diffx, 2) + pow(diffy, 2) <= pow(_shockWave->getBoundingBox().size.width * 0.5f, 2)) {
       sprite->stopAllActions();
       sprite->runAction( _explosion->clone());
       SimpleAudioEngine::getInstance()->playEffect("boom.wav");
       if (sprite->getTag() == kSpriteMeteor) {
         _shockwaveHits++;
         _score += _shockwaveHits * 13 + _shockwaveHits * 2;
       }
       //play sound
       _fallingObjects.erase(i);
    }
    }
    _scoreDisplay->setString(std::to_string(_score));
    }
  2. If _shockwave is visible, we check the distance between it and each sprite in _fallingObjects vector. If we hit any meteors, we increase the value of the _shockwaveHits property so we can award the player for multiple hits. Next we move the clouds:
    //move clouds
    for (auto sprite : _clouds) {
    sprite->setPositionX(sprite->getPositionX() + dt * 20);
    if (sprite->getPositionX() > _screenSize.width + sprite->getBoundingBox().size.width * 0.5f)
       sprite->setPositionX(-sprite->getBoundingBox().size.width * 0.5f);
    }
  3. I chose not to use a MoveTo action for the clouds to show you the amount of code that can be replaced by a simple action. If not for Cocos2d-x actions, we would have to implement logic to move, rotate, swing, scale, and explode all our sprites!
  4. And finally:
    if (_bomb->isVisible()) {
       if (_bomb->getScale() > 0.3f) {
         if (_bomb->getOpacity() != 255)
           _bomb->setOpacity(255);
       }
    }
  5. We give the player an extra visual cue to when a bomb is ready to explode by changing its opacity.

What just happened?

The main loop is pretty straightforward when you don’t have to worry about updating individual sprites, as our actions take care of that for us. We pretty much only need to run collision checks between our sprites, and to determine when it’s time to throw something new at the player.

So now the only thing left to do is grab the meteors and health packs from the pools when their timers are up. So let’s get right to it.

Time for action – retrieving objects from the pool

We just need to use the correct index to retrieve the objects from their respective vector:

  1. To retrieve meteor sprites, we’ll use the resetMeteor method:
    void GameLayer::resetMeteor(void) {
       //if too many objects on screen, return
       if (_fallingObjects.size() > 30) return;
      
       auto meteor = _meteorPool.at(_meteorPoolIndex);
         _meteorPoolIndex++;
       if (_meteorPoolIndex == _meteorPool.size())
         _meteorPoolIndex = 0;
         int meteor_x = rand() % (int) (_screenSize.width * 0.8f) + _screenSize.width * 0.1f;
       int meteor_target_x = rand() % (int) (_screenSize.width * 0.8f) + _screenSize.width * 0.1f;
      
       meteor->stopAllActions();
       meteor->setPosition(Vec2(meteor_x, _screenSize.height + meteor->getBoundingBox().size.height * 0.5));
       //create action
       auto rotate = RotateBy::create(0.5f , -90);
       auto repeatRotate = RepeatForever::create( rotate );
       auto sequence = Sequence::create (
                   MoveTo::create(_meteorSpeed, Vec2(meteor_target_x, _screenSize.height * 0.15f)),
                   CallFunc::create(std::bind(&GameLayer::fallingObjectDone, this, meteor) ), nullptr);  
    meteor->setVisible ( true );
    meteor->runAction(repeatRotate);
    meteor->runAction(sequence);
    _fallingObjects.pushBack(meteor);
    }
  2. We grab the next available meteor from the pool, then we pick a random start and end x value for its MoveTo action. The meteor starts at the top of the screen and will move to the bottom towards the city, but the x value is randomly picked each time.
  3. We rotate the meteor inside a RepeatForever action, and we use Sequence to move the sprite to its target position and then call back fallingObjectDone when the meteor has reached its target. We finish by adding the new meteor we retrieved from the pool to the _fallingObjects vector so we can check collisions with it.
  4. The method to retrieve the health (resetHealth) sprites is pretty much the same, except that swingHealth action is used instead of rotate. You’ll find that method already implemented in GameLayer.cpp.

What just happened?

So in resetGame we set the timers, and we update them in the update method. We use these timers to add meteors and health packs to the screen by grabbing the next available one from their respective pool, and then we proceed to run collisions between an exploding bomb and these falling objects.

Notice that in both resetMeteor and resetHealth we don’t add new sprites if too many are on screen already:

if (_fallingObjects->size() > 30) return;

This way the game does not get ridiculously hard, and we never run out of unused objects in our pools.

And the very last bit of logic in our game is our fallingObjectDone callback, called when either a meteor or a health pack has reached the ground, at which point it awards or punishes the player for letting sprites through.

When you take a look at that method inside GameLayer.cpp, you will notice how we use ->getTag() to quickly ascertain which type of sprite we are dealing with (the one calling the method):

if (pSender->getTag() == kSpriteMeteor) {

If it’s a meteor, we decrease energy from the player, play a sound effect, and run the explosion animation; an autorelease copy of the _groundHit action we retained earlier, so we don’t need to repeat all that logic every time we need to run this action.

If the item is a health pack, we increase the energy or give the player some points, play a nice sound effect, and hide the sprite.

Play the game!

We’ve been coding like mad, and it’s finally time to run the game. But first, don’t forget to release all the items we retained. In GameLayer.cpp, add our destructor method:

GameLayer::~GameLayer () {
  
   //release all retained actions
   CC_SAFE_RELEASE(_growBomb);
   CC_SAFE_RELEASE(_rotateSprite);
   CC_SAFE_RELEASE(_shockwaveSequence);
   CC_SAFE_RELEASE(_swingHealth);
   CC_SAFE_RELEASE(_groundHit);
   CC_SAFE_RELEASE(_explosion);
   CC_SAFE_RELEASE(_ufoAnimation);
   CC_SAFE_RELEASE(_blinkRay);
  
   _clouds.clear();
   _meteorPool.clear();
   _healthPool.clear();
   _fallingObjects.clear();
}

The actual game screen will now look something like this:

Cocos2d-x by Example: Beginner's Guide - Second Edition

Now, let’s take this to Android.

Time for action – running the game in Android

Follow these steps to deploy the game to Android:

  1. This time, there is no need to alter the manifest because the default settings are the ones we want. So, navigate to proj.android and then to the jni folder and open the Android.mk file in a text editor.
  2. Edit the lines in LOCAL_SRC_FILES to read as follows:
    LOCAL_SRC_FILES := hellocpp/main.cpp \
                       ../../Classes/AppDelegate.cpp \
                       ../../Classes/GameLayer.cpp
  3. Follow the instructions from the HelloWorld and AirHockey examples to import the game into Eclipse.
  4. Save it and run your application. This time, you can try out different size screens if you have the devices.

What just happened?

You just ran a universal app in Android. And nothing could have been simpler.

Summary

In my opinion, after nodes and all their derived objects, actions are the second best thing about Cocos2d-x. They are time savers and can quickly spice things up in any project with professional-looking animations. And I hope with the examples found in this article, you will be able to create any action you need with Cocos2d-x.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here