Cocos2d-X Game Development Blueprints - Sample Chapter
Cocos2d-X Game Development Blueprints - Sample Chapter
Cocos2d-X Game Development Blueprints - Sample Chapter
ee
Sa
pl
Preface
Cocos2d-x offers you, as a game developer, an open source engine that enables you
to effortlessly build powerful 2D games and publish them on a multitude of popular
platforms. In today's fast-paced world, what more could a game developer need?
This book gets you started by introducing the continually evolving HTML5 platform
and familiarizes you with the Cocos2d-html5 framework.
This book is written to get you well versed with the many versatile features of
Cocos2d-x, by demonstrating the creation of nine different gamesfrom arcade
style games to new age physics games and from simple puzzle games to strategic
tower defense games! All the while, focusing on gameplay and letting Cocos2d-x
do all the heavy lifting for you.
For the finale, you will learn how to build your games for Android and Windows
Phone 8, bringing you to a full circle on Cocos2d-x.
Preface
Chapter 4, Back to the Drawing Board, shows you just how easy it is to make a game
that completely uses primitive drawing for its entities. You will also learn how to
implement simple accelerometer controls and how to add difficulty progression to
an endless game.
Chapter 5, Let's Get Physical!, guides you through the process of using Box2D to
build a physics game. You will also learn about state machines and object pools.
Chapter 6, Creativity with Textures, shows you how to fire OpenGL commands and
render your own sprite. You will also learn to use Box2D to make a picturesque
side-scroller game.
Chapter 7, Old is Gold!, introduces you to the Tiled map editor and the basics of a
tile-based game. You will also learn how to implement a reusable tile-based
collision detection algorithm.
Chapter 8, Box2D Meets RUBE, introduces you to RUBE, which is a powerful physics
editor. You will use it to create levels for a slingshot type physics game. You will
also learn about ray casting and how to implement explosions in Box2D.
Chapter 9, The Two Towers, shows you how easy it is to implement a tower defense
game in Cocos2d-x. In the process, you will learn how to implement scalable
architectures for your enemies and towers. You will also learn how a simple
gesture recognizer works and how you can control the speed of your gameplay.
Chapter 10, Cross-platform Building, shows you how to set up your environment
and build one of your games on Android and Windows Phone 8.
[ 93 ]
This is what you will have accomplished at the end of this chapter:
[ 94 ]
Chapter 4
Visual
Player
Enemy
Shield
Bomb
Missile
launcher
[ 95 ]
We passed in a vector of CCPoint that will actually hold the vertices along with
the number of vertices, the radius of the encompassing circle, and the angle the
first vertex will make with the x axis. The function then calculates the difference in
angles between successive vertices of the polygon based on the number of vertices
the polygon is supposed to have. Then, initializing theta with start_angle, we
run a loop to generate each vertex using the parametric equation of the circle. To
summarize, let's take a look at the output of this function:
GameGlobals::GetRegularPolygonVertices(vertices, 3, 50, 0)
[ 96 ]
Chapter 4
So, we have everything we need to draw most of the elements in our game. You can
already fill in the blanks and see that the Bomb, which happens to be a power-up, can
be summed up as a brown circle containing a green triangle. Similarly, the Shield
power-up is a brown circle containing a cyan hexagon, but MissileLauncher and
Enemy might not be that straightforward. We shall cross that bridge when we get
there, but now we need to focus on the main elements of the game world.
The init function for the parent class is called first before defining the vertices
that will make the player's ship. The variable PLAYER_RADIUS used to calculate the
vertices has been defined in the GameGlobals.h file and is equivalent to 20 pixels.
We then call the drawPolygon function, passing in the array of vertices, number of
vertices, fill color, border thickness, and border color. We need all this to draw the
player. Since this is a test of survival, our player must dodge enemies to prevent
being destroyed but invariably death catches up to us all. The player dies in the
following way in the Die function:
void Player::Die()
{
// don't die if already dying
if(is_dying_)
return;
is_dying_ = true;
// stop moving
speed_.x = 0;
speed_.y = 0;
float shake_duration = 0.5f;
int num_shakes = 8;
// create & animate the death and end the game afterwards
CCActionInterval* shake = CCSpawn::createWithTwoActions(
CCScaleTo::create(shake_duration, 1.2f), CCRepeat::create(
CCSequence::createWithTwoActions(CCRotateBy::create(shake_
duration/(num_shakes*2),
-20.0), CCRotateBy::create(shake_duration/(num_shakes*2), 20.0)),
num_shakes));
CCActionInterval* shrink = CCEaseSineIn::create(CCScaleTo::create(0
.1f, 0.0f));
CCActionInterval* death = CCSequence::create(shake, shrink, NULL);
runAction(CCSequence::createWithTwoActions(death,
CCCallFunc::create(this,
callfunc_selector(Player::Dead))));
SOUND_ENGINE->playEffect("blast_player.wav");
}
void Player::Dead()
{
game_world_->GameOver();
}
[ 98 ]
Chapter 4
A collision may occur in every tick of the game loop thus we prevent the Die
function from being called repeatedly by checking and immediately enabling the
is_dying_ flag. We then stop movement, animate the death with the help of a few
composite actions, and also play a sound. The callback to the function Dead tells
GameWorld to finish the game. There is more to the Player class than just this, but
we will define the remaining behavior, such as the movement, when we get to the
Moving the player section of this chapter.
We begin by calling the init function of the parent class and storing a reference
to GameWorld. We then initialize an array of the type CCPoint and pass it into
the GenerateVertices function. Here, NUM_SPIKES equals 10. We then call the
drawPolygon function followed by the drawDot function to create a red colour-filled
polygon and a black colour-filled circle, giving the enemies their deadly appearance.
Now let's take a look at the GenerateVertices function:
void Enemy::GenerateVertices(CCPoint vertices[])
{
[ 99 ]
This function simply creates two vectors for the inner and outer vertices that make
up the enemy's spiky appearance. Furthermore, you can see that we fill up these two
vectors with vertices generated by the GetRegularPolygonVertices function that
we defined earlier. Notice how the radius for the outer vertices is 1.5 times the radius
for the inner vertices. We then run a loop and fill up the input array of CCPoint with
inner and outer vertices, respectively. This order of inner vertex followed by outer
vertex is important because otherwise we wouldn't get a properly filled polygon.
You should reverse the order and see for yourself what happens to gain a better
understanding of how the colour is filled inside a convex polygon.
Let's now give the enemies their behavior by defining the Update function:
void Enemy::Update(CCPoint player_position, bool towards_player)
{
// no movement while spawning
if(is_spawning_)
return;
// first find a vector pointing to the player
CCPoint direction = ccpSub(player_position, m_obPosition);
// normalize direction then multiply with the speed_multiplier_
speed_ = ccpMult(direction.normalize(), speed_multiplier_ * (
towards_player ? 1 : -1));
// restrict movement within the boundary of the game
CCPoint next_position = ccpAdd(m_obPosition, speed_);
if(RECT_CONTAINS_CIRCLE(game_world_->boundary_rect_, next_position,
ENEMY_RADIUS * 1.5f))
{
[ 100 ]
Chapter 4
setPosition(next_position);
}
else
{
if(RECT_CONTAINS_CIRCLE(game_world_->boundary_rect_, CCPoint(
next_position.x - speed_.x, next_position.y), ENEMY_RADIUS *
1.5f))
{
setPosition(ccp(next_position.x - speed_.x, next_position.y));
}
else if(RECT_CONTAINS_CIRCLE(game_world_->boundary_rect_, CCPoint(
next_position.x, next_position.y - speed_.y), ENEMY_RADIUS *
1.5f))
{
setPosition(ccp(next_position.x, next_position.y - speed_.y));
}
}
}
The Update function, called from the main update loop by GameWorld, is passed the
player's position and whether the movement should be toward or away from the
player. Why would such a deadly, fearless enemy be moving away from the player
you wonder? Well, that happens when the player has the shield power-up enabled
of course! Each enemy also has a spawning animation defined in the Spawn function,
during which it is not supposed to movehence the conditional and the return
statements. We first calculate the speed for the enemy based on the direction towards
or away from the player and a speed multiplier. Next, we perform some bounds
checking and ensure that the enemy does not leave the boundary that is defined by
boundary_rect_ inside GameWorld. To perform this boundary check, we define a
resourceful function in GameGlobals.h with the name RECT_CONTAINS_CIRCLE.
I'm sure you remember how the enemies are supposed to move faster the longer they
live. This behavior is defined in the Tick function of the Enemy class:
void Enemy::Tick()
{
// no ticking while spawning
if(is_spawning_)
return;
++ time_alive_;
// as time increases, so does speed
switch(time_alive_)
{
[ 101 ]
= 0.5f;
= 0.75f;
= 1.25f;
= 1.5f;
The speed multiplier that you saw inside the Enemy::Update function is given
its value in the Tick function. We update the variable time_alive_ that measures
the seconds for which the enemy has been alive and, based on its value, assign
a particular speed value to the speed_multiplier_ variable so the enemy moves
faster the longer that it lives. To enumerate the various speeds an enemy may
move at, an enum by the name of EEnemySpeedTimer is defined in GameGlobals.h
as follows:
enum EEnemySpeedTimer
{
E_SLOW = 5,
E_MEDIUM = 10,
E_FAST = 15,
E_SUPER_FAST = 25,
};
We now have a player and an enemy that follows the player at increasing speeds.
But the player still has no way to defend himself or to defeat the enemies. So let's
define the last set of elements, the power-ups.
[ 102 ]
Chapter 4
void
void
void
void
void
Update();
Tick();
Spawn();
Activate();
Deactivate();
[ 103 ]
The PowerUp class will extend the CCDrawNode class, since all the power-ups will
need primitive drawing functionality. This class also defines a few variables to
keep track of the time on screen, the movement speed, whether the power-up is
active, and finally whether it should be removed by GameWorld. The main lifecycle
functions are declared here and marked as virtual so the child classes can override
them to implement their own respective behaviors. Let's now take a look at some of
these functions defined in PowerUp.cpp:
bool PowerUp::init(GameWorld* instance)
{
if(!CCDrawNode::init())
return false;
game_world_ = instance;
// calculate how much time the power-up should wait on screen before
activation
time_left_ = MAX_POWERUP_WAIT_ON_SCREEN / 2 +
CCRANDOM_0_1() * MAX_POWERUP_WAIT_ON_SCREEN / 2;
// calculate speed
speed_ = CCPoint(CCRANDOM_MINUS1_1() * 2,
CCRANDOM_MINUS1_1() * 2);
// draw the brown coloured ring
drawDot(CCPointZero, POWERUP_ICON_OUTER_RADIUS,
ccc4f(0.73725f, 0.5451f, 0, 1));
drawDot(CCPointZero, POWERUP_ICON_OUTER_RADIUS - 3,
ccc4f(0, 0, 0, 1));
setScale(0.0f);
return true;
}
The init function basically takes care of maintaining a reference to GameWorld for
ease of access and initializing a few variables. Each power-up comes to life with its
respective icon, but all those icons have their outer ring in common with each other,
so we define that here in the parent class. We also set the scale initially to 0 since we
will be animating the birth of each power-up in the Spawn function. We now look at
the Update function that will be called at every tick by GameWorld:
void PowerUp::Update()
{
// bounce within the boundary
if(!RECT_CONTAINS_CIRCLE(game_world_->boundary_rect_, m_obPosition,
POWERUP_ICON_OUTER_RADIUS))
{
[ 104 ]
Chapter 4
// bounce off the left & right edge
if( (m_obPosition.x - POWERUP_ICON_OUTER_RADIUS) <
game_world_->boundary_rect_.origin.x ||
(m_obPosition.x + POWERUP_ICON_OUTER_RADIUS) > (
game_world_->boundary_rect_.origin.x +
game_world_->boundary_rect_.size.width) )
speed_.x *= -1;
// bounce off the top & bottom edge
if( (m_obPosition.y + POWERUP_ICON_OUTER_RADIUS) > (
game_world_->boundary_rect_.origin.y +
game_world_->boundary_rect_.size.height) ||
(m_obPosition.y - POWERUP_ICON_OUTER_RADIUS) <
game_world_->boundary_rect_.origin.y )
speed_.y *= -1;
}
setPosition(m_obPosition.x + speed_.x, m_obPosition.y + speed_.y);
}
This function basically takes care of moving the power-up within the boundary
defined in GameWorld (similar to the Enemy class) by using the RECT_CONTAINS_CIRCLE
function.
Let's take a look at the following code:
void PowerUp::Tick()
{
-- time_left_;
// remove this power-up in the next iteration when it's on-screen
time is over
if(time_left_ < 0)
{
must_be_removed_ = true;
runAction(CCSequence::createWithTwoActions(CCEaseBackIn::create(
CCScaleTo::create(0.25f, 0.0f)), CCRemoveSelf::create(true)));
}
}
Each power-up must also have a life time after which it should be removed.
This happens in the Tick function that is called by GameWorld once every second.
[ 105 ]
Finally, we have the Activate and Deactivate functions that set the appropriate
flags and prepare the power-up for whatever behaviour the child class may define.
Notice how the clear function is called in the Activate method. This happens
because the power-up is initially nothing but an icon and must turn into its respective
manifestation now that it has been triggered or activated. Hence, we call the clear
function of parent class CCDrawNode, which basically clears all the geometry drawn
inside the node so far. Now that we have the parent class defined, we can take the
time to define each power-up separately starting with the Bomb class, followed by the
MissileLauncher class and Shield class.
[ 106 ]
Chapter 4
// get vertices for a triangle
vector<CCPoint> vertices;
GameGlobals::GetRegularPolygonVertices(vertices, 3,
POWERUP_ICON_INNER_RADIUS);
// draw a triangle with a green border
drawPolygon(&vertices[0], 3, ccc4f(0, 0, 0, 0), 3, ccc4f(0, 1, 0,
1));
return true;
}
Right at the beginning, we must invoke the init function of the parent class and
pass in the reference to GameWorld. Once that is done, we can generate the individual
icon for this power-up, which in this case is nothing but a green coloured triangle.
Let's wrap up the Bomb by overriding the Activate function:
void Bomb::Activate()
{
// must activate only once
if(is_active_)
return;
// first call parent function
PowerUp::Activate();
// create a blast 8 times the size of the player that should last
for
2 seconds
Blast* blast = Blast::createWithRadiusAndDuration(
PLAYER_RADIUS * 8, 2.0f);
// position blast over bomb
blast->setPosition(m_obPosition);
game_world_->AddBlast(blast);
SOUND_ENGINE->playEffect("big_blast.wav");
PowerUp::Deactivate();
}
[ 107 ]
This function must only be called once, hence the return statement at the beginning.
We then immediately call the Activate function of the parent class, since we want
the geometry cleared. Now comes the interesting part. This is where we actually
create the explosion. This is taken care of by another class: Blast. Why couldn't we
just create an explosion within this perfectly capable Bomb class itself? The reason
will become abundantly clear when we define the behaviour of the missiles. For
now, all I can tell you is that just like bombs, missiles also explode and hence that
functionality is shared by the two and separated out. We then set the position of
this blast over the bomb and finally hand it over to GameWorld. We also play a
sound effect before deactivating the bomb. Let's now glance at the behaviour
of the Blast class.
Chapter 4
// scale-up, then wait for 'duration_' amount of seconds
before cooling down
runAction(CCSequence::create(CCEaseSineOut::create(
CCScaleTo::create(0.25f, 1.0f)), CCDelayTime::create(duration_),
CCCallFunc::create(this, callfunc_selector(Blast::Cooldown)),
NULL));
return true;
}
The cool down involves an exit animation and enabling the must_be_removed_ flag
so that GameWorld will remove this Blast in the next iteration.
[ 109 ]
The init method for all power-ups takes care of creating the icon, so we begin
by generating the vertices for this particular power-up's icon. Notice how we
fetch vertices for two regular pentagons here and then splice them together into
another vector. We then pass that vector to the drawPolygon function to get the
yellow star with a border of 2 pixels. If you're confused, the following images will
clear things up:
Inner Pentagon
(vertices1)
[ 110 ]
Chapter 4
Now, let's define what happens when the missile launcher has been activated in the
Activate function:
void MissileLauncher::Activate()
{
if(is_active_)
return;
PowerUp::Activate();
// generate a target for each missile
vector<CCPoint> target = GenerateTargets();
// generate an initial direction vertor for each missile
vector<CCPoint> initial_direction;
GameGlobals::GetRegularPolygonVertices(initial_direction, 5,
SCREEN_SIZE.width/4, M_PI * 2/20);
for(int i = 0; i < 5; ++i)
{
// create a missile with a target, initial direction & speed
Missile* missile = Missile::createWithTarget(game_world_,
target[i],
ccpMult(initial_direction[i].normalize(), MISSILE_SPEED));
// position the missile over the launcher
missile->setPosition(m_obPosition);
game_world_->AddMissile(missile);
}
SOUND_ENGINE->playEffect("missile.wav");
PowerUp::Deactivate();
}
The MissileLauncher class is supposed to spawn five missiles that fly towards
enemies and explode on contact. Thus, the first thing we need to do is generate
targets and initial directions for each missile. We then run a loop to create five
Missile objects and pass in a reference to GameWorld, the target position, and the
initial speed. The initial speed is a scalar multiplication of initial_direction with
a constant called MISSILE_SPEED.
[ 111 ]
We set the position for each missile and hand them over to GameWorld. We wind up
this missile launcher by playing a sound effect and finally deactivate this power-up
since its job is done. Before we move to the Missile class, let's take a look at the
GenerateTargets function:
vector<CCPoint> MissileLauncher::GenerateTargets()
{
vector<CCPoint> target_points;
target_points.clear();
int targets_found = 0;
int num_enemies = game_world_->enemies_->count();
// loop through the first 5 enemies within GameWorld &
save their positions
for(int i = 0; i < num_enemies && targets_found < 5; ++i)
{
Enemy* enemy = (Enemy*)game_world_->enemies_->objectAtIndex(i);
target_points.push_back(enemy->getPosition());
++ targets_found;
}
// if less than 5 enemies were found, fill up with random positions
within the boundary
while(targets_found < 5)
{
target_points.push_back(CCPoint(CCRANDOM_0_1() * (
game_world_->boundary_rect_.origin.x +
game_world_->boundary_rect_.size.width) , CCRANDOM_0_1() * (
game_world_->boundary_rect_.origin.y +
game_world_->boundary_rect_.size.height)));
++ targets_found;
}
return target_points;
}
This function returns a vector of CCPoint, which will represent the target for a given
missile. So, we loop over the enemies currently held by GameWorld and save the
position of the first five enemies we find. However, if we don't have enough targets
after this loop, the missile still must fire to somewhere. Thus, we send that missile
to any random point within the boundary of the game. So, we have launched five
missiles with a target to hunt down and kill. Let's now take a look at the behaviour of
a missile by defining the Missile class.
[ 112 ]
Chapter 4
The Missile class will also inherit CCDrawNode, which means we must call the
parent class' init function. We save a reference to GameWorld, the target point, and
the initial speed that this missile should move at. We then define the missile's visual
representation by creating an array of vertices and passing them to the drawPoly
function. This results in a yellow color-filled missile that is similar in shape to
the Player entity. Finally, we schedule the Explode function to be called after
5.0f seconds. This is because we want this missile to automatically explode after
sometime if it hasn't collided with any enemies.
[ 113 ]
The update function is responsible for updating the position and rotation of the
missile. Thus, we calculate the difference between the target and the missile's current
position. Normalizing this CCPoint will give us the direction in which this missile
must move in order to hit its target. We then add this direction to speed_ because we
want the missile to move in a smooth curve towards its target. Finally, we normalize
speed_ and multiply it with a constant, MISSILE_SPEED. If we don't do this last step,
the missile's speed will keep increasing and it will never reach its target.
[ 114 ]
Chapter 4
We also set the rotation of this missile using the useful ccpToAngle function, passing
in the current and previous positions. To wrap up the update function, we check
whether the missile has reached its target by using the fuzzyEquals function of the
CCPoint class. This function basically adds some tolerance to the equality checking
of two points. In this case, this tolerance is the outer radius of an enemy. If a collision
is detected, we order this missile to explode in the Explode function, which we will
define as follows:
void Missile::Explode(float dt)
{
// can't expode more than once
if(has_exploded_)
return;
has_exploded_ = true;
// create three blasts on explosion
for(int i = 0; i < 3; ++i)
{
// create a blast twice the size of the player that should last
for
quarter of a second
Blast* blast = Blast::createWithRadiusAndDuration(
PLAYER_RADIUS * 2, 0.25f);
// position it randomly around the missile
blast->setPosition(ccpAdd(m_obPosition,
CCPoint(CCRANDOM_0_1() * PLAYER_RADIUS * 2 * i,
CCRANDOM_0_1() * PLAYER_RADIUS * 2 * i)));
game_world_->AddBlast(blast);
}
// remove this missile in the next iteration
must_be_removed_ = true;
runAction(CCSequence::createWithTwoActions(
CCDelayTime::create(0.01f), CCRemoveSelf::create(true)));
SOUND_ENGINE->playEffect("small_blast.wav");
}
A missile might explode on three different occasions; when it has run out of time,
when it has reached its target, or when it has collided with an enemy on the way
to its target. We must ensure that a missile must explode only once and hence we
enable the has_exploded_ flag to prevent further explosions. Each missile explodes
and results in three blasts. In a loop, we create three Blast objects, position them,
and hand them over to GameWorld. Notice that these blasts are smaller and stay for
much lesser time than the blast created by the Bomb. We finally finish the Explode
function by enabling the must_be_removed_ flag, running a CCRemoveSelf action,
and playing a sound effect. That wraps up our Missile class and it is time to define
our last power-up, and my personal favorite, the Shield class.
[ 115 ]
[ 116 ]
Chapter 4
Deactivate();
}
// start blinking the shield when there are just two seconds left
else if(shield_time_left_ == 2)
{
CCActionInterval* blink = CCBlink::create(2.0f, 8);
blink->setTag(SHIELD_BLINK_TAG);
runAction(blink);
}
}
else
{
PowerUp::Tick();
}
}
Before activation, the shield is dormant just like the other power-ups so we call the
Tick function of the parent class. But after activation, we monitor how much time the
shield has left till it must be disabled by the updating variable shield_time_left_.
Initially, shield_time_left_ is set to 10 and is decremented every Tick. When the
value hits 0, we Deactivate the shield. When the value hits 2, we start blinking the
shield so the user knows that shield is about to be disabled and that there are just 2
seconds of carnage left. We set a tag for the blinking action that will come in
handy later.
Let's take a look at the following code:
void Shield::Activate()
{
if(is_active_)
return;
// if a shield already exists on the player,
if(game_world_->player_->GetShield())
{
// reset the existing shield
game_world_->player_->GetShield()->Reset();
// deactivate self
Deactivate();
removeFromParentAndCleanup(true);
}
[ 117 ]
We first check if the player has a shield already enabled. Why should we do that?
This is because we can't have two shields active at the same timethat would look
silly! Instead, we reset the first shield and discard the second one.
Thus, if a shield is already active on the player, we disable this shield by calling
the Deactivate method and call removeFromParentAndCleanup. Also, we call the
Reset function of the shield currently active on the player, which looks like this:
void Shield::Reset()
{
// reset the shield duration
shield_time_left_ = SHIELD_DURATION;
// stop any blinking action & show the shield if it was hidden due
to the blink
stopActionByTag(SHIELD_BLINK_TAG);
setVisible(true);
}
As you can see, we simply reset the duration of the shield and stop any blinking
action. That last setVisible is there to undo any invisibility due to the blinking.
Now, let's move back to the Activate function:
// else if shield doesn't exist on the player
else
{
PowerUp::Activate();
// set the shield duration
shield_time_left_ = SHIELD_DURATION;
setScale(0);
// generate & draw a bigger cyan hexagon
vector<CCPoint> vertices;
GameGlobals::GetRegularPolygonVertices(vertices, 6,
PLAYER_RADIUS * 2.5f);
drawPolygon(&vertices[0], 6, ccc4f(0, 0, 0, 0), 4,
ccc4f(0, 0.96862f, 1, 1));
// animate the activation & life of the shield
runAction(CCEaseBounceOut::create(CCScaleTo::create(0.25f, 1.0f)));
runAction(CCRepeatForever::create(CCSequence::createWithTwoActions(
CCEaseSineOut::create(CCScaleTo::create(0.25f, 1.15f)),
CCEaseSineOut::create(CCScaleTo::create(0.25f, 1.0f)))));
[ 118 ]
Chapter 4
// inform the player that it now has a shield around it
game_world_->player_->SetShield(this);
}
SOUND_ENGINE->playEffect("shield.wav");
}
If there is no shield active on the player, we call the Activate function of the parent
class and initialize the shield's duration again. We also set the scale to 0, since we
want to animate the activation of the shield. Since PowerUp::Activate clears the
node's geometry, we generate vertices for a hexagon that's big enough to cover the
player. We then repeat a simple scale-up or scale-down animation and finally inform
the player that it has a shield around it. Outside the if-else block, we play a sound
effect marking the activation of the shield and finish off the Activate function.
In the Deactivate function (not shown in the code), we simply inform the player
that it doesn't have the shield around it any more. With that, we wrap up our
power-ups and can now move on to the game world.
[ 119 ]
Description
This is the amount of time the game has been active.
enemies_killed_total_
enemies_killed_combo_
combo_timer_
score_
is_popup_active_
The function then proceeds to create and add the BackgroundManager. We will not
discuss the BackgroundManager class, but it basically creates a CCDrawNode that
draws a number of white circles with varying opacities to serve as a starry space
background for the game.
The function then calls CreateBoundary, which creates a CCDrawNode with a semitransparent white rectangle drawn inside it to represent the bounds of the play area.
The CreatePlayer functions simply creates an object of the Player class and positions
it in the center of the play area before adding it to GameWorld. The CreateContainers
function creates and retains four arrays: enemies_, powerups_, blasts_, and
missiles_ to hold the Enemy, PowerUp, Blast, and Missile objects, respectively. The
CreateHUD function simply creates and adds a CCLabelBMFont to display the score.
We also add a formation of enemies along with a power-up to get the user started.
Finally, we schedule the update function and the Tick function to be called once
every second.
[ 120 ]
Chapter 4
Let's now look at the CheckCollisions function. This function will basically use
circular collision detection by using the CIRCLE_INTERSECTS_CIRCLE function
defined in GameGlobals.h. All you need to do is provide the center and radius
of the two circles and the function returns true if a collision is found.
Let's take a look at the following code:
void GameWorld::CheckCollisions()
{
// save player position & radius
CCPoint player_position = player_->getPosition();
float player_radius = player_->getRadius();
// iterate through all enemies
CCObject* object = NULL;
CCARRAY_FOREACH(enemies_, object)
{
Enemy* enemy = (Enemy*)object;
if(enemy)
{
CCPoint enemy_position = enemy->getPosition();
// check with Player
if(CIRCLE_INTERSECTS_CIRCLE(player_position, player_radius,
enemy_position, ENEMY_RADIUS))
{
// if shield is enabled, kill enemy
if(player_->GetShield())
{
enemy->Die();
EnemyKilled();
}
// else kill player...but only if enemy has finished spawning
else if(!enemy->getIsSpawning())
player_->Die();
}
A lot of the collision detection revolves around the enemies. So, we first check
for collisions of each enemy with the player. If a collision is found, we either
kill the player or the enemy based on the shield being enabled or disabled.
Another thing to consider is how we check whether the enemy is spawning and
skip killing the player. We do this on purpose, or else the player will die before
the enemy's spawning is completethereby leaving the user bewildered about the
player's death.
[ 122 ]
Chapter 4
We then proceed to check collisions of the enemy with the blasts and kill any enemy
coming in contact with the blast. We also explode any missile that has come in
contact with any enemy.
[ 123 ]
Finally, we check for collisions between the player and the power-ups and activate
the power-up on contact with the player. That wraps up the CheckCollisions
function but before we move to the Tick function, let's take a look at the
EnemyKilled function:
void GameWorld::EnemyKilled()
{
// increment counters
++ enemies_killed_total_;
++ enemies_killed_combo_;
// reset combo time
combo_timer_ = COMBO_TIME;
// add score & update the label
score_ += 7;
char buf[16] = {0};
sprintf(buf, "Score: %d", score_);
score_label_->setString(buf);
}
[ 124 ]
Chapter 4
Quite simply, we increment the total number of enemies killed as well as the number
of enemies killed in the current combo. We can then use the number of enemies
killed in the current combo to reward the player with some extra points when the
combo timer elapses. We also reset the combo timer, increment the score, and update
the HUD. Next up is the Tick function:
void GameWorld::Tick(float dt)
{
// don't tick if player is dying
if(player_->is_dying_)
return;
++ seconds_;
-- combo_timer_;
// show the combo achieved if time is up
if(combo_timer_ < 0)
combo_timer_ = 0;
else if(combo_timer_ == 0)
ComboTimeUp();
// Tick each enemy
CCObject* object = NULL;
CCARRAY_FOREACH(enemies_, object)
{
Enemy* enemy = (Enemy*)object;
if(enemy)
{
enemy->Tick();
}
}
// Tick each power-up
object = NULL;
CCARRAY_FOREACH(powerups_, object)
{
PowerUp* powerup = (PowerUp*)object;
if(powerup)
{
powerup->Tick();
}
}
[ 125 ]
The first thing to do is to increment the seconds_ counter. The use of this variable will
become clear in the last section when we use it to make the game progressively more
difficult. Next, we decrement the combo_timer_ and call the ComboTimeUp function
that displays the number of enemies killed by the player in the last 3 seconds.
Then, we call the Tick function for the Enemy and PowerUp objects. Finally, we add a
new formation of enemies every 5 seconds and a new power-up every 4 seconds.
Extreme left
-0.9f to -1.1f
0.0f
Extreme right
0.9f to 1.1f
0.0f
Extreme forward
0.0f
0.9f to 1.1f
Extreme backward
0.0f
-0.9f to -1.1f
[ 126 ]
Chapter 4
The preceding values are approximate and will differ a bit from
device to device.
For the purpose of our game, we will need just horizontal and vertical movement,
so we only use the x and y members of acceleration_value.
I'm sure you were expecting more, but the reason for separating the logic into the
HandleInput function is because this game was first built on win32 and the tilt
controls were tested on an Android device later. So, I needed to put in touch controls
for the win32 build. Anyway, the HandleInput function looks like this:
void GameWorld::HandleInput(CCPoint input)
{
/// don't accept input if popup is active or if player is dead
if(is_popup_active_ || player_->is_dying_)
return;
CCPoint input_abs = CCPoint(fabs(input.x), fabs(input.y));
// calculate player speed based on how much device has tilted
// greater speed multipliers for greater tilt values
player_->speed_.x = input.x * ( (input_abs.x > 0.3f) ? 36 : (
(input_abs.x > 0.2f) ? 28 : 20 ) );
player_->speed_.y = input.y * ( (input_abs.y > 0.3f) ? 36 : (
(input_abs.y > 0.2f) ? 28 : 20 ) );
// update the background
background_->setPosition(ccp(input.x * -30, input.y * -30));
}
We return from this function when there is a popup active or when the player is
dying. That next bit of calculation is necessary to offer a more sensitive controlling
experience. Basically, the user doesn't have to tilt his device all the way to the left
if he wants the player to move to the left at full speed. Thus, we multiply larger
values for larger readings and smaller values for smaller readings. We finally
set the player's speed to the resultant calculation. One of the cool features of an
accelerometer based game is to allow the users to calibrate the accelerometer. That
is left to you, my intelligent reader, as an exercise. Now that we have recorded the
user's input, let's add the movement logic into the Player class.
[ 127 ]
We first save the previous position of the player because we need it while setting
the rotation. The UpdateRotation function will just use ccpToAngle function to
set the rotation of the player with a bit of easing, so we will skip it and discuss
only the UpdatePosition function as follows:
void Player::UpdatePosition()
{
// don't move if speed is too low
if(ccpLength(speed_) > 0.75f)
{
// add speed but limit movement within the boundary
CCPoint next_position = ccpAdd(m_obPosition, speed_);
if(RECT_CONTAINS_CIRCLE(game_world_->boundary_rect_,
next_position, PLAYER_RADIUS))
{
setPosition(next_position);
}
else
{
if(RECT_CONTAINS_CIRCLE(game_world_->boundary_rect_, CCPoint(
next_position.x - speed_.x, next_position.y), PLAYER_RADIUS))
{
setPosition(ccp(next_position.x - speed_.x, next_position.y));
}
else if(RECT_CONTAINS_CIRCLE(game_world_->boundary_rect_,
CCPoint(
next_position.x, next_position.y - speed_.y), PLAYER_RADIUS))
{
setPosition(ccp(next_position.x, next_position.y - speed_.y));
}
}
}
}
[ 128 ]
Chapter 4
We ignore the values that are less than a certain threshold; otherwise, the player
will constantly be jerking around the screen owing to the accelerometer readings
constantly fluctuating by minor values. The logic is to calculate the next position
by adding up the speed and finally limiting the player within the boundary defined
by GameWorld.
First up, we have an enum called ESkillTimer that represents the skill level in
terms of the number of seconds the user has survived the game. Next, we have enum
EEnemyFormation defined as follows:
enum EEnemyFormation
{
E_FORMATION_RANDOM_EASY = 0,
E_FORMATION_VERTICAL_EASY,
E_FORMATION_HORIZONTAL_EASY,
E_FORMATION_POLYGON_EASY,
E_FORMATION_RANDOM_MEDIUM,
E_FORMATION_VERTICAL_MEDIUM,
E_FORMATION_HORIZONTAL_MEDIUM,
E_FORMATION_POLYGON_MEDIUM,
E_FORMATION_RANDOM_HARD,
[ 129 ]
We also have arrays that specify the frequency of the various formations for a
specific skill level. We will stitch these different bits of information together in
the GetEnemyFormationType function:
EEnemyFormation GameWorld::GetEnemyFormationType()
{
// return a formation type from a list of formation types, based on
time user has been playing
// the longer the user has survived, the more difficult the
formations will be
if(seconds_ > E_SKILL6)
{
int random_index = CCRANDOM_0_1() * GameGlobals::skill6_
formations_size;
return (EEnemyFormation)(GameGlobals::skill6_formations[random_
index]);
}
else if(seconds_ > E_SKILL5)
{
int random_index = CCRANDOM_0_1() * GameGlobals::skill5_
formations_size;
return (EEnemyFormation)(GameGlobals::skill5_formations[random_
index]);
}
[ 130 ]
Chapter 4
else if(seconds_ > E_SKILL4)
{
int random_index = CCRANDOM_0_1() * GameGlobals::skill4_
formations_size;
return (EEnemyFormation)(GameGlobals::skill4_formations[random_
index]);
}
else if(seconds_ > E_SKILL3)
{
int random_index = CCRANDOM_0_1() * GameGlobals::skill3_
formations_size;
return (EEnemyFormation)(GameGlobals::skill3_formations[random_
index]);
}
else if(seconds_ > E_SKILL2)
{
int random_index = CCRANDOM_0_1() * GameGlobals::skill2_
formations_size;
return (EEnemyFormation)(GameGlobals::skill2_formations[random_
index]);
}
else if(seconds_ > E_SKILL1)
{
int random_index = CCRANDOM_0_1() * GameGlobals::skill1_
formations_size;
return (EEnemyFormation)(GameGlobals::skill1_formations[random_
index]);
}
else
{
return E_FORMATION_RANDOM_EASY;
}
}
This function first checks up to which skill level the user has managed to survive
and then returns an appropriate formation for the respective skill level. Even
though the formations are chosen randomly, we still have control over how often
a given formation can pop up for a given difficulty level. All we need to do is
increase or decrease the number of occurrences of a formation type within the array
for a skill level.
[ 131 ]
Now that we have the type of formation based on difficulty, we can proceed by
actually adding the enemies to the game in the AddEnemyFormation function:
void GameWorld::AddEnemyFormation()
{
// fetch an enemy formation
EEnemyFormation type = GetEnemyFormationType();
// fetch a list of positions for the given formation
vector<CCPoint> formation = GameGlobals::GetEnemyFormation(
type, boundary_rect_, player_->getPosition());
int num_enemies_to_create = formation.size();
int num_enemies_on_screen = enemies_->count();
// limit the total number of enemies to MAX_ENEMIES
if(num_enemies_on_screen + num_enemies_to_create >= MAX_ENEMIES)
{
num_enemies_to_create = MAX_ENEMIES - num_enemies_on_screen;
}
// create, add & position enemies based on the formation
for(int i = 0; i < num_enemies_to_create; ++i)
{
Enemy* enemy = Enemy::create(this);
enemy->setPosition(formation[i]);
enemy->Spawn(i * ENEMY_SPAWN_DELAY);
addChild(enemy, E_LAYER_ENEMIES);
enemies_->addObject(enemy);
}
}
This function begins by fetching the type of formation to create and passes it into the
GetEnemyFormation function of the GameGlobals class. The GetEnemyFormation
function will return a vector of CCPoint that we will use to position the enemies. At
any given point, the total number of enemies on screen should not exceed 250, which
in this case is held by constant MAX_ENEMIES defined in GameGlobals.h. We ensure
the preceding condition is met and then run a loop where we create an Enemy object,
set its position, call its Spawn function, and finally add it to the screen as well as the
enemies_ CCARRAY.
By adding progression to Inverse Universe, we complete our fourth game. I
have skipped discussing some of the functionality in the chapter, such as the
BackgroundManager class, the way enemy formations are created and some of the
helper functions. You will find the code for such features and more in the source
bundle. Rest assured the comments will be enough to explain what's happening.
[ 132 ]
Chapter 4
Summary
With Inverse Universe, we went back to the drawing board and came out with flying
colors! You learned the difference between CCDrawingPrimitives and CCDrawNode.
We also extended the CCDrawNode class to create each and every one of our game's
elements. We implemented circle-to-circle collision detection and finally understood
how tilt controls or the accelerometer work in Cocos2d-x. As a good game design
principle, you also learned how to add progression to a game that is not level-based,
while still keeping it open-ended.
In our next chapter, we'll get acquainted with an extremely lively friend, a friend that
you can keep applying forces to, and a friend that can take any shape and size that
you'd likeyes it's none other than our great friend Box2D!
[ 133 ]
www.PacktPub.com
Stay Connected: