AS3 Starling Tower Defense Tutorial – Part 10 – Bullets, Collisions, and Managers

I’ve showed off a few Manager classes so far. We walked through the EnemyManager, TowerManager, and SharedObjectManager. We’ve talked about enemies taking damage from bullets and towers somehow magically having an array of enemies that are in range. We’ve talked about UI components on the screen but never how they got there or how those individual components create the game. In this post, we’re going to spotlight the Manager classes.

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

First, though, why the “Manager” classes? “Hey I didn’t see that pattern in the GoF Design Patterns book! This is nonsense!” As I’m writing this, I did a bit of twiddling with the google-fingers for design patterns and manager. Seems there’s a bit of back and forth on if you should officially call something a “Manager” class or if … blah blah. Calling these classes some other word besides “Manager” seems like we’re just haggling over semantics. These classes Manage a specific game object (EnemyManager, TowerManager, BulletManager), or specific game functionality (CollisionManager, KeyboardManager, HudManager), it’s just what these classes do. They manage that functionality. So, that’s where we’re at. I have a bunch of Manager classes and we’re going to start by taking a look at the CollisionManager first.

CollisionManager.as

What does the CollisionManager (“CMgr” from here on out) do and why is that even a class? Let’s play out the thought experiment…

“In a world… where the CMgr doesn’t exist… never did… and never will, every class is in charge of managing it’s own collisions. Coming this summer… or maybe next year when every object is done calculating every distance to every other object every game tick… a game with no central collision detection manager will rise… and so will your cpu temp as all those calculations melt your HD into oblivion!”

Ok, so maybe not that serious. But that’s kindof the idea behind this CMgr. I want one single class that handles my math. To me, having every tower running through a list of all enemies (or enemies running through towers, or both!) on every game tick seems like an OK idea. But what if all those enemies and towers never did any calculations until they actually had valid targets? I realize math in Flash is not the biggest bottleneck. But it is smart to optimize where you can. So, what is the CMgr doing every game tick? To the 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
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.managers
{
   import com.zf.core.Config;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.tower.Tower;
   import com.zf.states.Play;
 
public class CollisionManager implements IZFManager
{
   private var _play:Play;
   private var _enemies:Array;
   private var _towers:Array;
 
   public function CollisionManager(playState:Play)
   {
      _play = playState;
      _enemies = [];
      _towers = [];
   }
 
   public function update():void {
      checkEnemiesInRange();
   }
 
   public function checkEnemiesInRange():void {
      var eLen:int = _enemies.length,
          tLen:int = _towers.length;
 
      // If there are no enemies or no towers, abort the check
      if(eLen == 0 || tLen == 0) {
         return;
      }
 
      var tDistX:Number,
          tDistY:Number,
          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]);
               }
            }
         }
      }
   }
 
   public function onEnemyAdded(e:Enemy):void {
      Config.log('CollisionManager', 'onEnemyAdded', "Adding Enemy " + e.uid);
      _enemies.push(e);
   }
 
   public function onEnemyRemoved(e:Enemy):void {
      Config.log('CollisionManager', 'onEnemyRemoved', "Removing Enemy " + e.uid);
      var len:int = _enemies.length;
      for(var i:int = 0; i < len; i++) {
         if(e == _enemies[i]) {
            _enemies.splice(i, 1);
            Config.log('CollisionManager', 'onEnemyRemoved', "Enemy " + e.uid + " Removed");
         }
      }
   }
 
   public function onTowerAdded(t:Tower):void {
      _towers.push(t);
   }
 
   public function onTowerRemoved(t:Tower):void {
      var len:int = _towers.length;
      for(var i:int = 0; i < len; i++) {
         if(t == _towers[i]) {
            _towers.splice(i, 1);
         }
      }
   }
 
   public function destroy():void {
      _enemies = null;
      _towers = null;
      _play = null;
   }
 
   public function onGamePaused():void {}
   public function onGameResumed():void {}
}
}

So now let’s look at the BulletManager.

BulletManager.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
package com.zf.managers
{
   import com.leebrimelow.starling.StarlingPool;
   import com.zf.core.Config;
   import com.zf.objects.bullet.Bullet;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.tower.Tower;
   import com.zf.states.Play;
 
   import flash.geom.Point;
   import flash.geom.Rectangle;
   import flash.utils.Dictionary;
 
   import org.osflash.signals.Signal;
 
public class BulletManager implements IZFManager
{
   public var onBulletHitEnemy:Signal;
   public var delayCount:int = 0;
   public var play:Play;
 
   private var _bullets:Array;
   private var _pool:StarlingPool;
   private var _mapBounds:Rectangle;
   private var _enemies:Object;
 
   // Reusable Point variables so we're not constantly doing new Point
   private var _p1:Point = new Point(0,0);
   private var _p2:Point = new Point(0,0);
 
   public function BulletManager(playState:Play)
   {
      play = playState;
      _bullets = [];
      _enemies = {};
      _pool = new StarlingPool(Bullet, 40);
      _mapBounds = play.map.paddedBounds;
      onBulletHitEnemy = new Signal(Bullet);
   }
 
   public function update():void {
      if(_bullets.length > 0 && delayCount % 2 == 0) {
         var b:Bullet,
             len:int = _bullets.length;
 
         for(var i:int = len - 1; i >= 0; i--) {
            b = _bullets[i];
 
            // doublecheck that this bullet still exists
            if(b != null) {
               b.update();
 
               if(b.hitEnemy) {
                  bulletHitEnemy(b);
               } else if(b.isDestroyed) {
                  destroyBullet(b);
               }
            } else {
               // if it doesnt, we need to update our len
               len = _bullets.length;
            }
         }
      }
      delayCount++;
   }
 
   public function bulletHitEnemy(b:Bullet):void {
      b.destObj.takeDamage(b.damage);
      onBulletHitEnemy.dispatch(b);
      destroyBullet(b);
   }
 
   public function destroyBullet(b:Bullet):void {
      var len:int = _bullets.length,
          tLen:int,
          enemyBulletArr:Array;
 
      for(var i:int = 0; i < len; i++)
      {
         if(b == _bullets[i])
         {
            // First remove the reference of the bullet firing at the enemy
            enemyBulletArr = _enemies[b.destObj.uid];
 
            // check if the enemy has already been removed or destroyed
            if(enemyBulletArr) {
               tLen = enemyBulletArr.length;
               for(var j:int = 0; j < tLen; j++) {
                  if(b == enemyBulletArr[j]) {
                     enemyBulletArr.splice(j, 1);
                  }
               }
            }
 
            b.destroy();
            _bullets.splice(i, 1);
            b.removeFromParent(true);
            _pool.returnSprite(b);
         }
      }
   }
 
   public function onTowerFiring(t:Tower, e:Enemy):void {
      var b:Bullet = _pool.getSprite() as Bullet;
      b.init(t, e, play);
      play.addChild(b);
      b.x = t.centerX;
      b.y = t.centerY;
 
      _bullets.push(b);
      _addListenerOnEnemy(e, b);
 
      Config.totals.bulletsFired++;
   }
 
   private function _addListenerOnEnemy(e:Enemy, b:Bullet):void {
      if(!_enemies[e.uid]) {
         _enemies[e.uid] = [];
         e.onDestroy.add(onEnemyDestroyed);
      }
 
      _enemies[e.uid].push(b);
   }
 
   public function onEnemyDestroyed(e:Enemy):void {
      e.onDestroy.remove(onEnemyDestroyed);
 
      for each(var b:Bullet in _enemies[e.uid]) {
         destroyBullet(b);
      }
 
      delete _enemies[e.uid];
   }
 
   public function get activeBullets():String {
      return _bullets.length.toString();
   }
 
   public function destroy():void {
      _bullets = null;
      _enemies = null;
      _pool = null;
      onBulletHitEnemy.removeAll();
   }
 
   public function onGamePaused():void {}
   public function onGameResumed():void {}
}
}

HudManager.as

HudManager (com.zf.managers.HudManager.as) is in charge of the UI during the Play State. It manages the options panel, various TextFields (HitPoints, Wave number, Gold, Tower Info, etc), and the Tower select Buttons. Let’s look at the 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
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
package com.zf.managers
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.objects.tower.Tower;
   import com.zf.states.Play;
   import com.zf.ui.buttons.nextWaveButton.NextWaveButton;
   import com.zf.ui.gameOptions.GameOptionsPanel;
   import com.zf.ui.text.ZFTextField;
   import com.zf.ui.towerSelect.TowerSelectIcon;
   import com.zf.ui.waveTile.WaveTileBar;
 
   import flash.geom.Point;
   import flash.text.TextFormat;
 
   import feathers.controls.ScrollText;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.textures.Texture;
 
public class HudManager extends Sprite implements IZFManager
{
   public var play : Play;
   public var endOfHP : Signal;
 
   private var _invalidComponents : Array;
   private var _playBkgd : Image;
   private var _canvas : Sprite;
   private var _p : Point = new Point(-1,-1);
   private var _tf : TextFormat;
   private var _goldTF : ZFTextField;
   private var _hpTF : ZFTextField;
   private var _waveTF : ZFTextField;
   private var _waveTileBar : WaveTileBar;
   private var _tower1 : TowerSelectIcon;
   private var _tower2 : TowerSelectIcon;
   private var _infoText : ScrollText;
   private var _activeTower : Tower;
   private var _gameOptions : GameOptionsPanel;
   private var _optsBtn : Button;
   private var _sellTowerBtn : Button;
   private var _upgradeTowerBtn : Button;
   private var _pauseBtn : Button;
   private var _optionsVisible : Boolean;
   private var _nextWaveBtns : Array;
 
   public function HudManager(playState:Play) {
      play = playState;
      _canvas = play.hudLayer;
 
      _invalidComponents = [];
      _nextWaveBtns = [];
 
      endOfHP = new Signal();
 
      _optionsVisible = false;
 
      _waveTileBar = new WaveTileBar(Config.currentMapData.enemyWaveData);
      _waveTileBar.x = 36;
      _waveTileBar.y = 567;
      // when a tile is touched, let EnemyManager handle spawning a new wave
      _waveTileBar.waveTileTouched.add(play.enemyMgr.spawnWave);
      _canvas.addChild(_waveTileBar);
 
      _playBkgd = new Image(Assets.playBkgdT);
      _playBkgd.x = 0;
      _playBkgd.y = 0;
      _playBkgd.width = 800;
      _playBkgd.height = 600;
      _playBkgd.touchable = false;
      _canvas.addChild(_playBkgd);
 
      _tower1 = new TowerSelectIcon(Assets.towerData.towers[0]);
      _tower1.x = 590;
      _tower1.y = 90;
      _canvas.addChild(_tower1);
      _tower1.onHover.add(_onHoverTowerSelectIcon);
      _tower1.onClick.add(_onClickTowerSelectIcon);
 
      _tower2 = new TowerSelectIcon(Assets.towerData.towers[1]);
      _tower2.x = 630;
      _tower2.y = 90;
      _canvas.addChild(_tower2);
      _tower2.onHover.add(_onHoverTowerSelectIcon);
      _tower2.onClick.add(_onClickTowerSelectIcon);
 
      _goldTF = new ZFTextField('goldCoin', 'CalistoMT', Config.currentGold.toString());
      _goldTF.x = 100;
      _goldTF.y = 0;
      _goldTF.componentIsInvalid.add(handleInvalidTFComponent);
      _goldTF.touchable = false;
      _canvas.addChild(_goldTF);
      Config.currentGoldChanged.add(updateGoldTF);
 
      _hpTF = new ZFTextField('heartIcon', 'CalistoMT', Config.currentHP.toString());
      _hpTF.x = 200;
      _hpTF.y = 0;
      _hpTF.componentIsInvalid.add(handleInvalidTFComponent);
      _hpTF.touchable = false;
      _canvas.addChild(_hpTF);
      Config.currentHPChanged.add(updateHPTF);
 
      var waveTxt:String = Config.currentWave.toString() + ' / ' + Config.maxWave.toString();
      _waveTF = new ZFTextField('waveIcon', 'CalistoMT', waveTxt);
      _waveTF.x = 300;
      _waveTF.y = 0;
      _waveTF.componentIsInvalid.add(handleInvalidTFComponent);
      _waveTF.touchable = false;
      _canvas.addChild(_waveTF);
      Config.currentWaveChanged.add(updateWaveTF);
 
      _infoText = new ScrollText();
      _infoText.x = 588;
      _infoText.y = 250;
      _infoText.isHTML = true;
      _infoText.textFormat = new TextFormat('CalistoMT', 16, 0xFFFFFF);
      _infoText.text = "";
      _canvas.addChild(_infoText);
 
      _upgradeTowerBtn = new Button(Assets.ta.getTexture('upgradeTowerBtn'));
      _upgradeTowerBtn.x = 580;
      _upgradeTowerBtn.y = 520;
      _canvas.addChild(_upgradeTowerBtn);
 
      _sellTowerBtn = new Button(Assets.ta.getTexture('sellTowerBtn'));
      _sellTowerBtn.x = 660;
      _sellTowerBtn.y = 520;
      _canvas.addChild(_sellTowerBtn);
 
      _hideTowerButtons();
 
      // Set up options stuff but do not add to stage yet
      _gameOptions = new GameOptionsPanel(new Point(150,-350), new Point(150,150));
      _gameOptions.onQuitGame.add(play.onQuitGameFromOptions);
 
      _optsBtn = new Button(Assets.optsBtnT);
      _optsBtn.x = 667;
      _optsBtn.y = 0;
      _optsBtn.addEventListener(Event.TRIGGERED, _onOptionsClicked);
      _canvas.addChild(_optsBtn);
 
      _resetPauseBtn(Assets.playBtnT);
      play.enemyMgr.onSpawnWave.add(onSpawnWave);
   }

And more from com.zf.managers.HudManager.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
   public function onSpawnWave(waveID:String):void {
      // remove after the first time since we dont show the nextWaveButtons
      // except for the first round
      play.enemyMgr.onSpawnWave.remove(onSpawnWave);
 
      // remove the next wave buttons but dont trigger a pause event
      removeNextWaveButtons(false);
   }
 
   public function update():void {
      updateUI();
      // always update the _waveTileBar, it will handle it's own optimization
      _waveTileBar.update();
   }
 
   public function updateUI():void {
      if(_invalidComponents.length > 0) {
         for(var i:int = 0; i < _invalidComponents.length; i++) {
            _invalidComponents[i].update();
            _invalidComponents.splice(i, 1);
         }
      }
   }
 
   public function updateGoldTF(amt:int):void {
      _goldTF.text = amt.toString();
   }
 
   public function updateHPTF(amt:int):void {
      _hpTF.text = amt.toString();
 
      if(amt <= 0) {
         Config.log('HudManager', 'updateHPTF', "HudManager at Zero HP");
         _removeListeners();
         Config.log('HudManager', 'updateHPTF', "HudManager dispatching endOfHP");
         endOfHP.dispatch();
      }
   }
 
   public function updateWaveTF(amt:int):void {
      _waveTF.text = amt.toString() + ' / ' + Config.maxWave.toString();
   }
 
   public function handleInvalidTFComponent(tf:ZFTextField):void {
      _invalidComponents.push(tf);
   }
 
   public function showNextWaveButtons():void {
      var p:Array = play.wpMgr.groupStartPositions,
          len:int = p.length;
      for(var i:int = 0; i < len; i++) {
         var nextWaveBtn:NextWaveButton = new NextWaveButton(Assets.ta.getTexture('nextWaveBtn_' + p[i].dir));
         nextWaveBtn.x = p[i].pos.x;
         nextWaveBtn.y = p[i].pos.y;
         nextWaveBtn.onClicked.add(removeNextWaveButtons);
         _canvas.addChild(nextWaveBtn);
         _nextWaveBtns.push(nextWaveBtn);
      }
   }
 
   public function removeNextWaveButtons(triggerPause:Boolean = true):void {
      var len:int = _nextWaveBtns.length;
      for(var i:int = 0; i < len; i++) {
         // tell all buttons to fade out and destroy
         _nextWaveBtns[i].fadeOut();
      }
 
      if(triggerPause) {
         play.onPauseEvent();
      }
   }
 
   private function _onTowerUpgradeClicked(evt:Event):void {
      if(_activeTower.canUpgrade()) {
         _activeTower.upgrade();
         showTowerData(_activeTower);
         if(_activeTower.level == _activeTower.maxLevel) {
            _hideTowerButtons();
            _showTowerButtons(false);
         }
      } else {
         trace("Cant create tower" );
      }
   }
 
   private function _hideTowerButtons():void {
      _upgradeTowerBtn.visible = false;
      _sellTowerBtn.visible = false;
      _upgradeTowerBtn.removeEventListener(Event.TRIGGERED, _onTowerUpgradeClicked);
      _sellTowerBtn.removeEventListener(Event.TRIGGERED, _onSellTowerClicked);
   }
 
   private function _showTowerButtons(showUpgrade:Boolean = true):void {
      _sellTowerBtn.visible = true;
      _sellTowerBtn.addEventListener(Event.TRIGGERED, _onSellTowerClicked);
 
      if(showUpgrade) {
         _upgradeTowerBtn.visible = true;
         _upgradeTowerBtn.addEventListener(Event.TRIGGERED, _onTowerUpgradeClicked);
      }
   }
 
   private function _onSellTowerClicked(evt:Event):void {
      play.towerMgr.sellTower(_activeTower);
   }
 
   private function _onPauseClicked(evt:Event):void {
      play.onPauseEvent();
   }
 
   public function onGamePaused():void {
      _resetPauseBtn(Assets.playBtnT);
      if(_optionsVisible) {
         _removeListeners();
      }
   }
 
   public function onGameResumed():void {
      _resetPauseBtn(Assets.pauseBtnT);
      if(!_optionsVisible) {
         _addListeners();
      }
   }
 
   private function _resetPauseBtn(t:Texture):void {
      if(_canvas.contains(_pauseBtn)) {
         _pauseBtn.removeFromParent(true);
         _pauseBtn.removeEventListener(Event.TRIGGERED, _onPauseClicked);
      }
      _pauseBtn = new Button(t);
      _pauseBtn.x = 520;
      _pauseBtn.y = 0;
      _pauseBtn.addEventListener(Event.TRIGGERED, _onPauseClicked);
      _canvas.addChild(_pauseBtn);
   }

Continuing with com.zf.managers.HudManager.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
   private function _onOptionsClicked(evt:Event):void {
      _optionsVisible = true;
      // clear any infotext, it will display OVER the options box
      showInfo('');
 
      // if gameOptions isn't already added to play.topLayer
      if(!play.topLayer.contains(_gameOptions)) {
         play.topLayer.addChild(_gameOptions);
      }
 
      play.onPauseEvent(true);
      _gameOptions.onDeactivated.add(_onOptionsDeactivated);
      _gameOptions.activate();
      _pauseBtn.removeEventListener(Event.TRIGGERED, _onPauseClicked);
   }
 
   private function _onOptionsDeactivated():void {
      _optionsVisible = false;
 
      _gameOptions.onDeactivated.remove(_onOptionsDeactivated);
      play.onPauseEvent(true);
      _pauseBtn.addEventListener(Event.TRIGGERED, _onPauseClicked);
   }
 
   private function _onClickTowerSelectIcon(towerData:Object, p:Point):void {
      if(!_optionsVisible && play.canCreateTower(towerData.id)) {
         play.createTower(towerData.id, p);
      }
   }
 
   private function _onHoverTowerSelectIcon(towerData:Object):void {
      if(!_optionsVisible) {
         generateTowerInfoHTML(towerData);
      }
   }
 
   public function showTowerData(t:Tower):void {
      _activeTower = t;
      var showUpgrade:Boolean = false;
      if(_activeTower.level < _activeTower.maxLevel) {
         showUpgrade = true;
      }
      _showTowerButtons(showUpgrade);
      generateTowerInfoHTML(t);
   }
 
   public function hideTowerData(t:Tower):void {
      if(_activeTower && _activeTower == t) {
         _activeTower = null;
         _hideTowerButtons();
         showInfo('');
      }
   }
 
   public function generateTowerInfoHTML(towerData:Object):void {
      var fontOpenH1Tag:String = '',
          fontOpenLabelTag:String = '',
          fontCloseTag:String = '',
          tName:String = '',
          isTower:Boolean = towerData is Tower;
 
      if(isTower) {
         tName = towerData.towerName;
      } else {
         tName = towerData.name;
      }
 
      var txt:String = fontOpenH1Tag + tName + fontCloseTag + '<br><br>'
                     + fontOpenLabelTag + 'Level: ' + towerData.level + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'Cost: ' + towerData.levelData[towerData.level].cost + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'Damage: ' + towerData.levelData[towerData.level].damage + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'DPS: ' + towerData.levelData[towerData.level].dps + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'Speed: ' + towerData.levelData[towerData.level].speed + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'Range: ' + towerData.levelData[towerData.level].range + fontCloseTag + '<br>';
 
      if(isTower && towerData.level < towerData.maxLevel) {
         txt += fontOpenLabelTag + 'Upgrade Cost: ' + towerData.levelData[towerData.level + 1].cost + fontCloseTag + '<br>';
      }
 
      showInfo(txt);
   }
 
   public function showInfo(msg:String):void {
      _infoText.text = msg;
   }
 
   private function _removeListeners():void {
      Config.currentWaveChanged.remove(updateWaveTF);
      Config.currentHPChanged.remove(updateHPTF);
      Config.currentGoldChanged.remove(updateGoldTF);
      _tower1.onHover.remove(_onHoverTowerSelectIcon);
      _tower1.onClick.remove(_onClickTowerSelectIcon);
      _tower2.onHover.remove(_onHoverTowerSelectIcon);
      _tower2.onClick.remove(_onClickTowerSelectIcon);
 
      _upgradeTowerBtn.removeEventListener(Event.TRIGGERED, _onTowerUpgradeClicked);
      _sellTowerBtn.removeEventListener(Event.TRIGGERED, _onSellTowerClicked);
      _optsBtn.removeEventListener(Event.TRIGGERED, _onOptionsClicked);
      _pauseBtn.removeEventListener(Event.TRIGGERED, _onPauseClicked);
   }
 
   private function _addListeners():void {
      Config.currentWaveChanged.add(updateWaveTF);
      Config.currentHPChanged.add(updateHPTF);
      Config.currentGoldChanged.add(updateGoldTF);
      _tower1.onHover.add(_onHoverTowerSelectIcon);
      _tower1.onClick.add(_onClickTowerSelectIcon);
      _tower2.onHover.add(_onHoverTowerSelectIcon);
      _tower2.onClick.add(_onClickTowerSelectIcon);
 
      _upgradeTowerBtn.addEventListener(Event.TRIGGERED, _onTowerUpgradeClicked);
      _sellTowerBtn.addEventListener(Event.TRIGGERED, _onSellTowerClicked);
      _optsBtn.addEventListener(Event.TRIGGERED, _onOptionsClicked);
      _pauseBtn.addEventListener(Event.TRIGGERED, _onPauseClicked);
   }
 
   public function destroy():void {
      Config.log('HudManager', 'destroy', "HudManager Destroying");
 
      _activeTower = null;
 
      _tower1.destroy();
      _tower1.removeFromParent(true);
      _tower1 = null;
 
      _tower2.destroy();
      _tower2.removeFromParent(true);
      _tower2 = null;
 
      Config.currentGoldChanged.removeAll();
      Config.currentHPChanged.removeAll();
      Config.currentWaveChanged.removeAll();
 
      _goldTF.destroy();
      _goldTF.removeFromParent(true);
      _goldTF = null;
      _hpTF.destroy();
      _hpTF.removeFromParent(true);
      _hpTF = null;
 
      _waveTF.destroy();
      _waveTF.removeFromParent(true);
      _waveTF = null;
      _upgradeTowerBtn.removeFromParent(true);
      _upgradeTowerBtn = null;
 
      _sellTowerBtn.removeFromParent(true);
      _sellTowerBtn = null;
 
      _optsBtn.removeFromParent(true);
      _sellTowerBtn = null;
 
      Config.log('HudManager', 'destroy', "HudManager Destroyed");
   }
}
}

KeyboardManager.as

The KeyboardManager handles my keyboard events. When a player presses the Spacebar or +/- or a few other combinations, this handles those events. Add as many things in here as you want!

Once again, I am using someone elses’ code in the com.zf.utils.KeyCode.as. It is simply an AS file that declares all the keycode constants so you can easily use them here in the KeyboardManager. Once again, I cannot remember where I actually got this code from. However, you can find a very similar file at Chris-N-Chris.

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
package com.zf.managers
{
   import com.zf.core.Config;
   import com.zf.states.Play;
   import com.zf.utils.KeyCode;
 
   import flash.display.Stage;
   import flash.events.KeyboardEvent;
 
   import org.osflash.signals.Signal;
public class KeyboardManager
{
   public var onPause:Signal;
 
   private var _stage:Stage;
   private var _play:Play;
   private var _signals:Array = [];
 
   public function KeyboardManager(play:Play, stage:Stage) {
      _play = play;
      _stage = stage;
      _stage.addEventListener(KeyboardEvent.KEY_DOWN, _onKeyDown);
 
      onPause = new Signal();
      _signals.push(onPause);
   }
 
   protected function _onKeyDown(evt:KeyboardEvent):void {
      // Handle events the game uses
      switch(evt.keyCode) {
         case KeyCode.SPACE:
            onPause.dispatch();
            break;
 
         case KeyCode.PLUS:
         case KeyCode.ADD:
            Config.changeGameSpeed(Config.GAME_SPEED_UP);
            break;
 
         case KeyCode.MINUS:
         case KeyCode.SUBTRACT:
            Config.changeGameSpeed(Config.GAME_SPEED_DOWN);
            break;
      }
   }
 
   public function destroy():void {
      _stage.removeEventListener(KeyboardEvent.KEY_DOWN, _onKeyDown);
      for(var i:int = 0; i < _signals.length; i++) {
         _signals[i].removeAll();
      }
   }
}
}

WaypointManager.as

We finally wrap up our Managers post with the WaypointManager. I probably should’ve brought this guy up way back in the Map Tiling post because this is predominately where it’s used. This file is already pretty heavily commented. I’m trying a slightly different format here… Please drop me an email (travis [at] zombieflambe [dot] com) or post a comment and let me know which style you prefer… either my line-by-line analysis below the code, or the code files heavily commented. Thanks!

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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package com.zf.managers
{
   import com.zf.objects.map.TileData;
   import com.zf.objects.map.WaypointData;
   import com.zf.objects.enemy.Enemy;
 
   import flash.geom.Point;
   import com.zf.states.Play;
 
public class WaypointManager
{
   public var groupStartPositions : Array;
 
   private var _waypoints : Object;
   private var _waypointDist : Object;
   private var _tempEndpoints : Object;
   private var _play : Play;
   private var _halfTileWidth : Number;
   private var _halfTileHeight : Number;
 
   public function WaypointManager(p:Play) {
      _play = p;
      _waypoints = {};
      _waypointDist = {};
      _tempEndpoints = {};
      groupStartPositions = [];
   }
 
   /**
    * Gets called by Map to add a Waypoint when it comes across the right 
    * criteria for a specific piece of Tile data
    **/
   public function addWaypoint(tileData:TileData, pos:Point, halfTileWidth:Number, halfTileHeight:Number):void {
      // save values so we can use later
      _halfTileWidth = halfTileWidth;
      _halfTileHeight = halfTileHeight;
 
      // create a new WaypointData Object and pass in the tileData
      var wpData:WaypointData = new WaypointData(tileData);
 
      // set the waypoint point
      wpData.setPoint(pos, halfTileWidth, halfTileHeight);
 
      // loop through our waypoint groups and create a new array if we dont have the current group
      // in our _waypoints Object
      for each(var group:String in wpData.groups) {
         if(!_waypoints[group]) {
            _waypoints[group] = [];
         }
      }
 
      // If this is a spawnpoint, add another point offscreen for the "real" spawnpoint
      if(wpData.isSpawnpoint) {
         // set index to 0, this will be the first point
         var tmpObj:Object = {
                              "src": wpData.srcImageName,
                              "isWalkable": true,
                              "wp": true,
                              "wpGroup": wpData.wpGroup,
                              "wpIndex": 0,
                              "sp": true,
                              "spDirection": wpData.spDirection
         };
         // create a new 'real' spawnpoint with data
         var spWaypoint:WaypointData = new WaypointData(new TileData(tmpObj));
 
         // add the newly created WP
         _waypoints[group].push(spWaypoint);
 
         // unset the former spawnpoint wp
         wpData.isSpawnpoint = false;
         wpData.spDirection = '';
      }
 
      // handle if this is an endpoint, saving the data for later
      if(wpData.isEndpoint) {
         for each(var groupName:String in wpData.groups) {
            if(!_tempEndpoints[groupName]) {
               _tempEndpoints[groupName] = [];
            }
 
            // Keep a copy to the last wp "so far", when we sort, 
            // we'll take this "last" wp and add another one with the
            // right values to be the real off-screen endpoint
            _tempEndpoints[groupName].push(wpData);
         }
      }
 
      _waypoints[group].push(wpData);
   }
 
   /**
    * Gets called by Map to add a Group Start Position when it comes across
    * the right criteria for a specific piece of Tile data
    **/
   public function addGroupStartPosition(dir:String, pos:Point, halfTileWidth:Number, halfTileHeight:Number):void {
      groupStartPositions.push({dir: dir, pos: new Point(pos.x + halfTileWidth, pos.y + halfTileHeight)});
   }
 
   /**
    * Handles the temporary endpoints array and creates a new point that is
    * the actual endpoint for the Enemy, then sorts the array so it appears
    * in order
    **/
   public function handleEndpointAndSort():void {
      var groupName:String;
 
      // Before sorting, handle the _tempEndpoints by adding a new WP that's the real
      // off-screen final endpoint wp
      for(groupName in _tempEndpoints) {
         // should only be ONE endpoint per group, so grab the 0th element
         var tempWP:WaypointData = _tempEndpoints[groupName][0];
 
         // Add one to the index so this point comes after the other endpoint
         var lastIndex:int = tempWP.wpIndex + 1;
 
         var tmpObj:Object = {
                              "src": tempWP.srcImageName,
                              "isWalkable": true,
                              "wp": true,
                              "wpGroup": tempWP.wpGroup,
                              "wpIndex": lastIndex,
                              "ep": true,
                              "epDirection": tempWP.epDirection
         };
 
         // create a new 'real' spawnpoint with XML
         var newWP:WaypointData = new WaypointData(new TileData(tmpObj));
 
         // add the newly created WP
         _waypoints[groupName].push(newWP);
 
         // set the former endpoint wp to be a regular waypoint now that we've added the new endpoint
         _waypoints[groupName][tempWP.wpIndex].isEndpoint = false;
         _waypoints[groupName][tempWP.wpIndex].epDirection = '';
 
         // empty vector of endpoints for the group
         _tempEndpoints[groupName] = null;
         delete _tempEndpoints[groupName];
      }
 
      // Loop through groups and sort them
      for(groupName in _waypoints) {
         // sort each group
         _waypoints[groupName].sort(_waypointSort);
      }
 
      _calcRouteDistance();
   }
 
   /**
    * Returns an Array of endpoints based on a groupName
    **/
   public function getWaypointsByGroup(groupName:String):Array {
      return _waypoints[groupName];
   }
 
   /**
    * Returns the distance (Number) of a particular waypoint 
    * group's route when given the groupName
    **/
   public function getRouteDistance(groupName:String):Number {
      return _waypointDist[groupName];
   }
 
   /**
    * This is the sort function to use so the Array knows how to sort
    * the array. We're sorting on wpIndex
    **/
   private function _waypointSort(a:WaypointData, b:WaypointData):int {
      if(a.wpIndex < b.wpIndex) {
         return -1;
      } else if(a.wpIndex > b.wpIndex) {
         return 1;
      } else {
         return 0;
      }
   }
 
   /**
    * Actually traces the route of a waypoint group and calculates 
    * the total distance in Pixels for the waypoint group
    **/
   private function _calcRouteDistance():void {
      // handle the xy values of spawn and endpoints before getting distances
      _handleSpawnEndpointXYValues();
 
      for(var groupName:String in _waypoints) {
         // Get distance of the route
         var dist:Number = 0,
             prevWP:WaypointData,
             newX:Number,
             newY:Number,
             dx:Number,
             dy:Number,
             addedDist:Number = 0;
 
         for each(var wp:WaypointData in _waypoints[groupName]) {
            // handle the first wp
            if(!prevWP) {
               prevWP = wp;
               // skip the rest of the loop this round and get the next wp
               continue;
            }
 
            // figure out which direction we're heading next, get dx,dy
            dx = wp.point.x - prevWP.point.x;
            dy = wp.point.y - prevWP.point.y;
 
            // Set the previousWP's next direction
            // So "from prevWP, which way should I face to the next WP"
            if(dx > 0 && dy == 0) {
               prevWP.nextDir = Enemy.ENEMY_DIR_RIGHT;
            } else if(dx < 0 && dy == 0) {
               prevWP.nextDir = Enemy.ENEMY_DIR_LEFT;
            } else if(dx == 0 && dy > 0) {
               prevWP.nextDir = Enemy.ENEMY_DIR_DOWN;
            } else if(dx == 0 && dy < 0) {
               prevWP.nextDir = Enemy.ENEMY_DIR_UP;
            }
 
            // find the distance
            // regular distance formula: Math.sqrt(dx * dx + dy * dy);
            // since we're only moving up, down, left, or right, never any diagonals
            // we can simplify because dx OR dy will always be 0 making squaring, then squarerooting useless
            // but we do want the Absolute value so distance is a positive number
            addedDist = Math.abs(dx) + Math.abs(dy);
 
            // sum the distance for later group totals
            dist += addedDist;
 
            // When unit begins heading towards this wp, now it knows the distance
            prevWP.distToNext = addedDist;
 
            // set current waypoint to previous
            prevWP = wp;
         }
 
         // add total distance to the group route
         _waypointDist[groupName] = dist;
      }
   }
 
   /**
    * Loops through all groups in _waypoints and handles the 
    * x & y values of the spawnpoints and endpoints for the group
    **/
   public function _handleSpawnEndpointXYValues():void {
      // quadrupling halfTileWidth and halfTileHeight so enemies 
      // start 2 full tile widths/heights off screen
      var tileWidth:int = _halfTileWidth << 2;
          tileHeight:int = _halfTileHeight << 2;
 
      for(var groupName:String in _waypoints) {
         // get the length of this group
         var groupLen:int = _waypoints[groupName].length;
 
         // temp spawnpoint
         var sp:WaypointData = _waypoints[groupName][0];
         // temp first wp
         var fwp:WaypointData = _waypoints[groupName][1];
 
         // temp endpoint
         var ep:WaypointData = _waypoints[groupName][groupLen - 1];
         // temp next-to-last waypoint
         var ntlwp:WaypointData = _waypoints[groupName][groupLen - 2];
 
         // use fwp.regPoint to get the top-left corner coordinate for the tile
         var newX:Number = fwp.regPoint.x,
             newY:Number = fwp.regPoint.y,
             halfWidth:int = 0,
             halfHeight:int = 0;
 
         switch(sp.spDirection) {
            case 'left':
               newX -= tileWidth;
               halfHeight = _halfTileHeight;
               sp.nextDir = Enemy.ENEMY_DIR_RIGHT;
               break;
 
            case 'up':
               newY -= tileHeight;
               halfWidth = _halfTileWidth;
               sp.nextDir = Enemy.ENEMY_DIR_DOWN;
               break;
 
            case 'right':
               newX += tileWidth;
               halfHeight = _halfTileHeight;
               sp.nextDir = Enemy.ENEMY_DIR_LEFT;
               break;
 
            case 'down':
               newY += tileHeight;
               halfWidth = _halfTileWidth;
               sp.nextDir = Enemy.ENEMY_DIR_UP;
               break;
         }
 
         // set the new point for the spawnpoint
         sp.setPoint(new Point(newX, newY), halfWidth, halfHeight);
 
         // reuse vars
         newX = ntlwp.regPoint.x;
         newY = ntlwp.regPoint.y;
         halfWidth = halfHeight = 0;
 
         switch(ep.epDirection) {
            case 'left':
               newX -= tileWidth;
               halfHeight = _halfTileHeight;
               break;
 
            case 'up':
               newY -= tileHeight;
               halfWidth = _halfTileWidth;
               break;
 
            case 'right':
               newX += tileWidth;
               halfHeight = _halfTileHeight;
               break;
 
            case 'down':
               newY += tileHeight;
               halfWidth = _halfTileWidth;
               break;
         }
 
         // set the new point for the endpoint
         ep.setPoint(new Point(newX, newY), halfWidth, halfHeight);
      }
   }
 
   public function destroy():void {
      _waypoints = null;
   }
}
}

Ok so it was tough to let the comments do all the talking… I had to comment on those few lines :D

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 all about the Managers classes!

-Travis

Series Navigation<< AS3 Starling Tower Defense Tutorial – Part 9 – UI Game ComponentsAS3 Starling Tower Defense Tutorial – Part 11 – Conclusion >>

Leave a Comment

Your email address will not be published.