AS3 Starling Tower Defense Tutorial – Part 5 – Enemies

It’s finally time to get into the good stuff!  In this post we’ll be looking at enemies and how they get set up, run through, and knocked down. We’ll look at how I’ve created some JSON metadata in the map JSON file to set up the types of enemies the map will contain, the sounds to use for those enemies, the different enemy groups that can be in a single wave, how those groups are defined, and how individual enemies are created inside those groups. Let’s take a quick high-level view at how I’ve structured my data and my classes. We’ll get to the actual JSON in a sec.

Before you start, feel free to check out the finished product of my AS3 Starling TD Demo.
You can also find all of the code used in srcview.
Or you can download the whole project zipped up.
Or check out the repo on Bitbucket

JSON Data Structure

Enemy Data

I’m going to hit EnemyWaves first, since the other two relate more to each other. Enemy Wave data here doesn’t pertain to an actual class file in the project, but it is used by the classes as a sort of map/hash set. For example, when the user starts the game and the first wave tile at the bottom of the screen “spawns”, that wave tile has an id of “wave1″. So the EnemyManager class will go through and say, “Ok, ‘wave1′ needs to spawn, which groups should I use?”

1
2
3
4
{
   "id": "wave1",
   "groups":"group1",
},

When ‘wave1′ spawns, the EnemyManager will check those “groups” which correspond to the ids of the EnemyGroups below. So ‘wave1′ spawns and EnemyManager goes to find EnemyGroup id ‘group1′ and tells it to begin spawning.

An EnemyGroup is a collection of Enemy classes (and subclasses) that all share a common set of Waypoints for that map. If you have one road on a map, you’ve probably just got one set of waypoint data, “All enemies walk this path from (10,10) to (100,10) to…” then the above JSON data would be fine for your enemyWaves groups definition. But lets say you’ve got two roads on your map, and so there are two different sets of waypoints, one set is for the group that spawns on the left side of the screen, one set is for the group that spawns at the top of the screen.

1
2
3
4
{
   "id": "wave1",
   "groups":"group1a,group1b",
},

In Map 2 of my demo there are two roads, and two separate enemy groups. And the above JSON code is used to spawn the first wave. The EnemyGroup designated with an id “group1a” and “group1b” will be spawned when the wave “wave1″ is spawned.

So we’ve got enemyTypes that correspond to com.zf.objects.enemy.EnemyType.as, enemyGroups that correspond to com.zf.objects.enemy.EnemyEnemyGroup.as, and inside of enemyGroups, enemies that correspond to com.zf.objects.enemy.Enemy.as as a base class. Let’s look at an enemy type excerpt from the JSON.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
   "id": "enemyA",
   "name": "Enemy A",
   "klass": "EnemyA",
   "sounds": [
      { 
         "state": "onEscape",
         "soundId": "escape1"
      },
      { 
         "state": "onBanish",
         "soundId": "banish1"
      }
   ]
},

Think of an EnemyType as the metadata outlining a specific sub-class of enemies. All enemies inherit from the base Enemy class. However, the above “enemyA” is going to use a specific sub-class of Enemy, com.zf.objects.enemy.types.EnemyA.as to be specific. Since that’s the “klass” attribute, we’re going to use the EnemyA class instead of the boring, old Enemy.as class. Though EnemyA extends Enemy, so we very much use most all of that class. We’ll see later.

So, an EnemyType definition lets us specify an internal id for the type, a name to display to the user for that type of enemy, a class to use for that type, and different sounds which consist of sound states. For example, as we’ll see, at the base Enemy class, when the Enemy escapes/leaks from the map, the base Enemy class will call a function that basically says, “play the ‘onEscape’ sound state if it exists.” So, in the case of enemyA, we’ve defined “onEscape” to be “escape1″ so when enemyA escapes from the map, escape1 will play. If I did not include an “onEscape” state for the enemy type, then nothing would play. You’re right though, I could certainly define a base set of sounds for the base Enemy class to default to, but I did not for this tutorial.

Let’s look at the enemyGroups definitions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
   "id": "group1",
   "name": "Group 1",
   "spawnDelay": "800",
   "wave": "wave1",
   "waypointGroup": "wpGroup1",
   "enemies": [
      { 
         "typeId": "enemyA", 
         "level": 1 
      },
      { 
         "typeId": "enemyB", 
         "level": 1 
      },
      { "typeId": "enemyB", "level": 1 },
      { "typeId": "enemyA", "level": 1 },
      { "typeId": "enemyB", "level": 1 },
      { "typeId": "enemyA", "level": 1 }
   ]
},

So this EnemyGroup consists of 6 total enemies that will be spawning 0.8 seconds apart from each other. If you wanted a “swarm” level, you may have 30 enemies defined with a spawnDelay of “100” or less! You may want all of your groups to be of the same EnemyType, or mixed types… however you want to do it. The beauty of having all that information in this JSON file is that your designers, or your level editors, or your playtesters can open this file in a text editor and change the number of enemies, spawn delay, types of enemies, etc in every wave. Then they save the file and refresh their browser. This will save them YEARS of time so they can actually tweak these settings themselves and find the right balance. This saves you YEARS of time from having this crap hardcoded and mister level editor has to have you recompile a new SWF every time he wants the enemies to spawn “just a little faster”. You don’t know if that’s 200ms faster or 500ms faster so you split the difference at 375ms faster and he really meant a second faster. You find that out 4 builds later when he just says “can you just make it one second faster?” With the data in the file, everyone can take care of their own tweaking.

ALRIGHT! Enough about JSON data nonsense. Let’s get to the AS3! That’s what we all came here for anyways…

Enemy.as

My enemy graphics were taken from a spritesheet that can be found here. I have contacted Flipz, the artist who created the tilesheet, and he was cool enough to let me borrow his images.

This is the entire base Enemy class file. We’ll go through it step by step below. I’m going to try to cut it to ~100-line chunks to make it easier to digest and please remember my formatting is jacked up so we can see more of each line, and my doc blocks have been removed to save space.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package com.zf.objects.enemy
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.states.Play;
   import com.zf.ui.healthBar.HealthBar;
 
   import org.osflash.signals.Signal;
 
   import starling.display.MovieClip;
   import starling.display.Sprite;
 
public class Enemy extends Sprite
{
   public static const ENEMY_DIR_UP:String = 'enemyUp';
   public static const ENEMY_DIR_RIGHT:String = 'enemyRight';
   public static const ENEMY_DIR_DOWN:String = 'enemyDown';
   public static const ENEMY_DIR_LEFT:String = 'enemyLeft';
 
   public static const ENEMY_SND_STATE_BANISH:String = "onBanish";
   public static const ENEMY_SND_STATE_ESCAPE:String = "onEscape";
 
   // set default speed to 1
   public var speed:Number = 1;
   public var maxHP:Number = 10;
   public var currentHP:Number = 10;
   public var reward:int = 5;
   public var damage:int = 1;
   public var isEscaped:Boolean = false;
   public var isBanished:Boolean = false;
   public var willBeBanished:Boolean = false;
 
   public var onDestroy:Signal;
   public var onBanished:Signal;
   public var type:String = "Enemy";
   public var uid:int;
   public var totalDist:Number;
 
   protected var _animState:String;
   protected var _animData:Object;
   protected var _animTexturesPrefix:String;
   protected var _soundData:Object;
 
   protected var _distToNext:Number;
   protected var _currentWPIndex:int;
   protected var _waypoints:Array = [];
   protected var _healthBar:HealthBar;
 
   // enemy speed * currentGameSpeed
   protected var _enemyGameSpeed:Number;
   protected var _enemyGameSpeedFPS:int;
   protected var _enemyBaseFPS:int = 12;
 
   /**
    * This is an Enemy's "currentHP" factoring in all bullets currently
    * flying towards it. So if this is <= 0, the enemy may still be alive,
    * but bullets have already been spawned with it's name on it
    */
   protected var _fluxHP:Number;
 
   public function Enemy()
   {
      uid = Config.getUID();
 
      _setInternalSpeeds();
 
      _setupAnimData();
      _changeAnimState(ENEMY_DIR_RIGHT);
      pivotX = width >> 1;
      pivotY = height >> 1;
 
      _soundData = {};
 
      onDestroy = new Signal(Enemy);
      onBanished = new Signal(Enemy);
 
      // let enemy listen for game speed change
      Config.onGameSpeedChange.add(onGameSpeedChange);
   }
 
   public function init(wps:Array, dist:Number):void {
      isEscaped = false;
      isBanished = false;
      willBeBanished = false
      _waypoints = wps;
      totalDist = dist;
      _distToNext = _waypoints[0].distToNext;
 
      // clear the old animState
      _animState = '';
      // set new animState
      _changeAnimState(_waypoints[0].nextDir);
 
      // reset WP index
      _currentWPIndex = 0;
      x = _waypoints[0].centerPoint.x;
      y = _waypoints[0].centerPoint.y;
 
      // reset current and _flux back to maxHP
      currentHP = _fluxHP = maxHP;
 
      _healthBar = new HealthBar(this, currentHP, maxHP, 30);
      _healthBar.x = -20;
      _healthBar.y = -10;
      addChild(_healthBar);
}

Continuing Enemy.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public function update():void {
   _distToNext -= _enemyGameSpeed;
 
   totalDist -= _enemyGameSpeed;
 
   if(_distToNext <= 0) {
      _getNextWaypoint();
   }
 
   switch(_animState) {
      case ENEMY_DIR_UP:
         y -= _enemyGameSpeed;
         break;
 
      case ENEMY_DIR_RIGHT:
         x += _enemyGameSpeed;
         break;
 
      case ENEMY_DIR_DOWN:
         y += _enemyGameSpeed;
         break;
 
      case ENEMY_DIR_LEFT:
         x -= _enemyGameSpeed;
         break;
   }
}
 
public function takeDamage(dmgAmt:Number):void {
   Config.log('Enemy', 'takeDamage', uid + " is taking " + dmgAmt + " damage");
   if(!isEscaped) {
      Config.totals.totalDamage += dmgAmt;
 
      currentHP -= dmgAmt;
      if(_healthBar) {
         _healthBar.takeDamage(dmgAmt);
      }
 
      if(currentHP <= 0) {
         _handleBanished();
      }
 
      Config.log('Enemy', 'takeDamage', "Enemy " + uid + " has " + currentHP + " hp remaining");
   }
}
 
public function onGameSpeedChange():void {
   _removeAnimDataFromJuggler();
 
   _setInternalSpeeds();
 
   _setupAnimData();
 
   _changeAnimState(_animState, true);
}
 
public function willDamageBanish(dmg:Number):Boolean {
   // deal damage to _fluxHP to see if this
   // damage amount will banish enemy
   _fluxHP -= dmg;
   if(_fluxHP <= 0) {
      willBeBanished = true;
   }
   Config.log('Enemy', 'willDamageBanish', "Enemy " + uid + " _fluxHP " + _fluxHP + " and willBeBanished is " + willBeBanished);
   return willBeBanished;
}
 
public function setSoundData(soundData:Array):void {
   var len:int = soundData.length;
   for(var i:int = 0; i < len; i++) {
      _soundData[soundData[i].state] = soundData[i].soundId;
   }
}
 
protected function _getNextWaypoint():void {
   _currentWPIndex++;
 
   if(_currentWPIndex < _waypoints.length - 1) {
      _changeAnimState(_waypoints[_currentWPIndex].nextDir);
      _distToNext = _waypoints[_currentWPIndex].distToNext;
   } else {
      _handleEscape();
   }
}
 
protected function _handleEscape():void {
   isEscaped = true;
   _playIfStateExists(ENEMY_SND_STATE_ESCAPE);
}
 
protected function _handleBanished():void {
   Config.log('Enemy', '_handleBanished', "Enemy " + uid + " is below 0 health and isBanished = true");
   isBanished = true;
   onBanished.dispatch(this);
   _playIfStateExists(ENEMY_SND_STATE_BANISH);
}

Finishing up Enemy.as…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
protected function _playIfStateExists(state:String):void {
   if(_soundData.hasOwnProperty(state)) {
      Game.soundMgr.playFx(_soundData[state], Config.sfxVolume);
   }
}
 
protected function _setupAnimData():void {
   //_animState = ''; // removed due to a speed change bug
   _animData = {};
 
   _animData[ENEMY_DIR_UP] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_t_'), _enemyGameSpeedFPS);
   _animData[ENEMY_DIR_RIGHT] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_r_'), _enemyGameSpeedFPS);
   _animData[ENEMY_DIR_DOWN] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_b_'), _enemyGameSpeedFPS);
   _animData[ENEMY_DIR_LEFT] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_l_'), _enemyGameSpeedFPS);
}
 
protected function _changeAnimState(newState:String, forceChange:Boolean = false):void {
   // make sure they are different states before removing and adding MCs
   // unless foreceChange is true
   if(_animState != newState || forceChange) {
 
      // _animState == '' on subsequent play throughs since init doesn't get called again
      if(_animState != '') {
         _removeAnimDataFromJuggler();
      }
      _animState = newState;
 
      _addAnimDataToJuggler()
   }
}
 
protected function _removeAnimDataFromJuggler():void {
   // remove the old MovieClip from juggler
   Play.zfJuggler.remove(_animData[_animState]);
   // remove the old MovieClip from this Sprite
   removeChild(_animData[_animState]);
}
 
protected function _addAnimDataToJuggler():void {
   // add the new MovieClip to the Juggler
   Play.zfJuggler.add(_animData[_animState]);
   // add the new MovieClip to the Sprite
   addChild(_animData[_animState]);
}
 
protected function _setInternalSpeeds():void {
   _enemyGameSpeed = Config.currentGameSpeed * speed;
   _enemyGameSpeedFPS = int(Config.currentGameSpeed * _enemyBaseFPS);
   // make sure _enemyGameSpeedFPS is at least 1
   if(_enemyGameSpeedFPS < 1) {
      _enemyGameSpeedFPS = 1;
   }
}
 
protected function _updateSpeed(spd:Number):void {
   // change speed from child classes
   speed = spd;
   // Call _setInternalSpeeds to reset internal speeds
   _setInternalSpeeds();
}
 
public function destroy():void {
   onDestroy.dispatch(this);
   onDestroy.removeAll();
   _removeAnimDataFromJuggler();
   removeChild(_healthBar);
   _healthBar = null;
   removeFromParent(true);
   Config.log('Enemy', 'destroy', "+ + " + uid + " destroyed");
}
}
}

So now that we’ve discussed the base Enemy class, let’s look at how freakin simple it is to make more enemy classes.

*OOP Note* in this example tutorial/demo, I’ve created a subclass and all it does is change some properties of the parent class. This is pretty bad OOP. If all I am doing in a subclass (in this tutorial) is changing the speed of the Enemy, and the texture prefix, I should just pass those in to the parent class, Enemy’s constructor.
var enemy:Enemy = new Enemy(speed, texturePrefix); and then do things that way. However, I chose to go the subclass route because I’m already working on more in-depth actual subclasses with new functionality in my other personal game I’m working on. So… just wanted to get that out there that I realize this is an excessive/unnecessary use of hierarchy for this specific demo.

EnemyA.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.zf.objects.enemy.types {
   import com.zf.objects.enemy.Enemy;
 
   public class EnemyA extends Enemy {
      public function EnemyA() {
         super();
         // speed for EnemyA
         _updateSpeed(1.2);
      }
 
      override protected function _setupAnimData():void {
         _animTexturesPrefix = 'enemies/enemyA';
         super._setupAnimData();
      }
   }
}

Yup. That’s it! All of our Enemy subclasses can be that short of a file. Thanks, OOP!

That’s it! All the movement code, the damage taking, the dispatching Signals when the Enemy gets killed or gets added, or reaches a waypoint and needs to change directions, all of that data has already been done in Enemy.as. Just for fun let’s look at our second Enemy subclass… EnemyB (com.zf.objects.enemy.types.EnemyB.as)

EnemyB.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.zf.objects.enemy.types {
   import com.zf.objects.enemy.Enemy;
 
   public class EnemyB extends Enemy {
      public function EnemyB() {
         super();
      }
 
      override protected function _setupAnimData():void {
         _animTexturesPrefix = 'enemies/enemyB';
         super._setupAnimData();
      }
   }
}

I do even less in this file. EnemyB just inherits Enemy’s default speed of 1.0. This makes EnemyA slightly faster than EnemyB.

With the Enemy class (and subclasses) out of the way, let’s look at the individual pieces that come together to help Enemy. We’re going to look at EnemyType. and EnemyGroup.as. After we finish both of those files, we’ll look at EnemyManager.as to tie everything together, then we’ll head out for a beer.

EnemyType.as

EnemyType.as (com.zf.objects.enemy.EnemyType.as) is a fairly small file that from the surface looks like just a data object. But there is a very significant reason this is here and with the way I am loading classes from a String in metadata, the game would not be possible without this…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.zf.objects.enemy 
{
   import com.zf.objects.enemy.types.EnemyA;
   import com.zf.objects.enemy.types.EnemyB;
 
   public class EnemyType
   {
      public var id:String;
      public var name:String;
      public var fullClass:String;
      public var soundData:Array;
 
      public function EnemyType(i:String, n:String, fC:String, sD:Array) {
         id = i;
         name = n;
         fullClass = 'com.zf.objects.enemy.types.' + fC;
         soundData = sD;
      }
 
      // create dummy versions of each enemy type, these will never be used
      private var _dummyEnemyA:EnemyA;
      private var _dummyEnemyB:EnemyB;
   }
}

EnemyGroup.as

EnemyGroup.as (com.zf.objects.enemy.EnemyGroup.as) is a bit more involved, but still under 100 lines of code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.zf.objects.enemy
{
   import com.zf.core.Config;
   import com.zf.utils.GameTimer;
 
   import flash.events.TimerEvent;
 
   import org.osflash.signals.Signal;
 
public class EnemyGroup
{
   public var id:String;
   public var name:String;
   public var spawnDelay:Number;
   public var wave:String;
   public var waypointGroup:String;
   public var enemies:Array;
   public var enemyObjects:Array;
   public var spawnTimer:GameTimer;
   public var onSpawnTimerTick:Signal;
   public var onSpawnTimerComplete:Signal;
   public var isFinished:Boolean;
 
   public function EnemyGroup(i:String, n:String, wpGroup:String, sD:Number, w:String, e:Array)
   {
      id = i;
      name = n;
      waypointGroup = wpGroup;
      spawnDelay = sD;
      wave = w;
      enemies = e;
      enemyObjects = [];
      isFinished = false;
 
      spawnTimer = new GameTimer(id, spawnDelay, enemies.length);
      spawnTimer.addEventListener(TimerEvent.TIMER, _onSpawnTimer);
      spawnTimer.addEventListener(TimerEvent.TIMER_COMPLETE, _onSpawnTimerComplete);
 
      // dispatches the enemy being spawned
      onSpawnTimerTick = new Signal();
      onSpawnTimerComplete = new Signal();
   }
 
   public function startGroup():void {
      spawnTimer.start();
   }
 
   public function pauseGroup():void {
      if(!isFinished && spawnTimer.running) {
         spawnTimer.pause();
      }
   }
 
   public function resumeGroup():void {
      if(!isFinished && spawnTimer.paused) {
         spawnTimer.start();
      }
   }
 
   private function _onSpawnTimer(evt:TimerEvent):void {
      onSpawnTimerTick.dispatch(enemyObjects.pop(), waypointGroup);
   }
 
   private function _onSpawnTimerComplete(evt:TimerEvent):void {
      isFinished = true;
   }
 
   public function destroy():void {
      Config.log('EnemyGroup', 'destroy', "+++ EnemyGroup Destroying");
      spawnTimer.removeEventListener(TimerEvent.TIMER, _onSpawnTimer);
      spawnTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, _onSpawnTimerComplete);
      spawnTimer = null;
 
      var len:int = enemyObjects.length;
      for(var i:int = 0; i < len; i++) {
         enemyObjects[i].destroy();
      }
      enemies = null;
      enemyObjects = null;
      Config.log('EnemyGroup', 'destroy', "--- EnemyGroup Destroyed");
   }
}
}

EnemyManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package com.zf.managers
{
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.enemy.EnemyGroup;
   import com.zf.objects.enemy.EnemyType;
   import com.zf.states.Play;
   import com.zf.utils.GameTimer;
 
   import flash.events.TimerEvent;
   import flash.utils.getDefinitionByName;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.extensions.PDParticleSystem;
 
public class EnemyManager implements IZFManager
{
   public var play:Play;
   public var onEnemyAdded:Signal;
   public var onEnemyRemoved:Signal;
   public var endOfEnemies:Signal;
   public var enemiesLeft:int;
   public var activeEnemies:int;
   public var delayCount:int = 0;
   public var onSpawnWave:Signal;
 
   private var _enemies:Array;
   private var _canvas:Sprite;
 
   // Holds the current map's enemy groups
   private var _enemyGroups:Object;
 
   // Holds the current map's enemy types
   private var _enemyTypes:Object;
 
   private var _enemyWaves:Object;
 
   public function EnemyManager(playState:Play)
   {
      play = playState;
      _canvas = play.enemyLayer;
      _enemies = [];
      activeEnemies = 0;
 
      onEnemyAdded = new Signal(Enemy);
      onEnemyRemoved = new Signal(Enemy);
      endOfEnemies = new Signal();
      onSpawnWave = new Signal(String);
 
      _enemyGroups = {};
      _enemyTypes = {};
      _enemyWaves = {};
   }
 
   public function update():void {
      if(_enemies.length > 0) {
         var e:Enemy;
         var len:int = _enemies.length;
         for(var i:int = len - 1; i >= 0; i--) {
            e = _enemies[i];
            e.update();
            if(e.isEscaped) {
               _handleEnemyEscaped(e);
            }
         }
      }
   }
 
   public function onGamePaused():void {
      _pauseGroups();
   }
 
   public function onGameResumed():void {
      _resumeGroups();
   }
 
   private function _pauseGroups():void {
      for each(var group:EnemyGroup in _enemyGroups) {
         group.pauseGroup();
      }
   }
 
   private function _resumeGroups():void {
      for each(var group:EnemyGroup in _enemyGroups) {
         group.resumeGroup();
      }
   }

Continuing EnemyManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
   public function destroyEnemy(e:Enemy):void {
      var len:int = _enemies.length;
      for(var i:int = 0; i < len; i++) {
         if(e == _enemies[i]) {
            Config.log('Enemy', 'destroyEnemy', "Destroying Enemy " + e.uid);
            _enemies.splice(i, 1);
            e.destroy();
            e.removeFromParent(true);
         }
      }
   }
 
   private function _spawn(e:Enemy, wpGroup:String):void {
      var waypoints:Array = play.wpMgr.getWaypointsByGroup(wpGroup);
      var totalDist:Number = play.wpMgr.getRouteDistance(wpGroup);
 
      Config.totals.enemiesSpawned++;
 
      e.init(waypoints, totalDist);
 
      e.onBanished.add(handleEnemyBanished);
 
      _enemies.push(e);
 
      activeEnemies++;
 
      _canvas.addChild(e);
 
      onEnemyAdded.dispatch(e);
   }
 
   public function spawnWave(waveId:String):void {
      Game.soundMgr.playFx('ding1', Config.sfxVolume);
 
      onSpawnWave.dispatch(waveId);
 
      Config.changeCurrentWave(1);
 
      for each(var groupName:String in _enemyWaves[waveId]) {
         _enemyGroups[groupName].startGroup();
      }
   }
 
   private function _handleEnemyEscaped(e:Enemy):void {
      enemiesLeft--;
      activeEnemies--;
 
      Config.totals.enemiesEscaped++;
 
      Config.changeCurrentHP(-e.damage);
      destroyEnemy(e)
      onEnemyRemoved.dispatch(e);
 
      if(enemiesLeft <= 0) {
         endOfEnemies.dispatch();
      }
   }
 
   public function handleEnemyBanished(e:Enemy):void {
      Config.log('EnemyManager', 'handleEnemyBanished', 'Enemy ' + e.uid + " is being destroyed");
      enemiesLeft--;
      activeEnemies--;
 
      Config.totals.enemiesBanished++;
      Config.changeCurrentGold(e.reward);
 
      onEnemyRemoved.dispatch(e);
      destroyEnemy(e);
 
      if(enemiesLeft <= 0) {
         endOfEnemies.dispatch();
      }
   }

And finally, to finish EnemyManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
   public function handleNewMapData(data:Object):void {
      var type:Object,
          group:Object,
          enemyType:EnemyType,
          enemy:*;
 
      // Create all enemy types
      for each(type in data.enemyTypes) {
         _enemyTypes[type.id] = new EnemyType(type.id, type.name, type.klass, type.sounds);
      }
 
      Config.maxWave = 0;
 
      // Create enemy wave mappings
      for each(var wave:Object in data.enemyWaves) {
         _enemyWaves[wave.id] = [];
         if(wave.groups.indexOf(',') != -1) {
            var groups:Array = wave.groups.split(',');
            _enemyWaves[wave.id] = groups;
         } else {
            _enemyWaves[wave.id].push(wave.groups);
         }
         Config.maxWave++;
      }
 
      // Create all enemy groups
      for each(group in data.enemyGroups) {
         _enemyGroups[group.id] = new EnemyGroup(group.id, group.name, group.waypointGroup, group.spawnDelay, group.wave, group.enemies);
         _enemyGroups[group.id].onSpawnTimerTick.add(onGroupSpawnTimerTick);
         _enemyGroups[group.id].onSpawnTimerComplete.add(onGroupSpawnTimerComplete);
      }
      // Create all actual enemies
      for each(group in _enemyGroups) {
         for each(var enemyObj:Object in group.enemies) {
            // get the enemyType
            enemyType = _enemyTypes[enemyObj.typeId];
 
            // Creates a new enemy type from the fullClass name
            var newEnemy:Enemy = new (getDefinitionByName(enemyType.fullClass) as Class)();
            newEnemy.setSoundData(enemyType.soundData);
 
            enemiesLeft++;
 
            // push new enemy onto object array
            group.enemyObjects.push(newEnemy);
         }
 
         // reverse array
         group.enemyObjects.reverse();
      }
   }
 
   public function onGroupSpawnTimerTick(e:Enemy, wpGroup:String):void {
      Config.log('EnemyManager', 'onGroupSpawnTimerTick', "onGroupSpawnTimerTick: " + e);
      _spawn(e, wpGroup);
   }
 
   public function onGroupSpawnTimerComplete(e:Enemy):void {
      Config.log('EnemyManager', 'onGroupSpawnTimerComplete', "onGroupSpawnTimerComplete: " + e);
   }
 
   public function destroy():void {
      Config.log('EnemyManager', 'destroy', "EnemyManager Destroying");
      _enemyTypes = null;
 
      var group:Object;
      for each(group in _enemyGroups) {
         group.destroy();
      }
      _enemyGroups = null;
 
      var len:int = _enemies.length;
      for(var i:int = 0; i < len; i++) {
         _enemies[i].destroy();
      }
 
      _enemies = null;
 
      Config.log("EnemyManager", "destroy", "EnemyManager Destroyed");
   }
}
}

There we go… That’s the full run-down on Enemies in my code! Don’t forget to check out the HealthBar class I made! It’s pretty simple, just some rectangle drawing code.

As always, feel free to check out the finished product of my AS3 Starling TD Demo.
You can also find all of the code used in srcview.
Or you can download the whole project zipped up.
Or check out the repo on Bitbucket

Thanks for reading, I hope this was helpful!

Travis

Series Navigation<< AS3 Starling Tower Defense Tutorial – Part 4 – Map TilingAS3 Starling Tower Defense Tutorial – Part 6 – Towers >>
FacebookTwitterGoogle+Share

1 Comment

 Add your comment
  1. Good blog you have got here.. It’s difficult to find high quality writing like yours nowadays. I really appreciate individuals like you! Take care!!

Leave a Comment

Your email address will not be published.