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:
(For more resources related to this topic, see here.)
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.
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 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.
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.
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.
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:
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:
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.
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.
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); }
//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.
enum { kBackground, kMiddleground, kForeground };
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.
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.
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:
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.
Creating a bitmap font is somewhat similar to creating a batch node.
_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);
auto icon = Sprite::createWithSpriteFrameName ("health_icon.png"); icon->setPosition( Vec2(_screenSize. width * 0.15f, _screenSize.height * 0.94f) ); _gameBatchNode->addChild(icon, kBackground);
We just created our first bitmap font object in Cocos2d-x. Now let's finish creating our game's sprites.
The last sprites we need to create are the clouds, the bomb and shockwave, and our game state messages.
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); }
_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);
_shockWave = Sprite::createWithSpriteFrameName ("shockwave.png"); _shockWave->getTexture()->generateMipmap(); _shockWave->setVisible(false); _gameBatchNode->addChild(_shockWave);
_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);
There is a lot of new information regarding sprites in the previous code. So let's go over it carefully:
_bomb->getTexture()->generateMipmap();
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.
bomb->addChild(halo, kMiddleground, kSpriteHalo);
auto halo = (Sprite *) bomb->getChildByTag(kSpriteHalo);
We now have our game screen in place:
Next come object pools.
The pools are just vectors of objects. And here are the steps to create them:
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); }
_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); }
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.
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).
Creating actions with Cocos2d-x is a very simple process:
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();
//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();
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.
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.
Animations are a specialized type of action that require a few extra steps:
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); }
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();
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();
We created two instances of a very special kind of action in Cocos2d-x: Animate. Here is what we did:
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.
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 to bring the player to our party:
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; }
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()); } }
child = (Sprite *) _bomb->getChildByTag(kSpriteHalo); child->stopAllActions(); child = (Sprite *) _bomb->getChildByTag(kSpriteSparkle); child->stopAllActions();
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.
Let's add the logic to start and restart the game.
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)); }
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); } }
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.
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:
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)); }
//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); }
if (_bomb->isVisible()) { if (_bomb->getScale() > 0.3f) { if (_bomb->getOpacity() != 255) _bomb->setOpacity(255); } }
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.
We just need to use the correct index to retrieve the objects from their respective vector:
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); }
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.
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:
Now, let's take this to Android.
Follow these steps to deploy the game to Android:
LOCAL_SRC_FILES := hellocpp/main.cpp \ ../../Classes/AppDelegate.cpp \ ../../Classes/GameLayer.cpp
You just ran a universal app in Android. And nothing could have been simpler.
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.
Further resources on this subject: