The Enemy
class inherits from CCSprite
just like the Tower
class and will contain all the properties of the struct
named EnemyData
. The Enemy
class defines the functionalities to make the enemy walk along the path dictated by the level, do damage if it reaches the end of the path, take damage from a tower, and die. Let's take a look at the class declaration in the Enemy.h
file:
class Enemy: public CCSprite { public: Enemy(); virtual ~Enemy(); static Enemy* create(GameWorld* game_world, int type); virtual bool init(GameWorld* game_world, int type); // copy data within the enemy library inside GameGlobals virtual void SetEnemyProperties(); // create & update the progress bar showing health left void CreateHealthBar(); void UpdateHealthBar(); // basic enemy behaviour functions void StartWalking(); void FinishWalking(); void DoDamage(); void TakeDamage(CCObject* object); void Die(); void TakeSpeedDamage(float speed_damage, float speed_damage_duration); void ResetSpeed(float dt); // accessors and mutators inline void SetAnimationName(const char* animation_name) { animation_name_ = animation_name; } inline void SetHealth(int health) { health_ = health; } inline int GetHealth() { return health_; } inline void SetArmor(int armor) { armor_ = armor; } inline int GetArmor() { return armor_; } inline void SetMagicResistance(int magic_resistance) { magic_resistance_ = magic_resistance; } inline int GetMagicResistance() { return magic_resistance_; } inline void SetSpeed(float speed) { speed_ = speed; } inline float GetSpeed() { return speed_; } inline void SetDamage(int damage) { damage_ = damage; } inline int GetDamage() { return damage_; } inline void SetReward(int reward) { reward_ = reward; } inline int GetReward() { return reward_; } inline float GetRadius() { return radius_; } inline bool GetHasDied() { return has_died_; } inline bool GetIsSlowed() { return is_slowed_; } inline void SetWalkPoints(int num_walk_points, vector<CCPoint> walk_points) { num_walk_points_ = num_walk_points; walk_points_ = walk_points; } protected: GameWorld* game_world_; // properties that define the enemy // these take values straight from the EnemyData struct int type_; const char* animation_name_; int health_; int armor_; int magic_resistance_; float speed_; int damage_; int reward_; // more properties that define the enemy float radius_; int health_left_; bool has_died_; bool is_slowed_; int num_walk_points_; int curr_walk_point_; vector<CCPoint> walk_points_; // the progress bar showing health left CCProgressTimer* health_bar_; };
The first half of this class declares the functions that will describe the enemy's behavior. In the second half of the class, you will find the variables that are copied from the EnemyData
structure.
You can also see a vector of CCPoint
named walk_points_
. This vector will copy all the positions stored inside the GameWorld
class' enemy_walk_points_
vector we created while parsing the level's TMX file. We also have a CCProgressTimer
object that we will use to show the enemy's health.
Without further ado, let's begin defining the enemy's behavior with the init
function from Enemy.cpp
:
bool Enemy::init(GameWorld* game_world, int type) { if(!CCSprite::init()) { return false; } // save reference to GameWorld & type of enemy game_world_ = game_world; type_ = type; // set the enemy's properties SetEnemyProperties(); // fetch the first frame of animation so we know the size of this enemy CCAnimation* animation = CCAnimationCache::sharedAnimationCache()-> animationByName(animation_name_); radius_ = ((CCAnimationFrame*)animation->getFrames()-> objectAtIndex(0))->getSpriteFrame()->getOriginalSize().width/2; // hide the enemy till it starts walking setVisible(false); CreateHealthBar(); return true; }
The init
function is passed a reference to GameWorld
and an integer variable named type
. The wave feeds the value of the variable type
into its enemy_list_
vector when GameWorld
parses the level's XML file. We will use this type
variable as an index while copying the properties from GameGlobals
to the SetEnemyProperties
function.
The init
function also saves the size of this enemy into a radius_
variable to be used when being targeted by a tower. Since all enemies from all waves are added to the node graph right at the start of the level, we initially hide the enemies to avoid unnecessary rendering. We finally call the CreateHealthBar
function to wind up the init
function.
Since we haven't really covered the use of a CCProgressTimer
object in previous chapters, let's take a look at the CreateHealthBar
function of Enemy.cpp
to see how this is done:
void Enemy::CreateHealthBar(){ CCPoint position = ccp(radius_, radius_ * 1.75f); // sprite behind the progress bar CCSprite* red_bar = CCSprite::create("red_bar.png"); red_bar->setPosition(position); addChild(red_bar); // create a horizontal progress bar health_bar_ = CCProgressTimer::create(CCSprite::create("green_bar.png")); health_bar_->setType(kCCProgressTimerTypeBar); // progress bar takes values from 0 to 100 health_bar_->setPercentage( (float)health_left_ / (float)health_ * 100 ); health_bar_->setMidpoint(ccp(0, 1)); health_bar_->setBarChangeRate(ccp(1, 0)); health_bar_->setPosition(position); addChild(health_bar_); }
We begin the function by calculating the position where this progress timer will be added. We then go ahead and add a background sprite to give us an illustration of the progress timer filling or emptying.
We then create a CCProgressTimer
object, passing in a CCSprite
, position it, and add it to the enemy. We set the type of this progress timer as kCCProgressTimerTypeBar
and set the percentage of the progress timer that must be filled, where 0
is empty, 50
is half full, and 100
is completely full.
The next two lines are very important to understand as they define the behavior of a progress timer. The bar change rate, in the context of a bar type progress timer, controls the direction in which the progress timer will expand. For a vertically expanding progress timer, you'd have to set the bar change rate as (0, 1), and for a horizontally expanding one as (1, 0).
Once the bar change rate has decided whether the progress timer expands vertically or horizontally, the midpoint decides whether it should expand up-to-down or down-to-up for a vertically expanding bar, and left-to-right or right-to-left for a horizontally expanding bar. So if you want the progress timer to expand down-to-up your midpoint would have to be (1, 0).
To summarize the way in which these variables control the behavior of a progress timer, take a look at the following table:
Behavior |
Bar change rate |
Midpoint |
---|---|---|
Vertical (down = 0 percent to up = 100 percent) |
(0, 1) |
(0, 1) |
Vertical (up = 0 percent to down = 100 percent) |
(0, 1) |
(1, 0) |
Horizontal (left = 0 percent to right = 100 percent) |
(1, 0) |
(0, 1) |
Horizontal (right = 0 percent to left = 100 percent) |
(1, 0) |
(0, 1) |
The CreateHealthBar
function then positions the progress timer and adds it to the enemy. Let's now look at the movement logic of the enemy in the StartWalking
function of Enemy.cpp
:
void Enemy::StartWalking() { // show the enemy when it starts walking setVisible(true); // position the enemy at the first walking point setPosition(walk_points_[curr_walk_point_]); // calculate duration in terms of time taken to walk a single tile float duration = speed_ * ENEMY_MOVE_DURATION * (ccpDistance( walk_points_[curr_walk_point_ + 1], walk_points_[curr_walk_point_]) / TILE_SIZE); // walk to the subsequent walk point CCActionInterval* walk = CCMoveTo::create(duration, walk_points_[curr_walk_point_ + 1]); CCActionInstant* finish_walking = CCCallFunc::create( this, callfunc_selector(Enemy::FinishWalking)); CCActionInterval* walk_sequence = CCSequence::createWithTwoActions( walk, finish_walking); // create a speed action to control the walking speed CCAction* walk_action = CCSpeed::create(walk_sequence, 1.0f); walk_action->setTag(ENEMY_MOVE_ACTION_TAG); runAction(walk_action); if(getActionByTag(ENEMY_ANIMATE_ACTION_TAG) == NULL) { CCActionInterval* animation = CCAnimate::create( CCAnimationCache::sharedAnimationCache()-> animationByName(animation_name_)); animation->setTag(ENEMY_ANIMATE_ACTION_TAG); runAction(animation); } }
Remember how we hid the enemy in the init
function? We set its visibility to true
and position it at the first walk point only when it is commanded to start walking. This vector of walk_points_
is filled when the enemy is spawned by GameWorld
.
We must now calculate the amount of time the enemy will take, walking at its current speed (speed_
), to reach the next walk point. We create a simple CCMoveTo
action and sequence it with CCCallFunc
to call the FinishWalking
function.
Here is the interesting part: we use CCSpeed
to control the execution speed of the walk_sequence
action interval. Thus, we pass walk_sequence
into the CCSpeed::create
function along with the default speed at which we want this action to run. Now, by calling the setSpeed
function of the CCSpeed
class, we can slow down or speed up the action passed to it. We also set the tag of this CCSpeed
action to ENEMY_MOVE_ACTION_TAG
so that we can fetch it later. Why would we need to control the speed of the enemy's movement? Well, because we have towers that can slow down enemies!
As you can see, we don't run really run the walk_sequence
action (of type CCSequence
) that we created, but instead run walk_action
action (of type CCSpeed)
. This is because the CCSpeed
action will internally step whatever action it is created with. Consequently, the CCSpeed
action will end when its inner action ends.
With the movement logic figured out, we simply run a CCAnimate
to animate this enemy and finish the StartWalking
function. We shall now move to the function that gets called when the enemy reaches the destination walk point, that is, the FinishWalking
function of Enemy.cpp
:
void Enemy::FinishWalking() { // can't stop walking if already dead if(has_died_) { return; } // move to the subsequent walk point ++ curr_walk_point_; if(curr_walk_point_ < num_walk_points_ - 1) { StartWalking(); } // enemy has reached the pumpkin else { DoDamage(); } }
We first check if the enemy is still alive and return from the function if the enemy has died while walking from one walk point to the next. We then increment the current_walk_point_
counter and check whether there are any more walk points left.
If there is a subsequent walk point, we simply call the StartWalking
function so that the enemy starts moving towards the next walk point. If there are no walk points left, it means the enemy has reached the pumpkin and we call the DoDamage
function.
We have completed the movement logic for the Enemy
class. Now we need to define the way in which the enemy damages the pumpkin, and the way in which the enemy takes damage from the towers. So let's begin with the DoDamage
function of Enemy.cpp
:
void Enemy::DoDamage() { // inform GameWorld that damage must be done game_world_->EnemyAtTheGates(this); stopAllActions(); // hide the enemy setVisible(false); }
This is a very lazy enemy indeed. It uses CCMoveTo
to walk and makes GameWorld
do all the work. The enemy simply informs GameWorld
that it has reached the pumpkin and passes in a reference to itself. It then hides itself, so no further processing is wasted on it.
Now, an enemy is damaged whenever a tower shoots a bullet or lightning bolt at it. This happens in the ShootBullet
and ShootLightning
functions (as we saw when defining the Tower
class). These functions run a CCCallFuncO
action to call the TakeDamage
function of the Enemy
class. So let's look at the way in which an enemy takes damage inside the TakeDamage
function of Enemy.cpp
:
void Enemy::TakeDamage(CCObject* object) { // sometimes a dead enemy might get shot if(has_died_) { return; } Tower* tower = (Tower*)object; // calculate total damage taken by this enemy from a given tower float physical_damage = tower->GetPhysicalDamage() - armor_; float magical_damage = tower->GetMagicalDamage() - magic_resistance_; float total_damage = (physical_damage > 0 ? physical_damage : 0) + (magical_damage > 0 ? magical_damage : 0); health_left_ -= total_damage; // slow the enemy if not already being slowed & if the tower has speed damage if(is_slowed_ == false && tower->GetSpeedDamage() < 1.0f) { TakeSpeedDamage(tower->GetSpeedDamage(), tower->GetSpeedDamageDuration()); } // check if enemy should die if(health_left_ <= 0) { Die(); } // update the health bar health_bar_->setPercentage( (float)health_left_ / (float)health_ * 100 ); }
This enemy cannot be damaged if it has already been killed. There may be times when multiple towers are simultaneously targeting this particular enemy. Then, the enemy might die by the first tower alone, and subsequent attacks must be ignored.
We now extract the Tower
object passed to this function with the CCCallFuncO
action that was run on this enemy object. We can then use this Tower
object to calculate the total damage that this enemy must suffer. The total damage is the sum of all physical and magical damage levied upon this enemy, with the quantities of armor and magic resistance subtracted. We subtract this total damage from the enemy's remaining health.
Now, if the tower has the capability to slow the enemy, we must call the TakeSpeedDamage
function, passing in the quantity of speed damage and the duration for which the enemy must be slowed. If the enemy has no health left, we must call the Die
function. Finally, we update the progress timer used to display the enemy's health.
We saw two new functions in the TakeDamage
function: TakeSpeedDamage
and Die
. Let's see how the enemy dies in the Die
function of Enemy.cpp
:
void Enemy::Die() { // inform GameWorld that an enemy has died has_died_ = true; game_world_->EnemyDown(this); stopAllActions(); runAction(CCSequence::createWithTwoActions( CCEaseBackIn::create(CCScaleTo::create(0.2f, 0.0f)), CCHide::create())); }
We first set the has_died_
flag and then inform the GameWorld
that an enemy has died, passing in a pointer to this enemy's object. Then we must stop all actions running on this enemy and run one last action to animate this enemy's death and finally hide it.
Moving on to the TakeSpeedDamage
function of Enemy.cpp
:
void Enemy::TakeSpeedDamage(float speed_damage, float speed_damage_duration) { // reduce the walking speed is_slowed_ = true; CCSpeed* walk_action = (CCSpeed*)getActionByTag( ENEMY_MOVE_ACTION_TAG); if(walk_action != NULL) { walk_action->setSpeed(speed_damage); // walking speed must return back to normal after certain duration scheduleOnce(schedule_selector(Enemy::ResetSpeed), speed_damage_duration); } }
We begin by setting the is_slowed_
variable to true
so that the enemy isn't slowed more than once at a time. We then fetch the CCSpeed
action and call its setSpeed
function. This manipulates the speed of its inner action, which in our case is the enemy's walking sequence. We also schedule a function to return the enemy's walking sequence to normal speed after the specified duration in the ResetSpeed
function.
So far, we have defined the movement, take damage, and do damage behaviors of our enemy—thereby completing the Enemy
class. We still have a decent amount of work left. We need to stitch everything together in our GameWorld
class, implement a simple gesture recognition mechanism to spawn our towers, and discuss a special feature at the end!
3.144.97.187