On to the game world...

We have a lot of work to do, so let's quickly list the main tasks at hand:

  • Create
    • Create the level by parsing an XML file containing level data
    • Create the player
    • Create the HUD
  • Move the enemies
  • Update
    • Fire player and enemy bullets
    • Collision detection
    • Level completion and game over conditions

However, before we complete all these tasks, we need to define the classes for our three major game play entities: player, enemy, and brick.

The Player class

Our Player entity inherits from CustomSprite and can die and come back to life, but only twice. The third time it dies, the game is over! Let's take a look at the significant functions that make our Player entity brave and enduring:

void Player::Enter()
{
  // initially position the player below the screen
  setPosition(ccp(SCREEN_SIZE.width * 0.5, SCREEN_SIZE.height * -0.1));

  // animate the move into the screen
  CCActionInterval* movement = CCEaseBackOut::create(CCMoveTo::create(
  1.0f, ccp(SCREEN_SIZE.width * 0.5, SCREEN_SIZE.height * 0.1)));
  CCActionInstant* movement_over = CCCallFunc::create(this, 
  callfunc_selector(Player::EnterFinished));
  runAction(CCSequence::createWithTwoActions(movement, movement_over));
}

void Player::EnterFinished()
{
  // player has entered, now start the game
    game_world_->StartGame();
}

We called the Enter function of Player at the time of level creation. Here, we placed the player outside the screen and ran an action to move him in. No game can start without the player, so we called the StartGame function of GameWorld only after the animation is over in the callback function EnterFinishe.

Now let's move on to the death logic by looking at the following code:

void Player::Die()
{
  // first reduce lives
  lives_ = (--lives_ < 0) ? 0 : lives_;

  // respawn only if there are lives remaining
  is_respawning_ = (lives_ > 0);

  // animate the death :(
  CCActionInterval* death = CCSpawn::createWithTwoActions(
  CCFadeOut::create(0.5f), CCScaleTo::create(0.5f, 1.5f));
  // call the appropriate callback based on lives remaining
  CCActionInstant* after_death = (lives_ <= 0) ? (
  CCCallFunc::create(this, callfunc_selector(
  Player::OnAllLivesFinished))) : (CCCallFunc::create(
  this, callfunc_selector(Player::Respawn)));
  runAction(CCSequence::createWithTwoActions(death, after_death));

  // play a particle...a sad one :(
  CCParticleSystemQuad* explosion = 
  CCParticleSystemQuad::create("explosion.plist");
  explosion->setAutoRemoveOnFinish(true);
  explosion->setPosition(m_obPosition);
  game_world_->addChild(explosion);

  SOUND_ENGINE->playEffect("blast_player.wav");
}

Our Player entity isn't invincible: it does get hit and must die. However, it does have three chances to defeat the enemies. The Die function starts by reducing the number of lives left. Then, we decide whether the Player entity can respawn based on how many lives there are left. If there are any lives left, we call the Respawn function or else the OnAllLivesFinished function after the death animation is over.

In addition to the scaling and fading animation, we also played a cool particle effect where the player died. We used the CCParticleSystemQuad class to load a particle effect from an external .plist file. I happened to generate this file online from a cool web app available at http://onebyonedesign.com/flash/particleeditor/.

Notice how we called the setAutoRemoveOnFinish function and passed in true. This causes the particle to be removed by the engine after it has finished playing. We also played a sound effect on the death of the Player entity.

Now, take a look at the following code:

void Player::Respawn()
{
  // reset the position, opacity & scale
  setPosition(ccp(SCREEN_SIZE.width * 0.5, 
  SCREEN_SIZE.height * 0.1));
  setOpacity(255);
  setScale(0.0f);

  // animate the respawn
  CCSpawn* respawn = CCSpawn::createWithTwoActions(
  CCScaleTo::create(0.5f, 1.0f), CCBlink::create(1.0f, 5));
  CCCallFunc* respawn_complete = CCCallFunc::create(
  this, callfunc_selector(Player::OnRespawnComplete));
  runAction(CCSequence::createWithTwoActions(respawn, 
  respawn_complete));
}

void Player::OnRespawnComplete()
{
  is_respawning_ = false;
}

void Player::OnAllLivesFinished()
{
  // player is finally dead...for sure...game is now over
  game_world_->GameOver();
}

The Respawn function places the player back at the initial position, resets the opacity and scale parameters. Then a cool blinking animation is run with a callback to OnRespawnComplete.

The OnAllLivesFinished function simply informs GameWorld to wind up the game—our player is no more!

The Enemy class

The Player entity is brave and tough for sure, but the enemies are no less malicious. That's right, I said enemies and we have three different kinds in this game. The Enemy class inherits from CustomSprite, just like the Player class. Let's look at the constructor and see how they're different:

Enemy::Enemy(GameWorld* game_world, const char* frame_name)
{
  game_world_ = game_world;
  score_ = 0;

  // different enemies have different properties
  if(strstr(frame_name, "1"))
  {
    bullet_name_ = "sfbullet3";
    particle_color_ = ccc4f(0.5255, 0.9373, 0, 1);
    bullet_duration_ = BULLET_MOVE_DURATION * 3;
    size_ = CCSizeMake(50, 35);
  }
  else if(strstr(frame_name, "2"))
  {
    bullet_name_ = "sfbullet1";
    particle_color_ = ccc4f(0.9569, 0.2471, 0.3373, 1);
    bullet_duration_ = BULLET_MOVE_DURATION * 1.5;
    size_ = CCSizeMake(50, 50);
  }
  else if(strstr(frame_name, "3"))
  {
    bullet_name_ = "sfbullet2";
    particle_color_ = ccc4f(0.9451, 0.8157, 0, 1);
    bullet_duration_ = BULLET_MOVE_DURATION * 0.8;
    size_ = CCSizeMake(55, 55);
  }
}

The properties for each Enemy entity are based on the frame_name passed in to the constructor. These properties are bullet_name_, which is the sprite frame name for the bullets that this enemy will shoot; particle_color_, which stores the color of the particle played when this enemy dies; bullet_duration_, which stores the amount of time this enemy's bullet takes to reach the edge of the screen; and finally, the size of this enemy used while checking collisions. Now let's take a look at what happens when an enemy dies in the Die function:

int Enemy::Die()
{
  // do this so that the movement action stops
  stopAllActions();

  // play an animation when this enemy is hit by player bullet
  CCActionInterval* blast = CCScaleTo::create(0.25f, 0.0f);
  CCRemoveSelf* remove = CCRemoveSelf::create(true);
  runAction(CCSequence::createWithTwoActions(blast, remove));

  // play a particle effect
  // modify the start & end color to suit the enemy
  CCParticleSystemQuad* explosion = 
  CCParticleSystemQuad::create("explosion.plist");
  explosion->setStartColor(particle_color_);
  explosion->setEndColor(particle_color_);
  explosion->setAutoRemoveOnFinish(true);
  explosion->setPosition(m_obPosition);
  game_world_->addChild(explosion);

  SOUND_ENGINE->playEffect("blast_enemy.wav");

  // return score_ so it can be credited to the player
  return score_;
}

We first called stopAllActions because all enemies will keep moving as a group across the screen from side to side as soon as the game starts. We then created a CCSequence that will animate and remove the enemy.

Similar to the Player class, the Enemy class also gets a cool particle effect on death. You can see that we used the same particle .plist file and changed the start and end color of the particle system. Basically, you can set and get every single property that comprises a particle system and tweak it to your needs or use them to start from scratch if you don't have a .plist file. There are also plenty of readymade particle systems in the engine that you can use and tweak.

Next, we played a sound effect when the enemy is killed. Finally, we returned the score that must be rewarded to the player for bravely slaying this enemy.

The Brick class

If it wasn't enough that there were multiple waves of enemies, the player must also deal with bricks blocking the player's line of sight. The Brick class also inherits from CustomSprite and is very similar to the Enemy class; however, instead of a Die function, it has a Crumble function that looks like this:

int Brick::Crumble()
{
 // play an animation when this brick is hit by player bullet
 CCActionInterval* blast = CCScaleTo::create(0.25f, 0.0f);
 CCRemoveSelf* remove = CCRemoveSelf::create(true);
 runAction(CCSequence::createWithTwoActions(blast, remove));

 SOUND_ENGINE->playEffect("blast_brick.wav");

 // return score_ so it can be credited to the player
 return score_;
}

We run a simple scale-down animation, remove the brick, play a sound effect, and return the score back to the calling function. That wraps up our three basic entities. Let's go ahead and see how these are linked to a level's XML file.

Parsing the level file

The first task on our list is parsing an XML file that will contain data about our enemies, bricks, and even the player. The level file for the first level, whose screenshot you saw at the start of this chapter, is given as follows:

<Level player_fire_rate="1.0">
<Enemy Setmove_duration="3.0" fire_rate="5.0">
<Enemy name="sfenmy1" score="25" position="280,650" />
<Enemy name="sfenmy1" score="25" position="460,650" />
<Enemy name="sfenmy2" score="50" position="640,650" />
<Enemy name="sfenmy1" score="25" position="820,650" />
<Enemy name="sfenmy1" score="25" position="1000,650" />
<Enemy name="sfenmy1" score="25" position="370,500" />
<Enemy name="sfenmy1" score="25" position="550,500" />
<Enemy name="sfenmy1" score="25" position="730,500" />
<Enemy name="sfenmy1" score="25" position="910,500" />
</EnemySet>
<BrickSet>
<Brick name="sfbrick1" score="10" position="300,350" />
<Brick name="sfbrick2" score="10" position="364,350" />
<Brick name="sfbrick1" score="10" position="450,250" />
<Brick name="sfbrick2" score="10" position="514,250" />
<Brick name="sfbrick1" score="10" position="600,350" />
<Brick name="sfbrick2" score="10" position="664,350" />
<Brick name="sfbrick1" score="10" position="750,250" />
<Brick name="sfbrick2" score="10" position="814,250" />
<Brick name="sfbrick1" score="10" position="900,350" />
<Brick name="sfbrick2" score="10" position="964,350" />
</BrickSet>
</Level>

Let's take some time to understand what the data in this XML represents. The root of the XML document is the Level tag. A given level contains a set of enemies and bricks represented by the EnemySet and BrickSet tags, respectively. The enemies and bricks contained within the EnemySet and BrickSet tags are represented by the Enemy and Brick tags, respectively. Now, we go over the attributes of these tags briefly in the following table:

Tag

Attribute

Description

Level

player_fire_rate

This is the rate at which a player fires bullets.

EnemySet

move_duration

This is the amount of time before which the entire set of enemies move.

EnemySet

fire_rate

This is the rate at which a bullet is fired from any one of the enemies.

Enemy and Brick

name

This is the sprite frame name to represent the given enemy/brick. It also serves as type of enemy.

Enemy&Brick

score

This is the score credited to the player when this enemy/brick is hit.

Now that we have understood what a level file can consist of, it's time to use a versatile XML parsing library named tinyxml2. You can find documentation and references on tinyxml2 at http://grinninglizard.com/tinyxml2docs/index.html. The best thing is how simple and lightweight the library actually is! So let's go ahead and see how to use tinyxml2 to actually parse this file inside the CreateLevel function of GameWorld:

void GameWorld::CreateLevel()
{
  // create the environment
  BackgroundManager* background_manager = 
    BackgroundManager::create();
  addChild(background_manager, E_LAYER_BACKGROUND);

  // create & add the batch node
  sprite_batch_node_ = 
    CCSpriteBatchNode::create("spacetex.png", 128);
  addChild(sprite_batch_node_);

  // initialize score & state machine flags
  score_ = score_to_carry_;
  has_game_started_ = false;
  has_game_stopped_ = false;
  is_game_paused_ = false;
  // initialize enemy position variables
  left_side_enemy_position_ = SCREEN_SIZE.width/2;
  right_side_enemy_position_ = SCREEN_SIZE.width/2;

Before we did any parsing and creates levels, we created and added BackgroundManager and CCSpriteBatchNode with the texture of our sprite sheet and the maximum number of child sprites. The BackgroundManager class will take care of creating the environment for SpaceCraze. We then initialized some member variables for our current level. The score that the player begins the current level with, is stored in score_. The next three variables are flags that are used to maintain the state of the game. The last two variables represent the left-most and right-most enemies that we will use when we move the enemies.

Now, let's take a look at the following code:

// generate level filename
  char level_file[16] = {0};
  sprintf(level_file, "Level%02d.xml", current_level_);

  // fetch level file data
  unsigned long size;
  char* data = (char*)CCFileUtils::sharedFileUtils()->
  getFileData(level_file, "rb", &size);

  // parse the level file
  tinyxml2::XMLDocument xml_document;
  tinyxml2::XMLError xml_result = xml_document.Parse(data, size);

  CC_SAFE_DELETE(data);

  // print the error if parsing was unsuccessful
  if(xml_result != tinyxml2::XML_SUCCESS)
  {
    CCLOGERROR("Error:%d while reading %s", xml_result, level_file);
    return;
  }

  // save player data
  tinyxml2::XMLNode* level_node = xml_document.FirstChild();
  player_fire_rate_ = level_node->ToElement()->
  FloatAttribute("player_fire_rate");

  // create set of enemies
  tinyxml2::XMLNode* enemy_set_node = level_node->FirstChild(); 
  CreateEnemies(enemy_set_node);

  // create set of bricks
  tinyxml2::XMLNode* brick_set_node = 
  enemy_set_node->NextSibling(); 
  CreateBricks(brick_set_node);

  CreatePlayer();

  CreateHUD();

  // everything created, start updating
  scheduleUpdate();
}

In the preceding code, we used tinyxml2 to parse the level file. We started by generating the path of the level file based on the number of the current level. We then asked CCFileUtils to return the data of the level file. We must delete the memory allocated to the char* by the name of data to avoid a memory leak in the game.

We must declare a object of class XMLDocument named xml_document and call the Parse function on it, providing the char* data and its size as parameters. We then checked for successful parsing of the XML document and printed an error message if unsuccessful. Now that we have our XML data parsed and ready to use, we save the root node of the document into the level_node variable by calling the FirstChild method on the xml_document. We can now painlessly extract the player_fire_rate attribute using the FloatAttribute function of the XMLElement class.

Note

While using tinyxml2, keep in mind the difference between XMLNode and XMLElement. The former is used when one wants to iterate through an XMLDocument. The latter is used to extract values and attributes for a given tag within the XMLDocument.

Creating enemies

Let's take a look at how the CreateEnemies function uses the enemy_set_node to generate the enemies for a given level:

void GameWorld::CreateEnemies(tinyxml2::XMLNode* enemy_set)
{
  // save enemy movement & firing information
  enemy_movement_duration_ = enemy_set->ToElement()->
  FloatAttribute("move_duration");
  enemy_fire_rate_ = enemy_set->ToElement()->
  FloatAttribute("fire_rate");
  
  // create array to hold enemies
  enemies_ = CCArray::create();
  enemies_->retain();

  // create array to hold enemy bullets
  enemy_bullets_ = CCArray::createWithCapacity(MAX_BULLETS);
  enemy_bullets_->retain();

  // iterate through <EnemySet> and create Enemy objects
  tinyxml2::XMLElement* enemy_element = NULL;
  for(tinyxml2::XMLNode* enemy_node = enemy_set->FirstChild(); 
  enemy_node != NULL; enemy_node = enemy_node->NextSibling())
  {
    enemy_element = enemy_node->ToElement();
    // Enemy sprite frame name taken from "name" attribute of <Enemy>
    Enemy* enemy = Enemy::createWithSpriteFrameName(this, 
    enemy_element->Attribute("name"));
    // Enemy score taken from "score" attribute of <Enemy>
    enemy->setScore(enemy_element->IntAttribute("score"));
    // add Enemy to batch node & array
    sprite_batch_node_->addChild(enemy, E_LAYER_ENEMIES_BRICKS);
    enemies_->addObject(enemy);

    // Enemy position taken from "position" attribute of <Enemy>
    CCPoint position = GameGlobals::GetPointFromString(
    string(enemy_element->Attribute("position")));
    enemy->setPosition(position);

    // save enemies at the left & right extremes
    left_side_enemy_position_ = (position.x < 
    left_side_enemy_position_) ? position.x : left_side_enemy_position_;
    right_side_enemy_position_ = (position.x > 
    right_side_enemy_position_) ? position.x : right_side_enemy_position_;

    // save size of largest enemy
    CCSize size = enemy->getContentSize();
    max_enemy_size_.width = (size.width > 
    max_enemy_size_.width) ? size.width:max_enemy_size_.width;
    max_enemy_size_.height = (size.height > 
    max_enemy_size_.height) ? size.height:max_enemy_size_.height;
  }
}

Before creating any enemies, the move_duration and fire_rate attributes are stored into the enemy_movement_duration and enemy_fire_rate variables, respectively. We then created and retained two CCArrays:enemies_ and enemy_bullets_ to store the enemies and enemy bullets, respectively.

Then, in a loop, we iterated through the EnemySet and created an object of the Enemy class to represent all the enemies in this level. We then set the score for each Enemy entity before adding it to the sprite_batch_node_ object and the enemies_ object respectively. Then, we positioned this Enemy entity based on the position attribute and one of our helper functions from GameGlobals. We also saved the position of the left-most and right-most enemies and also the size of the largest enemy's sprite. We will use these values while moving the enemies.

Creating bricks

Now, we will create bricks by iterating through the BrickSet tag. This is taken care of by the CreateBricks function, as shown in the following code:

void GameWorld::CreateBricks(tinyxml2::XMLNode* brick_set)
{
  // create array to hold bricks
  bricks_ = CCArray::create();
  bricks_->retain();

  // iterate through <BrickSet> and create Brick objects
  tinyxml2::XMLElement* brick_element = NULL;
  for(tinyxml2::XMLNode* brick_node = brick_set->FirstChild(); 
  brick_node != NULL; brick_node = brick_node->NextSibling())
  {
    brick_element = brick_node->ToElement();
    // Brick sprite frame name taken from "name" attribute of <Brick>
    Brick* brick = Brick::createWithSpriteFrameName(
    brick_element->Attribute("name"));
    // Brick score taken from "score" attribute of <Brick>
    brick->setScore(brick_element->IntAttribute("score"));
    // Brick position taken from "position" attribute of <Brick>
    brick->setPosition(GameGlobals::GetPointFromString(string(
    brick_element->Attribute("position"))));
    // add Brick to batch node & array
    sprite_batch_node_->addChild(brick, E_LAYER_ENEMIES_BRICKS);
    bricks_->addObject(brick);
  }
}

Just as in the CreateEnemies function, we started by creating and retaining an object of class CCArray named bricks_, to hold all the Brick objects. We then iterated through the brick_set and created the Brick objects, set their score and position, and finally added them to the sprite_batch_node_ object and to the bricks_ object respectively.

Creating the player

Now, we will create the Player entity in the CreatePlayer function:

void GameWorld::CreatePlayer()
{
  // create & add Player to batch node
  player_ = Player::createWithSpriteFrameName(this, "sfgun");
  sprite_batch_node_->addChild(player_, E_LAYER_PLAYER);

  // create array to hold Player bullets
  player_bullets_ = CCArray::createWithCapacity(MAX_BULLETS);
  player_bullets_->retain();

  // initialize Player properties
  player_->setLives(lives_to_carry_);
  player_->setIsRespawning(false);
  // tell Player to move into the screen
  player_->Enter();
}

In the CreatePlayer function, we created an object of the class Player and added it to the sprite_batch_node_ object. We also created and retained a CCArray to hold the player's bullets for this level. Finally, we initialized the player's attributes and called the Enter function.

Creating HUD elements

The last thing we need to write before we complete our CreateLevel function is the CreateHUD function:

void GameWorld::CreateHUD()
{
  // create & add "score" text
  CCSprite* score_sprite = 
    CCSprite::createWithSpriteFrameName("sfscore");
  score_sprite->setPosition(ccp(SCREEN_SIZE.width*0.15f, 
    SCREEN_SIZE.height*0.925f));
  sprite_batch_node_->addChild(score_sprite, E_LAYER_HUD);

  // create & add "lives" text
  CCSprite* lives_sprite = 
    CCSprite::createWithSpriteFrameName("sflives");
  lives_sprite->setPosition(ccp(SCREEN_SIZE.width*0.7f, 
    SCREEN_SIZE.height*0.925f));
  sprite_batch_node_->addChild(lives_sprite, E_LAYER_HUD);

  // create & add score label
  char buf[8] = {0};
  sprintf(buf, "%04d", score_);
  score_label_ = CCLabelBMFont::create(buf, "sftext.fnt");
  score_label_->setPosition(ccp(SCREEN_SIZE.width*0.3f, 
    SCREEN_SIZE.height*0.925f));
  addChild(score_label_, E_LAYER_HUD);

  // save size of life sprite
  CCSize icon_size = CCSpriteFrameCache::sharedSpriteFrameCache()->
    spriteFrameByName("sflifei")->getOriginalSize();
  // create array to hold life sprites
  life_sprites_ = CCArray::createWithCapacity(player_->getLives());
  life_sprites_->retain();

  // position life sprites some distance away from "life" text
  float offset_x = lives_sprite->getPositionX() + 
    lives_sprite->getContentSize().width*1.5f + icon_size.width;
  for(int i = 0; i < player_->getLives(); ++i)
  {
    // position each life sprite further away from "life" text
    offset_x -= icon_size.width * 1.5f;
    CCSprite* icon_sprite = CCSprite::createWithSpriteFrameName("sflifei");
    icon_sprite->setPosition(ccp( offset_x, SCREEN_SIZE.height*0.925f));
    // add life sprite to batch node & array
    sprite_batch_node_->addChild(icon_sprite, E_LAYER_HUD);
    life_sprites_->addObject(icon_sprite);
  }

  // create & add the pause menu containing pause button
  CCMenuItemSprite* pause_button = CCMenuItemSprite::create(
    CCSprite::createWithSpriteFrameName("sfpause"), 
    CCSprite::createWithSpriteFrameName("sfpause"), this, 
    menu_selector(GameWorld::OnPauseClicked));
  pause_button->setPosition(ccp(SCREEN_SIZE.width*0.95f, 
    SCREEN_SIZE.height*0.925f));
  CCMenu* menu = CCMenu::create(pause_button, NULL);
  menu->setAnchorPoint(CCPointZero);
  menu->setPosition(CCPointZero);
  addChild(menu, E_LAYER_HUD);
} 

We started by creating and adding CCSprite objects for the score and lives, respectively. Usually, labels are used for these kinds of HUD elements but we used sprite frames in this specific case.

Next, we created the score label using the CCLabelBMFont class that provides us with a label that uses a bitmap font. We need to supply the string that we want displayed along with the path to the .fnt file to the create function of the CCLabelBMFont class.

Note

Use CCLabelBMFont when you have texts that need to be updated regularly. They update much faster as compared to CCLabelTTF. Additionally, CCLabelBMFont inherits from CCSpriteBatchNode—every character within the label can be accessed separately and hence can have different properties!

We also need to add sprites to represent how many lives the player had left. In the for loop, we created and added sprites to the sprite_batch_node_. We also added these sprites to a CCArray named life_sprites_ because we will need to remove them one by one as the player loses a life.

So, we finally created a CCMenu containing a CCMenuItemSprite for the pause button and added it to GameWorld. That sums up level creation and we are now ready to begin playing.

The start and stop functions

Now that we have created the level with enemies, bricks and a player, it's time to start playing. So let's see what happens in the StartGame function. Remember that this function is called from the Player class after the player has finished entering the screen. The code is as follows:

void GameWorld::StartGame()
{
  // call this function only once when the game starts
  if(has_game_started_)
    return;

  has_game_started_ = true;
  // start firing player & enemy bullets
  schedule(schedule_selector(GameWorld::FirePlayerBullet),
    player_fire_rate_);
  schedule(schedule_selector(GameWorld::FireEnemyBullet), 
    enemy_fire_rate_);
  // start moving enemies
  StartMovingEnemies();
}

This function starts with a conditional that ensures it is called only once. We then scheduled two functions, FirePlayerBullet and FireEnemyBullet, at intervals of player_fire_rate_, and enemy_fire_rate_, respectively. Finally, we called StartMovingEnemies, which we will get to in a bit.

Now, let's take a look at the StopGame function:

void GameWorld::StopGame()
{
  has_game_stopped_ = true;
  // stop firing player & enemy bullets
  unschedule(schedule_selector(GameWorld::FirePlayerBullet));
  unschedule(schedule_selector(GameWorld::FireEnemyBullet));

  // stop Enemy movement
  CCObject* object = NULL;
  CCARRAY_FOREACH(enemies_, object)
  {
    CCSprite* enemy = (CCSprite*)object;
    if(enemy)
    {
      enemy->stopAllActions();
    }
  }
}

We first set has_game_stopped_ to true, which is important to the update function. We then unscheduled the functions responsible for firing the player and enemy bullets that we just saw using the unschedule function. Finally, we needed the enemies to stop moving too, so we iterated over the enemies_ array and called stopAllActions on each Enemy entity. What is important to know is that the StopGame function is called whenever the level is complete or the game is over.

Moving the enemies

Just to add some liveliness to the game, we move all the enemies across the screen from side to side. Let's take a look at the StartMovingEnemies function to get a clear idea:

void GameWorld::StartMovingEnemies()
{
  // compute maximum distance movable on both sides
  float max_distance_left = left_side_enemy_position_;
  float max_distance_right = SCREEN_SIZE.width - 
    right_side_enemy_position_;
  // compute how much distance to cover per step
  float distance_per_move = max_enemy_size_.width*0.5;
  
  // calculate how many steps on both sides
  int max_moves_left = floor(max_distance_left / distance_per_move);
  int max_moves_right = floor(max_distance_right / distance_per_move);
  int moves_between_left_right = floor( (right_side_enemy_position_ - 
    left_side_enemy_position_) / distance_per_move );

  CCActionInterval* move_left = CCSequence::createWithTwoActions(
    CCDelayTime::create(enemy_movement_duration_), 
    CCEaseSineOut::create(CCMoveBy::create(0.25f, 
    ccp(distance_per_move*-1, 0))));
  CCActionInterval* move_right = CCSequence::createWithTwoActions(
    CCDelayTime::create(enemy_movement_duration_), 
    CCEaseSineOut::create(CCMoveBy::create(0.25f, 
    ccp(distance_per_move, 0))));
  CCActionInterval* move_start_to_left = CCRepeat::create(
    move_left, max_moves_left);
  CCActionInterval* move_left_to_start = CCRepeat::create(
    move_right, max_moves_left);
  CCActionInterval* move_start_to_right = CCRepeat::create(
    move_right, max_moves_right);
  CCActionInterval* move_right_to_start = CCRepeat::create(
    move_left, max_moves_right);
  CCActionInterval* movement_sequence = CCSequence::create(
    move_start_to_left, move_left_to_start, move_start_to_right, 
    move_right_to_start, NULL);

  // Move each Enemy
  CCObject* object = NULL;
  CCARRAY_FOREACH(enemies_, object)
  {
    CCSprite* enemy = (CCSprite*)object;
    if(enemy)
    {
      enemy->runAction(CCRepeatForever::create( (CCActionInterval*)
        movement_sequence->copy() ));
    }
  }
}

We started by calculating the maximum distance the group of enemies can move both towards the left and the right. We also fixed the amount of distance to cover in one single step. We can now calculate how many steps it will take to reach the left and right edge of the screen and also how many steps are there between the left and right edges of the screen. Then, we created a sleuth of actions that will move the entire bunch of enemies repeatedly from side to side.

We defined the actions to move a single Enemy entity one step to the left and one step to the right into variables move_left and move_right, respectively. This movement needs to occur in steps, so the previous actions are a CCSequence of CCDelayTime followed by CCMoveBy. We then create four actions move_start_to_left, move_left_to_start, move_start_to_right, and move_right_to_start to take the entire group of enemies from their starting position to the left edge of the screen, then back to the starting position, then to the right edge of the screen and back to the start position. We created a CCSequence of these four actions and repeated them on every Enemy object in enemies_.

Fire the bullets!

Now that we have everything set in place, it's time for some fire power. The code for firing both player and enemy bullets is almost the same, so I will only go over the FirePlayerBullet and RemovePlayerBullet functions:

void GameWorld::FirePlayerBullet(float dt)
{
  // position the bullet slightly above Player
  CCPoint bullet_position = ccpAdd(player_->getPosition(), 
    ccp(0, player_->getContentSize().height * 0.3));

  // create & add the bullet sprite
  CCSprite* bullet = CCSprite::createWithSpriteFrameName("sfbullet");
  sprite_batch_node_->addChild(bullet, E_LAYER_BULLETS);
  player_bullets_->addObject(bullet);

  // initialize position & scale
  bullet->setPosition(bullet_position);
  bullet->setScale(0.5f);

  // animate the bullet's entry
  CCScaleTo* scale_up = CCScaleTo::create(0.25f, 1.0f);
  bullet->runAction(scale_up);

  // move the bullet up
  CCMoveTo* move_up = CCMoveTo::create(BULLET_MOVE_DURATION, 
    ccp(bullet_position.x, SCREEN_SIZE.height));
  CCCallFuncN* remove = CCCallFuncN::create(this, callfuncN_selector(
    GameWorld::RemovePlayerBullet));
  bullet->runAction(CCSequence::createWithTwoActions(move_up, remove));

  SOUND_ENGINE->playEffect("shoot_player.wav");
}

We started by calculating the position of the bullet a bit above the player. We then created the player's bullet and added it to both sprite_batch_node_ and player_bullets_. Next, we set the position and scale properties for the bullet sprite before running an action to scale it up. Finally, we created the action to move and the callback to RemovePlayerBullet once the move is finished. We ran a sequence of these two actions and played a sound effect to finish our FirePlayerBullet function.

Let's take a look at the following code:

void GameWorld::RemovePlayerBullet(CCNode* bullet)
{
  // remove bullet from list & GameWorld
  player_bullets_->removeObject(bullet);
  bullet->removeFromParentAndCleanup(true);
}

The RemovePlayerBullet function simply removes the bullet's sprite from the sprite_batch_node_ and player_bullets_ and is called when the bullet leaves the top edge of the screen or when it collides with an enemy or brick.

The update function

We call the scheduleUpdate function at the end of the CreateLevel function, thus we need to define an update function that will be called by the engine at every tick:

void GameWorld::update(float dt)
{
  // no collision checking if game has not started OR has stopped 
    OR is paused
  if(!has_game_started_ || has_game_stopped_ || is_game_paused_)
    return;

  CheckCollisions();
}

You were expecting a bigger update function, weren't you? Well, when you have Cocos2d-x doing so much work for you, all you need to do is check for collisions. However, it is important that these collisions not be checked before the game has started, after it has stopped, or when it has been paused for obvious reasons. So, let's move ahead to collision detection in the next section.

Checking for collisions

Collision detection in this game will happen between the player's bullet and the enemies and bricks, and between the enemies' bullets and the player. Let's dive into the CheckCollisions function:

void GameWorld::CheckCollisions()
{
  CCObject* object = NULL;
  CCSprite* bullet = NULL;
  bool found_collision = false;

  // collisions between player bullets and bricks & enemies
  CCARRAY_FOREACH(player_bullets_, object)
  {
    bullet = (CCSprite*)object;
    if(bullet)
    {
      CCRect bullet_aabb = bullet->boundingBox();
      
      object = NULL;
      CCARRAY_FOREACH(bricks_, object)
      {
        CCSprite* brick = (CCSprite*)object;
        // rectangular collision detection between player bullet & brick
        if(brick && bullet_aabb.intersectsRect(brick->boundingBox()))
        {
          // on collision, remove brick & player bullet
          RemoveBrick(brick);
          RemovePlayerBullet(bullet);
          found_collision = true;
          break;
        }
      }

      // found collision so stop checking
      if(found_collision)
        break;

      object = NULL;
      CCARRAY_FOREACH(enemies_, object)
      {
        CCSprite* enemy = (CCSprite*)object;
        // rectangular collision detection between player bullet & enemy
        if(enemy && bullet_aabb.intersectsRect(enemy->boundingBox()))
        {
          // on collision, remove enemy & player bullet
          RemoveEnemy(enemy);
          RemovePlayerBullet(bullet);
          found_collision = true;
          break;
        }
      }

      // found collision so stop checking
      if(found_collision)
        break;
    }
  }

  // no collision checking with player when player is respawning
  if(player_->getIsRespawning())
    return;

    .
    .
    .
}

We first checked for collisions between the player bullets, enemies, and bricks. Thus, we iterated over player_bullets_. Within this loop, we iterated through enemies_ and bricks_. Then, we called the boundingBox function that we had overridden in the CustomSprite class. Thus, we conducted a simple rectangular collision detection. If a collision was found, we called the RemoveBrick or RemoveEnemy function, followed by the function RemovePlayerBullet.

If the player wasn't respawning, we checked for collisions between the enemy bullets and the player in a similar way. If a collision was found, we told the player to die and called the ReduceLives function followed by the RemoveEnemyBullet function. The ReduceLives function simply removes one of the tiny life sprites from the HUD. This portion of the function has been left out and can be found in the source bundle for this chapter.

Let's quickly go over the functions we call when collisions occur under the different circumstances, starting with the RemoveEnemy and RemoveBrick functions:

void GameWorld::RemoveEnemy(CCSprite* enemy)
{
  // remove Enemy from array
  enemies_->removeObject(enemy);
  // tell Enemy to die & credit score
  AddScore(((Enemy*)enemy)->Die());

  // if all enemies are dead, level is complete
  if(enemies_->count() <= 0)
    LevelComplete();
}

void GameWorld::RemoveBrick(CCSprite* brick)
{
  // remove Brick from array
  bricks_->removeObject(brick);
  // tell Brick to crumble & credit score
  AddScore(((Brick*)brick)->Crumble());
}

The RemoveEnemy function first removes the enemy from enemies_ and then tells the enemy to die. The score returned from the Die function of Enemy is then passed to the AddScore function, which basically updates the score_ variable as well as the score label on the HUD. Finally, if all the enemies have been killed, the level is completed. The RemoveBricks function is virtually the same except there is no checking for level completion, as the player doesn't need to destroy all the bricks to complete a level.

Touch controls

So we have created the level, given the enemies some movement, implemented both enemy and player bullet firing, and implemented collision detection. But the Player is still stuck at the center of the screen. So, let's give him some movement and add some basic touch control:

void GameWorld::ccTouchesBegan(CCSet* set, CCEvent* event)
{
  CCTouch* touch = (CCTouch*)(*set->begin());
  CCPoint touch_point = touch->getLocationInView();
  touch_point = CCDirector::sharedDirector()->convertToGL(touch_point);
  HandleTouch(touch_point);
}

void GameWorld::ccTouchesMoved(CCSet* set, CCEvent* event)
{
  CCTouch* touch = (CCTouch*)(*set->begin());
  CCPoint touch_point = touch->getLocationInView();
  touch_point = CCDirector::sharedDirector()->convertToGL(touch_point);
  HandleTouch(touch_point);
}

void GameWorld::HandleTouch(CCPoint touch)
{
  // don't take touch when a popup is active & when player is respawning
  if(is_popup_active_ || player_->getIsRespawning())
    return;

  player_->setPositionX(touch.x);
}

Both the ccTouchesBegan and ccTouchesMoved functions call the HandleTouch function with the converted touch point as parameter. The HandleTouch function then sets the player's x position to the touch's x position if there is no popup or if the player is not respawning.

Level complete and game over

A given level is complete when all enemies have been killed, and the game is over when all the player's lives are over. Let's see what happens in the LevelComplete and GameOver functions:

void GameWorld::LevelComplete()
{
  // tell player to leave screen
  player_->Leave();
  // stop game & update level variables
  StopGame();
  lives_to_carry_ = player_->getLives();
  score_to_carry_ = score_;
  // create & add the level complete popup
  LevelCompletePopup* level_complete_popup = LevelCompletePopup::create(
    this, score_, player_->getLives());
  addChild(level_complete_popup, E_LAYER_POPUP);
  SOUND_ENGINE->playEffect("level_complete.wav");
}

Now that the level is complete, we ask the player to leave the current level and stop the game. We need to carry forward the player's lives and score to the next level. Finally, we created and added the LevelCompletePopup and played a sound effect.

Let's take a look at the following code:

void GameWorld::GameOver()
{
  // stop game & reset level variables
  StopGame();
  current_level_ = 1;
  lives_to_carry_ = 3;
  score_to_carry_ = 0;
  // create & add the game over popup
  GameOverPopup* game_over_popup = GameOverPopup::create(this, score_);
  addChild(game_over_popup, E_LAYER_POPUP);
  SOUND_ENGINE->playEffect("game_over.wav");
}

The player has lost all his lives so we must stop the game and reset the level number, lives, and score variables. We also added the GameOverPopup and played a sound effect.

With these last two functions, we have completed our third game in this book and our first Cocos2d-x game. I have skipped over some functionality in this chapter, for example, the animations on the MainMenu, the BackgroundManager class that takes care of the environment, and the Popups class. I urge you to go through the code bundle for this chapter to understand them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.131.10