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.

[toc]

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!!

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

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...

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

   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

   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 + '

' + fontOpenLabelTag + 'Level: ' + towerData.level + fontCloseTag + '
' + fontOpenLabelTag + 'Cost: ' + towerData.levelData[towerData.level].cost + fontCloseTag + '
' + fontOpenLabelTag + 'Damage: ' + towerData.levelData[towerData.level].damage + fontCloseTag + '
' + fontOpenLabelTag + 'DPS: ' + towerData.levelData[towerData.level].dps + fontCloseTag + '
' + fontOpenLabelTag + 'Speed: ' + towerData.levelData[towerData.level].speed + fontCloseTag + '
' + fontOpenLabelTag + 'Range: ' + towerData.levelData[towerData.level].range + fontCloseTag + '
'; if(isTower && towerData.level < towerData.maxLevel) { txt += fontOpenLabelTag + 'Upgrade Cost: ' + towerData.levelData[towerData.level + 1].cost + fontCloseTag + '
'; } 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.

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!

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 😀

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

Categories:

0 Comments

Leave a Reply

Avatar placeholder