Our Tower
class will inherit from CCSprite
and will contain all the properties that we saw in the TowerDataSet
and TowerData
structures. In addition to that, the Tower
class will also possess the behavior of targeting and shooting enemies, as well as upgrading itself.
Let's take a look at how the Tower
class is declared in Tower.h
:
class Tower: public CCSprite { public: Tower(); virtual ~Tower(); static Tower* create(GameWorld* game_world, int type, CCPoint position); virtual bool init(GameWorld* game_world, int type, CCPoint position); // copy the data within the TowerDataSet library inside GameGlobals void SetTowerProperties(); // update functions virtual void Update(); void UpdateRotation(); // functions that take care of upgradation & resale void Upgrade(); void Sell(); // basic tower behaviour void CheckForEnemies(); void SetTarget(Enemy* enemy); void Shoot(float dt); void ShootBullet(); void ShootLightning(); // show the range for this tower void CreateRangeNode(); void ShowRange(); // accessors & mutators void SetSpriteName(const char* sprite_name); void SetIsRotating(bool is_rotating); inline void SetBulletName(const char* bullet_name) { bullet_name_ = bullet_name; } inline void SetIsLightning(bool is_lightning) { is_lightning_ = is_lightning; } inline bool GetIsLightning() { return is_lightning_; } inline void SetRange(float range) { range_ = range * TILE_SIZE; } inline float GetRange() { return range_; } inline void SetPhysicalDamage(float physical_damage) { physical_damage_ = physical_damage; } inline float GetPhysicalDamage() { return physical_damage_; } inline void SetMagicalDamage(float magical_damage) { magical_damage_ = magical_damage; } inline float GetMagicalDamage() { return magical_damage_; } inline void SetSpeedDamage(float speed_damage) { speed_damage_ = speed_damage; } inline float GetSpeedDamage() { return speed_damage_; } inline void SetSpeedDamageDuration( float speed_damage_duration) { speed_damage_duration_ = speed_damage_duration; } inline float GetSpeedDamageDuration() { return speed_damage_duration_; } inline void SetFireRate( float fire_rate) { fire_rate_ = fire_rate; } inline float GetFireRate() { return fire_rate_; } inline void SetCost(int cost) { cost_ = cost; } inline int GetCost() { return cost_; } inline int GetType() { return type_; } inline int GetLevel() { return current_level_; } protected: GameWorld* game_world_; // properties that define the tower // these take values straight from the TowerDataSet & TowerData structs int type_; const char* bullet_name_; bool is_lightning_; bool is_rotating_; float range_; float physical_damage_; float magical_damage_; float speed_damage_; float speed_damage_duration_; float fire_rate_; int cost_; // the level of upgrade the tower is currently at int current_level_; // the tower's current target Enemy* target_; // a sprite to represent the base for a rotating tower CCSprite* base_sprite_; // a node to draw the circular range for this tower CCDrawNode* range_node_; };
The top half of the declaration of this class deals with the behavior of this tower. You can see the Upgrade
and Sell
functions that do exactly what their names suggest, followed by functions CheckForEnemies
, SetTarget
, and the shoot functions. You can also see a function that will create and show the range for this tower.
If you look at the bottom half of the class declaration, where all the member variables are declared, you will notice that they are identical to the member variables inside the TowerDataSet
and TowerData
structures. The SetTowerProperties
function of the Tower
class takes care of filling these member variables with values from GameGlobals
, based on the type_
and current_level_
variables. Let's take a look at how this happens in Tower.cpp
:
void Tower::SetTowerProperties() { // tower properties are set from the TowerDataSet & TowerData structs SetBulletName(GameGlobals::tower_data_sets_[ type_]->bullet_name_); SetIsLightning(GameGlobals::tower_data_sets_[ type_]->is_lightning_); SetIsRotating(GameGlobals::tower_data_sets_[ type_]->is_rotating_); SetSpriteName(GameGlobals::tower_data_sets_[ type_]->tower_data_[current_level_]->sprite_name_); SetRange(GameGlobals::tower_data_sets_[ type_]->tower_data_[current_level_]->range_); SetPhysicalDamage(GameGlobals::tower_data_sets_[ type_]->tower_data_[current_level_]->physical_damage_); SetMagicalDamage(GameGlobals::tower_data_sets_[ type_]->tower_data_[current_level_]->magical_damage_); SetSpeedDamage(GameGlobals::tower_data_sets_[ type_]->tower_data_[current_level_]->speed_damage_); SetSpeedDamageDuration(GameGlobals::tower_data_sets_[ type_]->tower_data_[current_level_]->speed_damage_duration_); SetFireRate(GameGlobals::tower_data_sets_[ type_]->tower_data_[current_level_]->fire_rate_); SetCost(GameGlobals::tower_data_sets_[ type_]->tower_data_[current_level_]->cost_); }
If you remember, each tower is represented by a TowerDataSet
object inside the tower_data_sets_
vector. Hence, we use the type_
variable as an index to access all the properties of a particular tower stored inside tower_data_sets_
.
Notice how the current_level_
variable is used to access the appropriate TowerData
object. This structure makes it a no-brainer for us to implement the upgrades to our towers. We will simply increment the current_level_
variable and call the SetTowerProperties
function to equip this tower with the subsequent upgrade. This is exactly what we do in the Upgrade
function from Tower.cpp
:
void Tower::Upgrade() { // are there any upgrades left? if(current_level_ >= NUM_TOWER_UPGRADES - 1) { return; } // increment upgrade level and reset tower properties ++ current_level_; SetTowerProperties(); // debit cash game_world_->UpdateCash(-cost_); // reset the range range_node_->removeFromParentAndCleanup(true); range_node_ = NULL; ShowRange(); }
At the top of the function, we ensure that the tower doesn't upgrade infinitely. The NUM_TOWER_UPGRADES
constant is assigned a value of 3
in GameGlobals.h
. Then we simply increment the current_level_
variable and call SetTowerProperties
.
Since upgrades are not free, we must tell GameWorld
exactly how much this particular upgrade has set the player back by calling the UpdateCash
function. Finally, since an upgrade may increase the range of the tower, we remove the range_node_
and call the ShowRange
function. This will recreate range_node_
and visually communicate to the player the increase in the tower's range after the upgrade.
Let's focus on the targeting functionality of a tower by defining the CheckForEnemies
and SetTarget
functions of the Tower
class within Tower.cpp
:
void Tower::CheckForEnemies() { // only check the current wave for enemies Wave* curr_wave = game_world_->GetCurrentWave(); if(curr_wave == NULL) { return; } // search for a target only when there isn't one already if(target_ == NULL) { // loop through each enemy in the current wave for(int i = 0; i < curr_wave->num_enemies_; ++i) { Enemy* curr_enemy = curr_wave->enemies_[i]; // save this enemy as a target it if it still alive and if it is within range if(curr_enemy->GetHasDied() == false && ccpDistance( m_obPosition, curr_enemy->getPosition()) <= ( range_ + curr_enemy->GetRadius())) { SetTarget(curr_enemy); break; } } } // check if a target should still be considered if(target_ != NULL) { // a target is still valid if it is alive and if it is within range if(target_->GetHasDied() == true || ccpDistance( m_obPosition, target_->getPosition()) > ( range_ + target_->GetRadius())) { SetTarget(NULL); } } }
The first thing this function does is query GameWorld
for the wave that is currently on screen, and returns in the case that there is none. There might be a couple of occasions when the player might place a bunch of towers before the first wave has even started.
If the tower currently has no target, we must loop through each enemy in the current wave in an attempt to find if any are within the tower's range. We must also ensure that the tower doesn't target an enemy that is already dead. Once these two conditions are met, we save the enemy as this tower's target by passing a pointer to the enemy's object into the SetTarget
function, before breaking from this loop.
In addition to finding enemies within range, the tower must also realize when enemies have escaped its range or have died and stop shooting at them. In the last part of the CheckForEnemies
function, we check for conditions if an enemy has died or if it has walked out of the tower's range. If these conditions are met, we set the target to NULL
.
Let's now look at the SetTarget
function that is called from within the CheckForEnemies
function in Tower.cpp
:
void Tower::SetTarget(Enemy* enemy) { target_ = enemy; if(target_ != NULL) { // shoot as soon as you get a target Shoot(0.0f); schedule(schedule_selector(Tower::Shoot), fire_rate_); } else { // stop shooting when you lose a target unschedule(schedule_selector(Tower::Shoot)); } }
The SetTarget
function simply stores a reference to the enemy and either schedules or unschedules the Shoot
function as required. Notice how the fire_rate_
variable is used as the interval for the schedule
function. Things are about to get exciting now as we define the Shoot
, ShootBullet
, and ShootLightning
functions.
The Shoot
function simply checks the value of the is_lightning_
flag and calls either ShootBullet
or ShootLightning
. So, we're heading straight into the action with the ShootBullet
function of Tower.cpp
:
void Tower::ShootBullet() { float bullet_move_duration = ccpDistance(m_obPosition, target_->getPosition()) / TILE_SIZE * BULLET_MOVE_DURATION; // damage the enemy CCActionInterval* damage_enemy = CCSequence::createWithTwoActions( CCDelayTime::create(bullet_move_duration), CCCallFuncO::create( target_, callfuncO_selector(Enemy::TakeDamage), this)); target_->runAction(damage_enemy); // create the bullet CCSprite* bullet = CCSprite::create(bullet_name_); bullet->setScale(0.0f); bullet->setPosition(m_obPosition); game_world_->addChild(bullet, E_LAYER_TOWER - 1); // animate the bullet CCActionInterval* scale_up = CCScaleTo::create(0.05f, 1.0f); bullet->runAction(scale_up); // move the bullet then remove it CCActionInterval* move = CCSequence::create( CCMoveTo::create(bullet_move_duration, target_->getPosition()), CCRemoveSelf::create(true), NULL); bullet->runAction(move); }
The ShootBullet
function begins by calculating how long this particular bullet must travel to reach its target. The constant BULLET_MOVE_DURATION
is defined in GameGlobals.h
with a value of 0.15f
and signifies the amount of time a bullet will take to travel a single tile.
Then, we inform the enemy that it must take some damage from this tower by creating a sequence of CCDelayTime
and CCCallFuncO
. We pass this tower's object as the CCObject
pointer as we're using the callfuncO_selector
selector type. The delay is to ensure that the enemy doesn't get hurt before the bullet has reached it.
We now create a new CCSprite
for the bullet, using the bullet_name_
variable as input, then add it to GameWorld
and run a simple move-remove sequence on it. Next up is the function that shoots the bolt of lightning. First, let's take a detour to create a Lightning
class that we can use inside the ShootLightning
function.
18.222.114.28