- Cocos2d-x Game Development Blueprints
- Karan Sequeira
- 4688字
- 2021-07-16 13:47:58
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:
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 pe 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.
- C# Programming Cookbook
- OpenStack Orchestration
- 從零開始學C語言
- Java實戰(第2版)
- 微服務架構深度解析:原理、實踐與進階
- C++20高級編程
- Mastering ArcGIS Enterprise Administration
- 快速入門與進階:Creo 4·0全實例精講
- 交互式程序設計(第2版)
- Learning Kotlin by building Android Applications
- 虛擬現實建模與編程(SketchUp+OSG開發技術)
- Java EE實用教程
- 前端Serverless:面向全棧的無服務器架構實戰
- Scratch 3.0少兒積木式編程(6~10歲)
- 自然語言處理NLP從入門到項目實戰:Python語言實現