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 {}
}
}
- Line 16-18 - keep a reference to the Play State, init my enemies and towers arrays
- Line 22 - the Play State is going to call update() on all its Managers on every game tick. For CMgr, each tick I want to call checkEnemiesInRange().
- Line 26-27 - get the length of our _enemies and our _towers arrays. When a new Enemy or Tower is added to the stage, CMgr is notified and adds that Enemy or Tower to the appropriate array.
- Line 31 - if there are no Enemies OR there are no Towers, just return right away and don't bother with the rest of this function. If there were Towers on stage but no enemies, there's no reason to loop through the _towers, we'd find no enemies in range because they don't exist yet. Same with enemies.
- Line 34-36 - declaring variables outside of nested loops
- Line 38 - loop through all of the enemies
- Line 40 - loop through all the towers
- Line 42-43 - we're getting a flat x distance and y distance here. The x and y of the current Enemy we're iterating over minus the x and y of the current Tower we're iterating over.
- Line 46 - the "typical" (or "actual") distance formula you'll see usually looks like Math.sqrt(distX * distX + distY * distY). I'm skipping that squareroot. This is a minor optimization. You won't see huge performance increases from this. You probably won't even notice anything. But we're running at 60fps and this happens inside a nested loop 60 times a second, so why not try.
- Line 48 - if you'll recall from Towers, the Tower class has a rangeSquared property where when the Tower is created I've already taken the range and squared it. So, if my dist from line 49 is less than the rangeSquared of the Tower, we've got an Enemy in range!
- Line 51-54 - the Tower class has a hasEnemyInRange() function that takes an Enemy instance and checks to see if the Tower is currently tracking that Enemy. So if the Tower does not have this specific Enemy in it's list of enemies, then add this enemy to the Tower's list of enemies.
- Line 60-63 - if the Enemy's distance was outside the range of the Tower, we need to double check if the Tower has the Enemy already in its sights. If it is tracking the Enemy, then we need to remove this Enemy from its sights because it is no longer in range.
- Line 25-67 - So that's it! That's the biggest job of the CMgr. It loops through every Enemy and every Tower and passes references to Towers of the Enemies that are currently in range of the Tower. Now when the Tower updates, it can handle its own Enemy-finding strategy. Now every Tower doesn't have to loop uselessly over enemies that are out of range.
- Line 69-72 - when an Enemy gets added, push the Enemy onto our _enemies array
- Line 74-83 - when an Enemy gets removed, loop through all enemies and remove the Enemy from the array. Note we are not destroying the Enemy or anything here, that's for the EnemyManager to do.
- Line 85-87 - when a Tower gets added, push the Tower onto our _towers array
- Line 89-96 - when a Tower gets removed, loop through all towers and remove the Tower from the array.
- Line 98-102 - just null out the variables to get rid of references to the Objects. CMgr has nothing to remove from the stage or anything... that's not its job.
- Line 104-105 - CMgr has nothing to do on pause or resume but it has to have these functions
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 {}
}
}
- Line 33-35 - grab a reference to the Play State and init _bullets array and _enemies object
- Line 36 - this is Lee Brimlow's StarlingPool. You can find the code here at GitHub - StarlingPool.as, and make sure you check out his tutorial on Using Object Pools. Basically we're creating a new StarlingPool and we're telling the pool to create 40 Objects of type Bullet. This saves time and processing power as we can create these well before the game starts rendering so we don't have to create Objects while the game is being played.
- Line 37 - get the padded map bounds so we'll know when the Bullet has traveled off screen.
- Line 38 - create a new Signal for when a Bullet hits an Enemy. Other classes may want to be aware of when that happens.
- Line 42 - if there are any _bullets in play, and delay count is an even number, then we update. Basically I'm doing % 2 because I want the bullets to all update every other tick. Updating every other tick saves us 50% of the time to render bullets, they're updating 30times a second instead of 60 and they still look good.
- Line 46 - get the Bullet for this iteration
- Line 49 - make sure the Bullet still exists
- Line 50 - call Bullet's update() function which we'll look at next
- Line 53 - after the Bullet has updated, check to see if the Bullet hitEnemy is true, if so, call bulletHitEnemy() and pass in the Bullet
- Line 55 - if the Bullet hasn't hit an Enemy, check to see if the Bullet isDestroyed is true and destroy the bullet.
- Line 59 - if the Bullet doesn't exist, update the len property so we don't loop too far
- Line 63 - increment our delayCount
- Line 68 - if a Bullet hit an Enemy, then tell b.destObj (the Enemy the Bullet struck) to takeDamage() and pass in the amount of damage the Bullet does
- Line 69 - dispatch the onBulletHitEnemy Signal to let other classes know a Bullet hit an Enemy
- Line 70 - destroy the Bullet
- Line 78 - loop through all the Bullets
- Line 80 - find the Bullet from the array that we're looking for
- Line 85 - as we'll see in a few lines, when a Bullet is fired, we create a new Bullet and we also create a new Array for that specific Enemy so we always know how many Bullets are heading towards a particular Enemy. Here we need to get that array of bullets and remove the reference of the Bullet firing at the Enemy
- Line 90 - loop through the Bullets in the Enemy's array and find the correct Bullet and remove the reference to it
- Line 95 - call destroy() on the Bullet
- Line 96 - remove the Bullet from the _bullets array
- Line 97 - remove the Bullet from the stage
- Line 98 - return the Bullet back to the _pool to be reused later
- Line 104 - when a Tower fires a Bullet at an Enemy, it dispatches a Signal that gets handled here. A Tower has fired a Bullet, so now we get a Bullet from our pool
- Line 105 - call init() on the Bullet, passing in a reference to the Tower, the Enemy, and the Play State
- Line 106 - add the Bullet directly to the Play State
- Line 107-108 - update the Bullet's x and y to the center of the Tower that fired it.
- Line 110 - add the Bullet to the _bullets array
- Line 111 - call the _addListenerOnEnemy() function and pass in the reference to the Enemy and the Bullet
- Line 113 - since we're keeping track of how many bullets we fired, update the Config.totals.bulletsFired var
- Line 117-120 - check to see if this Enemy already has its own array, if not, add a new array to the _enemies object by the enemy's uid and add a listener for the Enemy's onDestroy Signal
- Line 122 - push the Bullet onto the enemy's array
- Line 126 - when an Enemy is destroyed, remove the Signal listener
- Line 129 - go through all the bullets in the Enemy's bullet array and destroy the bullets. If bullets kill the Enemy, this shouldn't get run because there should only be the exact amount of Bullets fired that are required to kill an Enemy. This is here so that if an Enemy escapes the map and Bullets were on their way, this destroys those bullets.
- Line 132 - delete the array by the Enemy's uid
- Line 136 - this returns a string of how many bullets are active on the stage. I used this for debugging purposes and left it in because maybe you want to have a TextField on stage showing how many bullets are active at a time or something?
- Line 139 - destroy this BulletManager
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);
}
- Line 53-54 - grab a reference to the Play State and set our HUD canvas to the be play.hudLayer. This hudLayer should be the second-highest layer off the Stage so that our HUD sits on top of everything else (except the Options panel's layer)
- Line 56-57 - init invalidComponents and nextWaveBtns arrays. _invalidComponents is an array that will hold any UI components that dispatch a Signal to the HudManager letting it know that they need to be redrawn. And the next wave buttons we saw in the UI posts last week
- Line 59 - create a new Signal to dispatch when we're out of Hit Points
- Line 61 - _optionsVisible is set to false because at this point, no options should be visible. This gets set to true later when the player clicks the Options button
- Line 62 (no code on this line) - the HudManager itself never gets added to stage, so that's why you don't see an event listener for ADDED_TO_STAGE. Instead, we use the hudLayer as our canvas to add UI elements to as it's already been added to the stage before HudManager gets called. So, we continue on with creating and adding UI elements.
- Line 63-65 - create the WaveTileBar component that we saw in a previous tutorial. Set it's x/y.
- Line 68-69 - Now here's a bit of sorcery. The WaveTileBar dispatches a waveTileTouched Signal when a player clicks one of the WaveTiles, right? The HudManager doesn't care if a player clicks a WaveTile to spawn a new wave, but the EnemyManager does. So I'm setting EnemyManager's spawnWave() function as the callback for when the waveTileTouched Signal gets dispatched. Then add the WaveTileBar to the canvas
- Line 70-76 - create the _playBkgd Image, set x, y, width, height, set touchable to false, and add it to the canvas. This is the blue background that goes all around the whole 800x600 screen
- Line 78-90 - here we're creating our two TowerSelectIcons. As we went over the TowerSelectIcons in detail, I'm going to skip the explanation, we're just setting their x,y values, adding them to the _canvas and adding some callback functions for when the TowerSelectIcons dispatch Signals.
- Line 92-97 - create our first ZFTextField which we went over already. Set it's x,y, add a callback to handle when this ZFTextField becomes invalid (its number changes, but it hasn't redrawn so the player doesn't know the number hasn't changed yet), set touchable to false, and add it to the canvas.
- Line 98 - when other classes tell Config to change it's currentGold var (creating a new Tower, upgrading a tower, an Enemy gets killed giving the player more gold, etc) the Config class will dispatch a Signal that the currentGold changed. I'm adding a callback listener for when that gold changes, and it's to be handled by updateGoldTF().
- Line 100-106 - same thing as the _goldTF, _hpTF and the _waveTF to come are all set up the same... create a new instance, set the x/y, listen for its componentIsInvalid Signal, add it to the canvas, and listen for Config's Signals pertaining to that particular TextField.
- Line 117-123 - _infoText is a feathers.controls.ScrollText component and it's my cheap way out of actual UI. It's a simple TextArea (from old Flash) that scrolls and I'm updating this text area every time we hover over a tower. Set the x,y, set isHTML true to we can display things in HTML, set the textFormat to a new TextFormat passing in CalistoMT as my font of choice for that area, then add it to the canvas.
- Line 125-133 - the _upgradeTowerBtn and _sellTowerBtn are to regular old Starling Buttons, one that has an "Upgrade" Texture to it and one has a "Sell" Texture. This appear when a player has clicked an active tower on the Map so they can choose to upgrade it or sell it. Set their x,y, add them to the canvas
- Line 138-139 - create a new GameOptionsPanel and add Play's onQuitGameFromOptions() function as the callback for when the Panel dispatches its onQuitGame Signal. Note, I don't add that Panel to the stage yet. This is because Starling hasn't sorted out its display layers properly yet. If I add the _gameOptions to Play's topLayer Sprite (which should put it above everything in the game) and I add more towers to the towerLayer (which should be BEHIND the topLayer Sprite) the towers will appear on top of the GameOptionsPanel. Very frustrating.
- Line 141-145 - create a new Button for the "Options" button on the stage, set its x,y, add an event listener when the player clicks it and add it to the canvas
- Line 147 - reset the pause button with the "Play" button Texture
- Line 148 - add a callback to EnemyManager's onSpawnWave() Signal and handle it with our onSpawnWave() function
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);
}
- Line 1-8 - onSpawnWave() handles when the EnemyManager dispatches it's onSpawnWave Signal. This is just so that we can remove our next wave buttons as for some reason I just wanted those elements on the very first wave and not after. Remove the callback from EnemyManager's Signal, and call removeNextWaveButtons() passing in false to remove our buttons.
- Line 10-14 - update() gets called by Play and it calls updateUI() and WaveTileBar's update() function. Now, these are separated for a reason. Right after the Play State loads, I need to be able to update certain UI elements (HP, Gold, Wave Count TextFields, etc) without updating the WaveTileBar. As soon as I call update on that sucker, it breaks open the first wave and the game starts. So these are separate so from other classes I can specifically just call updateUI() and here, where Play is saying "Hey, there's been a game tick, update everything" we update both the updateUI() and the WaveTileBar.
- Line 16-23 - if we have any components that have told HudManager they need to be updated, loop through those components and call update() on the component, then splice it out of the _invalidComponents array. To put this into a concrete scenario, every game tick this gets called. If my Gold or HP hasn't changed at all, I don't want to waste time re-rendering those components. But let's say an Enemy has been killed and Config's currentGold var has had 5 gold added to it. _goldTF gets that changed event (Signal) and dispatches its own componentIsInvalid Signal. HudManager adds _goldTF to the _invalidComponents array and on the next game tick, we now have _goldTF in our _invalidComponents array to loop over here. For the majority of the game this shouldn't really loop because our TextFields stay pretty constant (relative to 60 updates a second and our TextFields changing maybe once every 180-300 updates).
- Line 26, 30 and 41 - now remember, _goldTF (also _hpTF and _waveTF) is of type ZFTextField which extends Sprite. For a normal TextField, setting the .text property would cause the TextField to update with the new text that you assigned it. Since this is a ZFTextField, and that extends Sprite, there is no "TextField.text" property on this class. Instead as we saw in the ZFTextField explanation I've got a setter function on ZFTextField public function set text(val:String):void which sets the text value to be used, and dispatches the componentIsInvalid Signal so that HudManager knows to update that component on the next tick.
- Line 32-37 - after we've set _hpTF's text property, we check to see if the HitPoint amount coming in via the "amt" param is less than or equal to 0. If it is, I call HudManager's _removeListeners so nothing else listens to update off any Signals, and then I dispatch the endOfHP Signal so Play knows we're out of hitpoints.
- Line 45 - when the ZFTextField components dispatch their componentIsInvalid Signal, this function handles it and we just add the component to the _invalidComponents array so we can update it on the next game tick
- Line 49 - this function adds the NextWaveButtons to the stage. we start by getting the array of positions that NextWaveButtons should be placed from the WaypointManager.
- Line 52-57 - create a new NextWaveButton, set its x and y based on the groupStartPositions array sent from the WaypointManager which we'll be looking at in this post. Add a callback for when the NextWaveButton gets clicked. Add the Button to the canvas and push this nextWaveButton onto the _nextWaveBtns array.
- Line 61-66 - removes the next wave buttons from the canvas. It does this by looping through all the _nextWaveBtns and calls NextWaveButton's fadeOut() function which causes it to shrink away and remove itself from the stage
- Line 68-70 - if triggerPause is true (default) then we want to tell Play that a pause event has occurred. What does that mean? So a pause event is an event that will cause the game to pause or resume itself. When the NextWaveButtons are on the stage remember, we have not started the game yet. So the game is paused. If triggerPause is true, this calls Play's onPauseEvent() function which will resume (start) the game. The reason triggerPause may be false is if the player has started the game either by pressing the Spacebar, or by clicking the Play/Pause button. If the game has already been started by those methods then I don't want to tell Play to pause.
- Line 74 - the player has already clicked on an active tower (setting that Tower to be the _activeTower var) and we call _activeTower.canUpgrade(). If the Tower says it can be upgraded...
- Line 75-76 - call Tower.upgrade() so the Tower sets its own new properties based on the upgrade info. Show the new Tower data for the _activeTower now that it has updated its values.
- Line 77-80 - if the Tower's level is equal to the Tower's maxLevel value (maxLevel is 3 and the Tower just upgraded to level 3) then we want to hide the upgrade and sell buttons, then call the show tower buttons function but passing false for the showUpgrade Boolean. This may seem strange and it is. You would find it slightly more efficient to just flat remove the _upgradeTowerBtn's event listener and make it not visible, however I already had the functions to do this, so I just figured I would use those functions to handle this.
- Line 82 - this is the case where the player has clicked the Upgrade button but they cant really upgrade the tower. Now, UI perspective, If you can't upgrade the Tower you've clicked on, This button should be grayed out or hidden. If you can't actually click something, make sure the player clearly knows they can't (hiding or showing it disabled) otherwise that's a point of frustration. All I've done for this case is to trace out where no one can see that the Tower can't be upgraded
- Line 86-91 - hides the tower buttons. It sets visible = false on _upgradeTowerBtn and _sellTowerBtn and removes their event listeners
- Line 93-101 - show's the tower buttons. It sets the Sell button to visible and adds the event listener. If showUpgrade is true, then also add the upgrade button.
- Line 104 - when the Sell button gets clicked, we call the TowerManager's sellTower() function passing in this _activeTower
- Line 108 - when the Pause button gets clicked, call Play's onPauseEvent() function to pause or resume the game
- Line 111-116 - when Play tells its Managers that the game is paused, HudManager calls _resetPauseBtn() and passes in the Play Button Texture, if _optionsVisible is true (the game options panel is on screen) then we remove all event listeners from the HUD. This way users can't click to Resume the game while the options panel is visible.
- Line 118-123 - when the game is resumed, _resetPauseBtn to the Pause Button Texture and if _optionsVisible is false, then we can re-add all the event listeners
- Line 125-135 - if the _pauseBtn is already on the _canvas, then we want to remove the _pauseBtn and remove its event listener. Create a new Button with the Texture that was passed in via the t param, set its x,y, add an event listener for if it gets clicked then add it to the _canvas
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");
}
}
}
- Line 1-4 - _onOptionsClicked() gets called when the player clicks the Options button. Set the _optionsVisible to true and clear any text that was in the Tower Info area because that FeathersUI ScrollText will actually display OVER the GameOptionsPanel even though we added that to the top of the display list.
- Line 7-9 - if _gameOptions isn't already on the stage, add it
- Line 11-14 - tell Play we've had a pause event and it's coming from the Options panel. Add a callback for the onDeactivated Signal and tell _gameOptions to activate(). Remove the event listener for the pause button since we don't want the player resuming the game when the options panel is visible.
- Line 17-23 - _onOptionsDeactivated() gets called when the GameOptionsPanel has been deactivated (cleared off the stage). Set _optionsVisible back to false, remove the callback listeners from the Options Signal, tell Play to resume, and add the event listener back to the Pause button.
- Line 25-29 - _onClickTowerSelectIcon is a callback for TowerSelectIcon's onClick Signal. It checks to see if the Options panel is visible and if we can create a Tower of this particular ID, if we can, then we call Play's createTower() and pass in the towerData ID, and the p:Point that the player clicked so we can create a tower right on the mouse click.
- Line 31-35 - _onHoverTowerSelectIcon() is a callback for TowerSelectIcon's onHover Signal. If the Options panel is not visible, it calls the generateTowerInfoHTML() function and passes in the towerData object to be turned into tower info html
- Line 37-45 - set the _activeTower to be the t:Tower passed in. if the _activeTower's level is less than the Tower's maxLevel, then set showUpgrade to true. Call _showTowerButtons() passing in that showUpgrade Boolean. Then call generateTowerInfoHTML() passing in the t:Tower
- Line 47-53 - if there's an _activeTower and the _activeTower is the t:Tower param passed in, then set the _activeTower to null, hide the tower buttons, and clear the Tower Info area. This gets called when a player had selected a tower, then clicked off to some other part of the Map.
- Line 55 - this function takes a towerData Object or an actual Tower class Object and converts the info from the Object/Tower to HTML that we can display in the Tower Info box.
- Line 60 - check to see if towerData is actually a Tower class Object, or if it's just your basic {} Object
- Line 62-66 - if isTower is true, as in, towerData is an actual Tower class, then use towerData.towerName for the name, otherwise use towerData.name. I could have skipped the isTower Boolean and simply checked
- Line 68-74 - build the HTML string
- Line 76-78 - if this is a Tower class and the Tower's level is < the Tower's maxLevel then add the Upgrade Cost: line of text to the Info
- Line 80 - show the info text of the HTML we just built
- Line 84 - sets the ScrollText's text property to the HTML String passed in
- Line 87+ - from Line 87 onward, we hit the _removeListeners(), _addListeners(), and destroy() functions that do just that. No real explanation is needed here I don't think.
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();
}
}
}
}
- Line 20-22 - grab a reference to the Play State, grab a reference to the Stage (flash.display.Stage), and add an event listener for the KEY_DOWN event.
- Line 24 - create a new onPause Signal
- Line 25 - the idea here was that each key that should cause an event to happen would have it's own Signal. So EnemyManager could add a callback for KeyboardManager's onSpacebar Signal, and HudManager could subscribe to the onP (when the P key was pressed) and I could handle events that way. But as it seems with the game change speed below, I dropped that. Anyways, I like that pattern, and will continue using it in my personal game, however in this Demo with the very small number of keyboard commands I'm using, it's a bit spotty. Push the onPause Signal onto the _signals array so that they can be destroyed later.
- Line 31-33 - when the player presses the Spacebar, dispatch the onPause Signal
- Line 35-43 - when the player presses the +/- button either on the number row of a keyboard or over on the keypad, call changeGameSpeed() and pass in the right constant for which way the speed should be set
- Line 47-52 - remove the Stage KeyDown event listener, then loop through all Signals and remove all listeners from them
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;
}
}
}
- Line 76-86 - here's the deal with my spawnPoints and endPoints. So in the map JSON file I declare all the tiles and which images should be drawn for those tiles. The problem adding waypoint info to that file is that the Tiles only show you what is visible in the Map. These Spawn and Endpoints need to start OFF The map. So for the spawnpoint around Line 53-73, I'm saving the spawn point data and pushing it onto its group array. For the Endpoint however, I push it into a temporary endpoints group object array to be used later.
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
0 Comments