AS3 Starling Tower Defense Tutorial – Part 6 – Towers

After five long code-heavy tutorials, we finally arrive at the most central of components to the Tower Defense (TD) genre: Towers! Towers are crucial in your TD game. Now, by “towers” I don’t necessarily mean some phallic, stone structure jutting into the sky that rains death upon enemies nearby. “Towers” here could be any unit, object, creature, etc that the player can place on the map, that can impede the enemies from reaching their destination. Some games may have actual tower structures, some games may have army guys with bazookas, some games may have a house or spawning structure you place nearby and the player’s units spawn from there to stop enemies. The tower is the primary mechanic that empowers players with a way to experience strategy, tactics, accomplishment, and fun (not an exhaustive list of nouns!).

That said, unfortunately I really don’t do the tower justice in this demo. If you’ve played the demo, you’ve seen that my two Towers are simply a blue and a red box. The enemies are animated and much much prettier than the towers. Hell, even the bullets have some particles with them and are prettier than the towers. Anyways, here’s the point. I showed you last time how I created the Enemy animations. You could do the exact same thing for Towers and have yourself some pretty animated towers with different states “reloading”, “firing”, etc.

Before we 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

**Update 8/1/13 – abstracted out Tower’s enemy-finding strategy to a ITowerStrategy.as and TowerStrategyNearest.as. Also added some commentary on design patterns.

Ok, like the previous post on enemies, we’ll start with the JSON data of how we define towers.

JSON Data Structure

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
{
   "towers": [
      {
         "id": "tower1",
         "index": 0,
         "name": "Blue Tower",
         "imageName": "towers/tower1_001",
         "bulletImageName": "bullets/sparkle_blue_001",
         "bulletSpeed": 12,
         "bulletWidth": 32,
         "bulletHeight": 32,
         "towerWidth": 20,
         "towerHeight": 20,
         "maxLevel": 3,
         "sounds": [
            { 
               "state": "onFire",
               "soundId": "shot1"
            }
         ],
         "levelData": [
            {
               "level": 1,
               "range": 75,
               "damage": 5,
               "speed": 1500,
               "dps": 3.33,
               "cost": 10
            },
            {
               "level": 2,
               "range": 150,
               "damage": 10,
               "speed": 1250,
               "dps": 8,
               "cost": 30
            },
            {
               "level": 3,
               "range": 200,
               "damage": 20,
               "speed": 900,
               "dps": 22.22,
               "cost": 50
            }
         ]
      }
   ]
}

This is the first tower listed in the src/assets/json/towers/towerData.json file that defines two types of towers. Let’s look at it line-by-line.

If you check out the file, you’ll see the other tower data listed there as well. Now we’re going to follow the data into the TowerManager and see what happens to it, then afterwards we’ll actually look at our Tower class and how that works.

TowerManager.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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.zf.managers
{
   import com.zf.core.Config;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.tower.Tower;
   import com.zf.states.Play;
 
   import flash.geom.Point;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Sprite;
   import starling.events.Touch;
   import starling.events.TouchEvent;
   import starling.events.TouchPhase;
 
public class TowerManager implements IZFManager
{
   public var play:Play;
   public var onTowerRemoved:Signal;
   public var onTowerAdded:Signal;
   public var onTowerManagerRemovingEnemy:Signal;
 
   private var _towers:Array;
   private var _towerData:Object;
   private var _currentTower:Tower;
   private var _p:Point = new Point();
   private var _isDragging:Boolean = false;
   private var _canvas:Sprite;
 
   public function TowerManager(playState:Play, towerData:Object) {
      play = playState;
      _canvas = play.towerLayer;
      _towers = [];
      _setTowerData(towerData);
 
      onTowerAdded = new Signal(Tower);
      onTowerRemoved = new Signal(Tower);
      onTowerManagerRemovingEnemy = new Signal(Enemy);
   }
 
   public function update():void {
      if(_towers.length > 0) {
         var t:Tower,
             len:int = _towers.length;
         for(var i:int = len - 1; i >= 0; i--) {
            t = _towers[i];
            t.update();
         }
      }
   }
 
   public function destroy():void {}
 
   public function destroyTower(t:Tower):void {
      var len:int = _towers.length;
      for(var i:int = 0; i < len; i++) {
         if(t == _towers[i]) {
            _towers.splice(i, 1);
            t.destroy();
            t.removeFromParent(true);
         }
      }
      onTowerRemoved.dispatch(t);
   }
 
   public function createNewTower(towerID:String, pos:Point):void {
      if(_currentTower) {
         _currentTower.deactivate();
      }
 
      var tower:Tower = new Tower(_towerData[towerID], this);
      tower.setSoundData(_towerData[towerID].sounds);
      tower.x = pos.x - tower.halfWidth;
      tower.y = pos.y - tower.halfHeight;
      tower.activated = true;
      _currentTower = tower;
 
      play.addChild(tower);
      play.addEventListener(TouchEvent.TOUCH, towerFollowMouse);
   }
 
   public function towerFollowMouse(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(play);
      if(touch)
      {
         switch(touch.phase) {
            case TouchPhase.BEGAN:
               var checkObject:Object = play.map.checkCanPlaceTower(_p);
               if(checkObject.canPlace) {
                  play.removeEventListener(TouchEvent.TOUCH, towerFollowMouse);
                  evt.stopImmediatePropagation();
                  placeTower(checkObject.point);
               }
               break;
 
            case TouchPhase.ENDED:
               break;
 
            case TouchPhase.MOVED:
               break;
 
            case TouchPhase.HOVER:
               var tPos:Point = touch.getLocation(play);
               _currentTower.x = _p.x = tPos.x - _currentTower.halfWidth;
               _currentTower.y = _p.y = tPos.y - _currentTower.halfHeight;
 
               // Check if we can place, then update the tower image accordingly
               _currentTower.enterFrameTowerPlaceCheck(play.map.checkCanPlaceTower(_p).canPlace);
               break;
         }
      }
   }

Quick break… then continuing with TowerManager.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
   public function removeEnemyFromTowers(e:Enemy):void {
      Config.log('TowerManager', 'removeEnemyFromTowers', "Removing Enemy " + e.uid + " from Towers");
      var len:int = _towers.length;
      for(var i:int = 0; i < len; i++) {
         _towers[i].removeEnemyInRange(e);
      }
      onTowerManagerRemovingEnemy.dispatch(e);
   }
 
   public function addListenerToDeactivateTower(t:Tower):void {
      if(_currentTower != t) {
         // handle previous _currentTower
         Config.log('TowerManager', 'addListenerToDeactivateTower', 'TowerManager.addListenerToDeactivateTower() -- deactivating old tower');
         _currentTower.deactivate();
      }
      _currentTower = t;
      Config.log('TowerManager', 'addListenerToDeactivateTower', "TowerManager.addListenerToDeactivateTower() -- adding listener: " + play.map.width + ", " + play.map.height);
      play.map.addEventListener(TouchEvent.TOUCH, _onMapClicked);
   }
 
   public function placeTower(p:Point):void {
      Config.totals.towersPlaced++;
 
      var xOffset:int = int((play.map.tileWidth - (_currentTower.halfWidth << 1)) >> 1) + play.mapOffsetX;
      var yOffset:int = int((play.map.tileHeight - (_currentTower.halfHeight << 1)) >> 1) + play.mapOffsetY;
 
      _currentTower.x = p.x + xOffset;
      _currentTower.y = p.y + yOffset;
 
      _canvas.addChild(_currentTower);
      play.map.placedTower(_currentTower);
      _towers.push(_currentTower);
      _currentTower.init();
      _currentTower.onFiring.add(play.bulletMgr.onTowerFiring);
      onTowerAdded.dispatch(_currentTower);
   }
 
   public function sellTower(t:Tower):void {
      // Add half the cost back to currentGold This can be modified later!
      Config.changeCurrentGold(int(t.cost >> 1));
      destroyTower(t);
   }
 
   private function _onMapClicked(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(play.map, TouchPhase.BEGAN);
      if (touch)
      {
         var localPos:Point = touch.getLocation(play.map);
         Config.log('TowerManager', '_onMapClicked', "mapClicked! " + play.map.width + " -- " + localPos.x);
         // if we clicked anywhere but the tower
         if(touch.target != _currentTower) {
            play.hudMgr.hideTowerData(_currentTower);
            _currentTower.deactivate();
            play.map.removeEventListener(TouchEvent.TOUCH, _onMapClicked);
         }
      }
   }
 
   public function getTowerCost(towerID:String, level:int):int {
      return _towerData[towerID].levelData[level].cost;
   }
 
   private function _setTowerData(td:Object):void {
      _towerData = {};
      for each(var data:Object in td.towers) {
         _towerData[data.id] = data;
      }
   }
 
   // If TowerManager needs to do anything on Pause/Resume
   public function onGamePaused():void {}
   public function onGameResumed():void {}
}
}

Alright… so now we’re through the TowerManager.as. Let’s look at the actual Tower.as.

Update 8/1/13 – updated Tower to actually use a proper abstracted TowerStrategy which we’ll talk about later.

Tower.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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package com.zf.objects.tower
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.managers.TowerManager;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.tower.strategies.*;
   import com.zf.states.Play;
 
   import flash.display.Bitmap;
   import flash.display.BitmapData;
   import flash.display.Shape;
   import flash.events.TimerEvent;
   import flash.geom.Matrix;
   import flash.utils.Timer;
   import flash.utils.clearInterval;
 
   import org.osflash.signals.Signal;
 
   import starling.core.Starling;
   import starling.display.Image;
   import starling.display.MovieClip;
   import starling.display.Sprite;
   import starling.events.Touch;
   import starling.events.TouchEvent;
   import starling.events.TouchPhase;
 
public class Tower extends Sprite
{
   public static const TOWER_STATE_NEW	  : int = 0;
   public static const TOWER_STATE_INIT	  : int = 1;
   public static const TOWER_STATE_READY  : int = 2;
   public static const TOWER_STATE_RELOAD : int = 3;
   public static const TOWER_STATE_FIRING : int = 4;
 
   public static const TOWER_ANIM_STATE_SAME:String = 'towerAnimStateSame';
 
   public static const TOWER_STRAT_NEAREST:String = 'towerStrategyNearest';
 
   // Sound states
   public static const TOWER_SND_STATE_FIRE:String = 'onFire';
 
   public var uid:int;
 
   public var towerName:String;
   public var index:int;
   public var level:int;
   public var range:Number;
   public var damage:Number;
   public var speed:Number;
   public var dps:Number;
   public var cost:int;
   public var maxLevel:int;
 
   public var halfWidth:Number;
   public var halfHeight:Number;
   public var state:int = TOWER_STATE_NEW;
   public var onFiring:Signal;
   public var bulletType:String;
   public var bulletSpeed:int;
   public var bulletWidth:int;
   public var bulletHeight:int;
   public var centerX:Number;
   public var centerY:Number;
   public var towerStrategy:ITowerStrategy;
   public var levelData:Object;
   public var nextDamage:Number;
 
   public var onUpgrade:Signal;
 
   // Range distance Squared
   public var rangeSquared:Number;
 
   // If this tower is currently clicked on
   public var activated:Boolean = false;
 
   protected var _animState:String;
   protected var _animData:Object;
   protected var _imageName:String;
   protected var _bulletImageName:String;
 
   protected var _rangeRing  : Image;
   protected var _badRangeRing : Image;
   protected var _rangeRingDepthIndex:int;
 
   protected var _reloadTimer:Timer;
 
   protected var _soundData:Object;
 
   // reload timer delay with game speed factored in
   protected var _reloadTimerGameSpeedDelay:Number;
 
   // Enemies in range
   protected var _enemiesObj:Object;
   protected var _enemies:Array;
 
   protected var _currentRange:int;
   protected var _lowestSqrt:Number;
 
   // Range ring graphics options
   protected var _rangeRingFillColor:int = 0X009999;
   protected var _badRangeRingFillColor:int = 0XFF0000;
   protected var _borderColor:int = 0X000000;
   protected var _borderSize:int = 1;
 
 
   protected var _mgr:TowerManager
 
   private var _interval:uint;
 
   public function Tower(towerData:Object, tMgr:TowerManager)
   {
      uid              = Config.getUID();
      _mgr             = tMgr;
      towerName        = towerData.name;
      index            = towerData.name;
      level            = 1;
      range            = towerData.levelData[0].range;
      damage           = towerData.levelData[0].damage;
      speed            = towerData.levelData[0].speed;
      cost             = towerData.levelData[0].cost;
      dps	       = towerData.levelData[0].dps;
      maxLevel	       = towerData.maxLevel;
      _imageName       = towerData.imageName;
      _bulletImageName = towerData.bulletImageName;
      halfWidth        = towerData.towerWidth >> 1;
      halfHeight       = towerData.towerHeight >> 1;
      bulletType       = towerData.bulletImageName;
      bulletSpeed      = towerData.bulletSpeed;
      bulletWidth      = towerData.bulletWidth;
      bulletHeight     = towerData.bulletHeight;
 
      _updateStatsByUpgrades();
 
      nextDamage = 0;
 
      onUpgrade = new Signal(Tower);
      onFiring = new Signal(Tower, Enemy);
 
      _enemiesObj = {};
      _enemies = [];
 
      _soundData = {};
 
      resetTowerAfterUpgrade();
 
      _parseLevelData(towerData.levelData);
      _setupAnimData();
      _createRangeRing();
      _createBadRangeRing();
 
      setTowerStrategy(new TowerStrategyNearest());
   }
 
   protected function resetTowerAfterUpgrade():void {
      rangeSquared = range * range;
 
      // set up reloadTimer
      _reloadTimerGameSpeedDelay = Config.currentGameSpeed * speed;
      _reloadTimer = new Timer(_reloadTimerGameSpeedDelay, 1);
      _reloadTimer.addEventListener(TimerEvent.TIMER_COMPLETE, reloadDoneReadyTower, false, 0, true);
   }
 
   protected function _parseLevelData(data:Array):void {
      levelData = {};
      for each(var tData:Object in data) {
         levelData[tData.level] = tData;
      }
   }
 
   public function init():void {
      _interval = flash.utils.setInterval(_init, 100);
   }
 
   protected function _init():void {
      // clear the stupid interval because of the touchevents
      flash.utils.clearInterval(_interval);
 
      hideRangeRing();
 
      // let tower listen for game speed change
      Config.onGameSpeedChange.add(onGameSpeedChange);
 
      centerX = this.x + halfWidth;
      centerY = this.y + halfHeight;
 
      // since we dont need this anymore, null it out
      _badRangeRing.visible = false;
      _badRangeRing = null;
 
      _addListeners();
      state = TOWER_STATE_READY;
   }
 
   public function setTowerStrategy(strat:ITowerStrategy):void {
      towerStrategy = strat;	
   }

Splitting Tower.as up… and continuing…

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
   public function update():void {
      if(state == TOWER_STATE_READY && _enemies.length > 0) {
         var targetEnemy:Enemy = towerStrategy.findEnemy(_enemies);
 
         if(targetEnemy) {
            _generateNextShotDamage();
            if(targetEnemy.willDamageBanish(nextDamage)) {
               _mgr.removeEnemyFromTowers(targetEnemy);
            }
            fireBullet(targetEnemy);
         }
      }
   }
 
   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 _generateNextShotDamage():void {
      nextDamage = damage;
   }
 
   protected function fireBullet(targetEnemy:Enemy):void {
      state = TOWER_STATE_FIRING;
      _playIfStateExists(TOWER_SND_STATE_FIRE);
      onFiring.dispatch(this, targetEnemy);
      reload();
   }
 
   protected function _setupAnimData():void {
      _animData = {};
 
      _animData[TOWER_ANIM_STATE_SAME] = new MovieClip(Assets.ta.getTextures(_imageName), 8);
 
      _changeAnimState(TOWER_ANIM_STATE_SAME);
   }
 
   public function enterFrameTowerPlaceCheck(canPlace:Boolean):void {
      if(canPlace) {
         _rangeRing.visible = true;
         _badRangeRing.visible = false;
      } else {
         _rangeRing.visible = false;
         _badRangeRing.visible = true;
      }
   }
 
   public function addEnemyInRange(e:Enemy, range:Number):void {
      Config.log('Tower', 'addEnemyInRange', "Tower " + uid + " Adding Enemy " + e.uid + " in range");
      _enemiesObj[e.uid] = e;
      e.onBanished.add(removeEnemyInRange);
      _enemies.push(e);
   }
 
   public function removeEnemyInRange(e:Enemy):void {
      if(_enemiesObj[e.uid]) {
         e.onBanished.remove(removeEnemyInRange);
 
         // delete object version
         delete _enemiesObj[e.uid];
 
         // remove array version
         var len:int = _enemies.length;
         for(var i:int = 0; i < len; i++) {
            if(e == _enemies[i]) {
               _enemies.splice(i, 1);
            }
         }
      }
   }
 
   public function canUpgrade():Boolean {
      var canUp:Boolean = false;
 
      if(level < maxLevel && levelData[level + 1].cost <= Config.currentGold) {
         canUp = true;
      }
 
      return canUp;
   }

Ok, one final break and let’s finish up Tower.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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
   public function upgrade():void {
      level++;
      range = levelData[level].range;
      damage = levelData[level].damage;
      speed = levelData[level].speed;
      dps = levelData[level].range;
      cost = levelData[level].cost;
 
      // apply upgrade bonuses
      _updateStatsByUpgrades();
 
      // remove (add a negative cost) gold to currentGold in Config
      Config.changeCurrentGold(-cost);
 
      // redraw the range ring
      _createRangeRing();
      resetTowerAfterUpgrade();
      onUpgrade.dispatch(this);
   }
 
   public function hasEnemyInRange(e:Enemy):Boolean {
      return (_enemiesObj[e.uid]);
   }
 
   public function showRangeRing():void {
      _rangeRing.visible = true;
   }
 
   public function hideRangeRing():void {
      _rangeRing.visible = false;
   }
 
   protected function _changeAnimState(newState:String):void {
      // make sure they are different states before removing and adding MCs
      if(_animState != newState) {
         // remove the old MovieClip from juggler
         Starling.juggler.remove(_animData[_animState]);
         // remove the old MovieClip from this Sprite
         removeChild(_animData[_animState]);
 
         _animState = newState;
 
         // add the new MovieClip to the Juggler
         Starling.juggler.add(_animData[_animState]);
         // add the new MovieClip to the Sprite
         addChild(_animData[_animState]);
      }
   }
 
   protected function _addListeners():void {
      addEventListener(TouchEvent.TOUCH, _onTowerSelected);
      state = TOWER_STATE_READY;
   }
 
   protected function _removeListeners():void {
      removeEventListener(TouchEvent.TOUCH, _onTowerSelected);
   }
 
   public function destroy():void {
      // remove the old MovieClip from juggler
      Starling.juggler.remove(_animData[_animState]);
      // remove the old MovieClip from this Sprite
      removeChild(_animData[_animState]);
   }
 
   protected function reload():void {
      state = TOWER_STATE_RELOAD;
      _reloadTimer.reset();
      // If game speed has changed, update the delay after the tower is done reloading
      _reloadTimer.delay = _reloadTimerGameSpeedDelay;
 
      _reloadTimer.start();
   }
 
   public function onGameSpeedChange():void {
      // update the game speed timer
      _reloadTimerGameSpeedDelay = int(speed / Config.currentGameSpeed);
      _reloadTimer.delay = _reloadTimerGameSpeedDelay - _reloadTimer.currentCount;
   }
 
   protected function reloadDoneReadyTower(evt:TimerEvent):void {
      state = TOWER_STATE_READY;
   }
 
   public function activate():void {
      activated = true;
      showRangeRing();
      _mgr.addListenerToDeactivateTower(this);
      _mgr.play.hudMgr.showTowerData(this);
   }
 
   public function deactivate():void {
      activated = false;
      hideRangeRing();
      _addListeners();
   }
 
   private function _updateStatsByUpgrades():void {
      range = Config.currentGameSOData.applyUpgradeToValue(range, 'towerRng');
      speed = Config.currentGameSOData.applyUpgradeToValue(speed, 'towerSpd');
      damage = Config.currentGameSOData.applyUpgradeToValue(damage, 'towerDmg');
   }
 
   protected function _onTowerSelected(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(this, TouchPhase.BEGAN);
      if (touch)
      {
         _removeListeners();
         activate();
      }
   }
 
   protected function _playIfStateExists(state:String):void {
      if(_soundData.hasOwnProperty(state)) {
         Game.soundMgr.playFx(_soundData[state], Config.sfxVolume);
      }
   }
 
   protected function _createRangeRing():void {
      if(contains(_rangeRing)) {
         _rangeRing.removeFromParent(true);
      }
 
      var _s:Shape = new Shape();
      _s.graphics.lineStyle(1);
      _s.graphics.beginFill(_rangeRingFillColor);
      _s.graphics.lineStyle(_borderSize , _borderColor);
      _s.graphics.drawCircle(0, 0, range);
 
      var matrix:Matrix = new Matrix();
      matrix.tx = _s.width >> 1;
      matrix.ty = _s.height >> 1;
 
      var _bmd:BitmapData = new BitmapData(_s.width << 1, _s.height << 2) + halfWidth;
      _rangeRing.y = -(_rangeRing.height >> 2) + halfHeight;
 
      _rangeRingDepthIndex = numChildren - 1;
      _rangeRing.touchable = false;
 
      addChildAt(_rangeRing, _rangeRingDepthIndex);
   }
 
   protected function _createBadRangeRing():void {
      var _s:Shape = new Shape();
      _s.graphics.lineStyle(1);
      _s.graphics.beginFill(_badRangeRingFillColor);
      _s.graphics.lineStyle(_borderSize, _borderColor);
      _s.graphics.drawCircle(0, 0, range);
 
      var matrix:Matrix = new Matrix();
      matrix.tx = _s.width >> 1;
      matrix.ty = _s.height >> 1;
 
      var _bmd:BitmapData = new BitmapData(_s.width << 1, _s.height << 2) + halfWidth;
      _badRangeRing.y = -(_badRangeRing.height >> 2) + halfHeight;
      _badRangeRing.touchable = false;
 
      addChildAt(_badRangeRing , numChildren - 1);
   }
}
}

So there is how the Towers are made and managed. Now let’s turn our attention to a really fun topic, Design Patterns!

Strategy Design Pattern

Design Patterns are a tool. They are a way for you, as the developer, to use battle-tested patterns of how to structure your code so that you can get the most reusability, extensibility, and other -ity words that you’ll find in the definitions of “Object Oriented Programming”. Being that Design Patterns are a tool, if you ignore them, you’re none too bright and you may be wasting your time when perhaps the wheel has already been invented. Like a tool though, you don’t use a hammer when trying to cut down a tree. That’s what chainsaws or axes are for. The sign of a solid developer is knowing when to use appropriate design patterns, and almost more importantly, when Not to use design patterns. Be well-versed in Design Patterns and know when and when not to use them.

If you overuse them…

Factory Pattern Ski Instructor

Getting down to brass tacks here, we’re specifically using the Strategy Pattern for our Tower’s algorithm to find enemies. I’ll explain what the code looked like before abstracting it out using the Strategy Pattern. The towerStrategy variable was a String, it held constants like this:

   public static const TOWER_STRAT_NEAREST:String = 'towerStrategyNearest';
   public var towerStrategy:String = TOWER_STRAT_NEAREST;

Already off to an inelegant start. Then when we got to the update() and actually went to look for an enemy…

public function update():void {
   if(state == TOWER_STATE_READY && _enemies.length > 0) {
      var closestEnemy:Enemy,
          tmpEnemy:Enemy;
          closestDist:Number = -1,
          len:int = _enemies.length;
 
      if(towerStrategy == TOWER_STRAT_NEAREST) {
         for(var i:int =0; i < len; i++) {
            tmpEnemy = _enemies[i];
            if(closestDist == -1) {
               closestDist = tmpEnemy.totalDist;
               closestEnemy = tmpEnemy;
            } else if(tmpEnemy.totalDist < closestDist) {
               closestDist = tmpEnemy.totalDist;
               closestEnemy = tmpEnemy;
            }
         }   
      } else if(towerStrategy == TOWER_STRAT_FARTHEST) {
 
      } else if(towerStrategy == TOWER_STRAT_MOST_HP) {
 
      } else if(towerStrategy == TOWER_STRAT_LEAST_HP) {
 
      }
 
      ... <Code> ...
   }
}

When we have just one single tower strategy (which you may very well have!) it makes sense not to jump through all the hoops of creating an Interface and implementing that interface in classes. It’s a bit of extra work and unnecessary. However, you can clearly see above that as soon as we add 2 or more strategies, your update() function is about to get incredibly cluttered and difficult to maintain. Enter the Strategy Pattern…

The intent of the Strategy Pattern is to be able to define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients (Towers) that use it.

ITowerStrategy.as

ITowerStrategy.as is found at com.zf.objects.tower.strategies and defines the Interface that all tower strategies should have.

1
2
3
4
5
6
7
8
9
package com.zf.objects.tower.strategies
{
   import com.zf.objects.enemy.Enemy;
 
   public interface ITowerStrategy
   {
      function findEnemy(enemies:Array):Enemy;
   }
}

Now we get to the implementation…

TowerStrategyNearest.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
package com.zf.objects.tower.strategies
{
   import com.zf.objects.enemy.Enemy;
 
public class TowerStrategyNearest implements ITowerStrategy
{
   public function TowerStrategyNearest() {
   }
 
   public function findEnemy(enemies:Array):Enemy {
      var targetEnemy:Enemy,
          len:int = enemies.length,
          tmpEnemy:Enemy,
          closestDist:Number = -1;
 
      for(var i:int =0; i <  len; i++) {
         tmpEnemy = enemies[i];
         if(closestDist == -1) {
            closestDist = tmpEnemy.totalDist;
            targetEnemy = tmpEnemy;
         } else if(tmpEnemy.totalDist < closestDist) {
            closestDist = tmpEnemy.totalDist;
            targetEnemy = tmpEnemy;
         }
      }
      return targetEnemy;
   }
}
}

So let’s step back from the code and look at this from a project perspective. Why is this useful or helpful at all? Well, 1) as previously stated, this cleans up our Tower.update() function considerably. There’s no longer a massive, nasty if/else if/else if (or switch) so there’s no way for the logic to get confusing. Each Strategy’s specific algorithm for finding a target enemy is encapsulated within it’s own class. And 2) Unit Tests.

You can easily Unit Test this class’ findEnemy() function infinitely easier than if you were trying to test Tower.update() with its nasty if/else lines. Think about how important this aspect of your code is; a Tower’s ability to select the right Enemy that the user wants. I ran into this case when I started this project: my Tower Nearest strategy was returning the Enemy that was by-pixel closer to the actual tower, than by-distance closer to escaping. While testing my demo it drove me nuts! I told the Tower to attack the Enemy that was nearest to escaping, and here it was shooting 4 enemies behind the leader because that Enemy was nearer to the Tower.

If you’ve read through comments on TD games on public sites like Kongregate or Armor Games, you’ve probably seen countless comments like “towers wont shoot at the correct enemies!”. This is a fairly common issue for devs to get wrong because we don’t realize how intensely important this is. Your player’s focus is 100% on that first Tower once they place it on the Map. That Tower is the only thing keeping them from losing the game, or getting an achievement or high score. They’re entirely invested in that Tower and since TD games are about “strategy”, your Tower properly doing exactly what is expected is paramount.

So, adding Unit Tests around these findEnemy functions will give you an easy way to test if your Tower will actually return the correct enemy to fire upon in the actual game. This saves you from having to playtest countless hours to make sure your tower is firing properly in every given scenario. Just Unit Test the code with a GOOD test, and you’ll save yourself a lot of time debugging and playtesting, and you’ll save your players from having to leave nasty comments on your game. :)

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 and I hope this was helpful!

Travis

Series Navigation<< AS3 Starling Tower Defense Tutorial – Part 5 – EnemiesAS3 Starling Tower Defense Tutorial – Part 7 – Sounds, Stats, and Saves >>
FacebookTwitterGoogle+Share

4 Comments

 Add your comment
  1. First, thanks for posting this in-depth and very clearly explained tutorial series! It’s awesome to see such helpful contributions to the AS3 community.

    In the Tower.as class I noticed that you have both an _enemies:Array as well as _enemiesObj:Object.
    What was the reasoning behind that?
    Is it so that the hasEnemiesInRange() method is more optimized than looping through the array to find the enemy?

  2. Absolutely right. I guess the argument could be made that I’m keeping track of twice the references to enemies, but when you know the specific enemy you’re looking for, you don’t have to loop through an array when you already know what you want.

  3. Thanks for clarifying the use of the object. It makes sense, but after reading your Jan 28, 2009 post on AS3 Dictionary Class vs. Array vs. Object!, I’m wondering why you decided to store the enemy reference in an Object instead of a Dictionary? Was it for code clarity? Is the performance gain of the Dictionary class over Object so minimal for a small data set that it’s better to just use an Object for clarity? Or does the new ASC 2.0 improve Object performance over the Dictionary class?

  4. You bring up several good questions! I haven’t tested Object/Dictionary access times since ASC2.0 has come out. There was an early version of the code where instead of giving bullets/towers/enemies references to other classes, I was trying to just pass around the class’ .uid property. I don’t remember why I switched to just passing around the actual objects, but you’re right. At that time I should’ve switched to a Dictionary instead of Object (using pre-ASC2.0 benchmarks). I actually just did a quick copy/paste job locally changing the _enemiesObj to _enemiesDict:Dictionary, instantiated it and changed all the _enemiesObj[e.uid] to _enemiesDict[e] and things work just fine.

Leave a Comment

Your email address will not be published.