The Problem
One of the biggest frustrations I’ve had while playing Tower Defense games over the years is… wasted bullets. The designers/developers of the game you are playing have meticulously (hopefully?) poured over tower fire/reload delay times, enemy speed/health stat charts scratched out and rewritten out several times in one of three legal pads that they keep around their house, because they never can seem to find the one they wrote in last.
Here’s the scenario. After a little proficiency with Game X, You’ve got your towers set up to where they are decimating wave after wave… just chewing through guys. And it happens so fast, you might not catch it. As this is my own particular pet peeve affliction, my stupid brain Watches for it; expects it. Take a wave of small, very-fast-moving enemies. They’re zipping right along the maze you’ve set up and the first few enemies arrive at your expertly configured, tactically sound, death field kill zone…
Tower 1, 2, 3, 4, and 5 fire a bullet at the closest enemy. As soon as Tower 1’s bullet hits the enemy, the enemy dies. Mind you, Towers 2 through 5 still have bullets in the air heading towards a dead enemy. 8 times out of 10* what happens next? The bullets disappear. Wasted. Bullets. Your towers have spent their rounds, they are waiting on a reload timer to tick complete before they can throw another bullet at those enemy jackholes that just keep on coming and the last shot they fired just didn’t count at all. It just fizzled. Sadness. On a side note, maybe that should be a game mechanic… Tower Sadness level? Awful. Just awful.
Sorry, anyways, during the milliseconds between all 5 of those towers firing at the same enemy, Enemy#2, the Enemy just behind that poor dumb sucker at the head of the line, will have run halfway through your towers’ ranges. Your towers finally reload and get ready to shoot, and look at that, all 5 shoot at Enemy#2. It’s still kindof in range. All 5 towers spawn bullets and begin reloading. All 5 bullets fly towards Enemy#2 and when the first one or two kill Enemy#2, the others vanish. More. Wasted. Bullets.
The Fix
I’ve been hard at work converting my previous “ZFEngine” to using the Starling framework. As with any new framework, a lot of things have made sense and have gone rather quickly. And some things have come with a pretty steep learning curve and taken longer. Just tonight I finished up some code interacting between Enemies, Towers, Bullets, and a CollisionManager handling the general frame-by-frame math of which enemy is in range of which tower. I just solved this problem, this pet peeve frustration and wanted to share. I plan on writing out the full game demo tutorials in a few months once everything is finished, hopefully the following relevant functions are helpful without being impossible to understand without the complete code.
The Code
CollisionManager.as
public function checkEnemiesInRange():void {
var eLen:int = _enemies.length;
var tLen:int = _towers.length;
// If there are no enemies or no towers, abort the check
if(eLen == 0 || tLen == 0) {
return;
}
var tDistX:Number;
var tDistY:Number;
var dist:Number;
for(var eCt:int = 0; eCt < eLen; eCt++)
{
for(var tCt:int = 0; tCt < tLen; tCt++)
{
tDistX = _enemies[eCt].x - _towers[tCt].x;
tDistY = _enemies[eCt].y - _towers[tCt].y;
// save some cycles not square rooting the value
dist = tDistX * tDistX + tDistY * tDistY;
if(dist < _towers[tCt].rangeSquared)
{
// check if enemy uid is in tower's enemies array
if(!_towers[tCt].hasEnemyInRange(_enemies[eCt])) {
// add enemy to tower
_towers[tCt].addEnemyInRange(_enemies[eCt]);
}
}
else
{
// enemy not in range of tower, check tower to see
// if it used to have the enemy in range, and remove
if(_towers[tCt].hasEnemyInRange(_enemies[eCt])) {
// Tell the tower to remove the enemy
_towers[tCt].removeEnemyInRange(_enemies[eCt]);
}
}
}
}
}
checkEnemiesInRange() runs every frame to check and see which enemies are in range of which towers.
- Line 2-3 - get lengths of towers and enemies arrays
- Line 6 - if there are no enemies or no towers, get out of this function, no need to go on mathing
- Line 14+ - loop through the enemies that have been added to the stage, loop through each tower on stage and test ranges
- Line 18-19 - get the X and Y distances between the enemy's X/Y and the tower's X/Y
- Line 22 - square distance X and distance Y, then add those together. The Tower class has a property called rangeSquared which, when the tower is created, just squares the range so we dont have to add the extra step of Math.sqrt inside this serious loop structure that happens Every Frame
- Line 24 - if the distance is less than the tower's range, the enemy is "in range"
- Line 27 - each tower has it's own array of enemies that are in range. Without CollisionManager, every tower added to the game would have to do its own range checks against every enemy. CollisionManager streamlines this by pre-calculating the distances and only giving towers the enemies that are in their range. Later in Tower.as we'll see that each tower then only has to loop through a much smaller subset of enemies (those that are in range) compared to the whole set of all enemies on stage. So each Tower can handle picking a target (based on its individual Tower strategy "closest enemy, farthest enemy, etc"), firing, reloading a possibly different rates, and then reacquiring a target from its updated list of enemies in-range this frame.
- Line 36 - Likewise, if the distance is greater than the tower's range, either the enemy has not gotten in-range yet, or the enemy has left the range of the tower, in which case we need to remove the enemy from the tower if it previously was in-range for the tower.
Tower.as
This is the update() function that runs every time TowerManager loops through its array of towers and update each one (every frame or two).
public function update():void {
if(state == TOWER_STATE_READY && _enemies.length > 0) {
var closestEnemy:Enemy,
tmpEnemy:Enemy,
closestDist:Number = rangeSquared,
len:int = _enemies.length,
tDistX:Number,
tDistY:Number,
dist:Number;
if(towerStrategy == TOWER_STRAT_NEAREST) {
for(var i:int = len - 1; i >= 0; i--) {
tmpEnemy = _enemies[i];
tDistX = tmpEnemy.x - this.x;
tDistY = tmpEnemy.y - this.y;
dist = tDistX * tDistX + tDistY * tDistY;
if(dist < closestDist) {
closestDist = dist;
closestEnemy = tmpEnemy;
}
}
if(closestEnemy) {
// get the next shot's damage
_generateNextShotDamage();
// found the closest
if(closestEnemy.willDamageBanish(nextDamage)) {
_mgr.removeEnemyFromTowers(closestEnemy);
}
// fire at enemy
fireBullet(closestEnemy);
}
}
}
}
- Line 2 - when update() is called, the Tower checks to make sure it is in a "Ready" state, as opposed to a "Reload", "Firing", or other non-Ready state. Also checks if the tower even has any enemies in range to scan for. _enemies is populated by the previously-seen CollisionManager
- Line 3-9 - declaring some vars outside of my loop
- Line 5 - Special Note here... closestDist is set to my Tower's rangeSquared. So the "closestDist on record so far is the furthest the tower can reach"
- Line 11 - Just a quick note about this early code, I have an if statement here checking the "towerStrategy" because for now, there is only one strategy. When this actually goes into an efficient, solid OOP codebase for a game, TowerStrategy will be abstracted out into separate classes. Instead of the if statement here, you'd just see something like "currentTowerStrategy.process()". For the "nearest" TowerStrategy, you'd seem basically this same loop inside it's process() function. But that would easily allow users to swap out a tower's strategy with a keypress. Obviously all TowerStrategy child classes would all have a process() function so this one line of code would work for any TowerStrategy. Design Patterns! ...ahem... moving on.
- Line 14-16 - get X/Y distances and calculate squared distance
- Line 17-19 - if this is the distance this loop iteration is less than previous closestDistances, update closestDist to this new dist, and update closestEnemy to this current enemy in the loop
- Line 23 - with CollisionManager supposedly only giving Towers enemies that are in range, the Tower should not have to have this check. I think removing this would show me if I had any logic errors elsewhere, but this check is here in this preliminary code.
- Line 25 - this function runs to get the next bullet's damage. If the tower had a min-max damage range, that function is where we'd generate the actual next damage value for the bullet we're about to shoot. That function also handles any modifiers, buffs, etc to the damage from the tower to the enemy from the tower's perspective. So the tower gathers all of its buffs and modifiers and says, BAM, here's the next shot's damage. That whole value gets passed along via the Bullet to the Enemy class at which time the Enemy's takeDamage() function would handle if that enemy actually took the full value of damage, or if the enemy had its own buffs/modifiers/armor/resistance/etc for that type of damage. Which reminds me... I need to add a damageType to my Bullet and Tower classes.
- Line 27 - HERE IT IS! This is the whole point of this blog post. Before I fire a single bullet at the enemy, do a simple check with the enemy I'm about to hit, "will my nextDamage amount kill you?" If the answer to that question is YES! then the Tower does a call to its manager (TowerManager) and says "hey boss, remove this enemy from ALL Towers' _enemies arrays so that no other tower tries to shoot at this guy... he's about to get whacked by this next shot!"
- Line 28 - calls the removeEnemyFromTowers() function on the TowerManager, passing in the specific Enemy we want removed.
- Line 32 - After no other towers are going to be firing at our enemy, Now we can fire this Tower's bullet at the closestEnemy.
Enemy.as
Alright, I won't leave you hanging... here's the Enemy side of that function.
public function willDamageBanish(dmg:Number):Boolean {
// deal damage to _fluxHP to see if this
// damage amount will banish enemy
_fluxHP -= _getDamageAfterMods(dmg);
if(_fluxHP <= 0) {
willBeBanished = true;
}
return willBeBanished;
}
- Line 4 - the Enemy class has, what I call, "fluxHP". This is separate from currentHP & maxHP which are "official" values/states for the enemy... the Enemy officially has this set currentHP to determine life or death. However, at Enemy initialization, this fluxHP & currentHP both get set to maxHP. So fluxHP starts the same as currentHP, but when doing these initial damage checks the damage comes off fluxHP first before the Tower has fired to see if it will drop below zero. Once the actual Bullet hits the actual Enemy, then that damage would officially be deducted from currentHP
- Line 4 - _getDamageAfterMods() - it's now the Enemy's turn to mitigate damage if possible. It takes the damage from the Bullet and does any reduction of damage based on armor/resistances/etc. Hmm... good point, maybe there should be a stat that tracks how much damage you fired at the enemy, and how much damage the enemy shrugged off... That would also get handled there in that _getDamageAfterMods function 🙂
- Line 6 - willBeBanished ... will it? Separate Boolean from isBanished or isEscaped.
So, hopefully that helps a little!
*I say 8 times out of 10 because, 1 time out of 10 maybe you're playing the Onslaught2 series where your rockets fly around in circles until another suitable enemy can be found to lock on to, at which point you're kindof just awesomely piling up a series of missles in the air waiting to deal death. And the other (8+1+this=10? yeah) time out of 10 is when you're playing a game coded by a dev or designer or playtester who also noticed this very irritating pet peeve and had a coder fix it.
0 Comments