After five long code-heavy tutorials, we finally arrive at the most central of components to the Tower Defense (TD) genre: Towers! Towers are crucial in your TD game. Now, by “towers” I don’t necessarily mean some phallic, stone structure jutting into the sky that rains death upon enemies nearby. “Towers” here could be any unit, object, creature, etc that the player can place on the map, that can impede the enemies from reaching their destination. Some games may have actual tower structures, some games may have army guys with bazookas, some games may have a house or spawning structure you place nearby and the player’s units spawn from there to stop enemies. The tower is the primary mechanic that empowers players with a way to experience strategy, tactics, accomplishment, and fun (not an exhaustive list of nouns!).
[toc]
That said, unfortunately I really don’t do the tower justice in this demo. If you’ve played the demo, you’ve seen that my two Towers are simply a blue and a red box. The enemies are animated and much much prettier than the towers. Hell, even the bullets have some particles with them and are prettier than the towers. Anyways, here’s the point. I showed you last time how I created the Enemy animations. You could do the exact same thing for Towers and have yourself some pretty animated towers with different states “reloading”, “firing”, etc.
Before we start, feel free to check out the finished product of my AS3 Starling TD Demo.
You can also find all of the code used in srcview.
Or you can download the whole project zipped up.
Or check out the repo on Bitbucket
**Update 8/1/13 – abstracted out Tower’s enemy-finding strategy to a ITowerStrategy.as and TowerStrategyNearest.as. Also added some commentary on design patterns.
Ok, like the previous post on enemies, we’ll start with the JSON data of how we define towers.
JSON Data Structure
{
"towers": [
{
"id": "tower1",
"index": 0,
"name": "Blue Tower",
"imageName": "towers/tower1_001",
"bulletImageName": "bullets/sparkle_blue_001",
"bulletSpeed": 12,
"bulletWidth": 32,
"bulletHeight": 32,
"towerWidth": 20,
"towerHeight": 20,
"maxLevel": 3,
"sounds": [
{
"state": "onFire",
"soundId": "shot1"
}
],
"levelData": [
{
"level": 1,
"range": 75,
"damage": 5,
"speed": 1500,
"dps": 3.33,
"cost": 10
},
{
"level": 2,
"range": 150,
"damage": 10,
"speed": 1250,
"dps": 8,
"cost": 30
},
{
"level": 3,
"range": 200,
"damage": 20,
"speed": 900,
"dps": 22.22,
"cost": 50
}
]
}
]
}
This is the first tower listed in the src/assets/json/towers/towerData.json file that defines two types of towers. Let’s look at it line-by-line.
- Line 2 – this is an array of “towers” objects to be used later
- Line 4 – this is the first tower’s id
- Line 5 – index allows me, from this JSON file, to rearrange the Towers. The towers get created and added to the stage based on the index property
- Line 6 – this is the player-facing name of the tower to use in info boxes and stuff
- Line 7 – imageName is used by HUDManager to place an image representing the tower onto the UI so the player can click it. Unfortunately it also becomes the image of the actual tower used on the map. Ideally, like with the Enemy data, there would just be a texturePrefix somewhere that said, “this tower uses ‘towers/tower1_’ tiles”.
- Line 8 – the image to use for the bullet the tower fires. If you wanted your towers to fire different bullets based on their levels, maybe you could move this property down into the levelData?
- Line 9 – bulletSpeed is how fast the actual bullet travels on the map per tick. You obviously want this to be much faster than the Enemy moves, but slow enough that you actually get to see the bullet in flight.
- Line 10-11 – the width/height of the bullet image
- Line 12-13 – the width/height of the tower image
- Line 14 – the maximum number of levels a tower can upgrade. So this tower starts at level 1 and can be upgrades to level 2, and finally level 3. So it can be upgraded 2 times essentially.
- Line 15 – this is an array of sound states like we saw with Enemy data. As we’ll see later, I’ve defined the Tower sound state “onFire” to play when the Tower fires a bullet. If you have the state: “onFire” defined here, it will play that sound.
- Line 21 – this is an array of the different params a tower has at different levels.
- Line 23 – what level is this data for? This Object has data for level 1.
- Line 24 – at this level, the tower has a range of 75
- Line 25 – at this level, the tower does 5 damage when a bullet hits
- Line 26 – at this level, the tower has a reload delay of 1500ms, or 1.5 seconds.
- Line 27 – at this level, the tower has a Damage Per Second (DPS) of 3.33 (5dmg / 1.5s = 3.33dps)
- Line 28 – at this level, the tower costs 10 gold
If you check out the file, you’ll see the other tower data listed there as well. Now we’re going to follow the data into the TowerManager and see what happens to it, then afterwards we’ll actually look at our Tower class and how that works.
TowerManager.as
package com.zf.managers
{
import com.zf.core.Config;
import com.zf.objects.enemy.Enemy;
import com.zf.objects.tower.Tower;
import com.zf.states.Play;
import flash.geom.Point;
import org.osflash.signals.Signal;
import starling.display.Sprite;
import starling.events.Touch;
import starling.events.TouchEvent;
import starling.events.TouchPhase;
public class TowerManager implements IZFManager
{
public var play:Play;
public var onTowerRemoved:Signal;
public var onTowerAdded:Signal;
public var onTowerManagerRemovingEnemy:Signal;
private var _towers:Array;
private var _towerData:Object;
private var _currentTower:Tower;
private var _p:Point = new Point();
private var _isDragging:Boolean = false;
private var _canvas:Sprite;
public function TowerManager(playState:Play, towerData:Object) {
play = playState;
_canvas = play.towerLayer;
_towers = [];
_setTowerData(towerData);
onTowerAdded = new Signal(Tower);
onTowerRemoved = new Signal(Tower);
onTowerManagerRemovingEnemy = new Signal(Enemy);
}
public function update():void {
if(_towers.length > 0) {
var t:Tower,
len:int = _towers.length;
for(var i:int = len - 1; i >= 0; i--) {
t = _towers[i];
t.update();
}
}
}
public function destroy():void {}
public function destroyTower(t:Tower):void {
var len:int = _towers.length;
for(var i:int = 0; i < len; i++) {
if(t == _towers[i]) {
_towers.splice(i, 1);
t.destroy();
t.removeFromParent(true);
}
}
onTowerRemoved.dispatch(t);
}
public function createNewTower(towerID:String, pos:Point):void {
if(_currentTower) {
_currentTower.deactivate();
}
var tower:Tower = new Tower(_towerData[towerID], this);
tower.setSoundData(_towerData[towerID].sounds);
tower.x = pos.x - tower.halfWidth;
tower.y = pos.y - tower.halfHeight;
tower.activated = true;
_currentTower = tower;
play.addChild(tower);
play.addEventListener(TouchEvent.TOUCH, towerFollowMouse);
}
public function towerFollowMouse(evt:TouchEvent):void {
var touch:Touch = evt.getTouch(play);
if(touch)
{
switch(touch.phase) {
case TouchPhase.BEGAN:
var checkObject:Object = play.map.checkCanPlaceTower(_p);
if(checkObject.canPlace) {
play.removeEventListener(TouchEvent.TOUCH, towerFollowMouse);
evt.stopImmediatePropagation();
placeTower(checkObject.point);
}
break;
case TouchPhase.ENDED:
break;
case TouchPhase.MOVED:
break;
case TouchPhase.HOVER:
var tPos:Point = touch.getLocation(play);
_currentTower.x = _p.x = tPos.x - _currentTower.halfWidth;
_currentTower.y = _p.y = tPos.y - _currentTower.halfHeight;
// Check if we can place, then update the tower image accordingly
_currentTower.enterFrameTowerPlaceCheck(play.map.checkCanPlaceTower(_p).canPlace);
break;
}
}
}
- Line 32 - grab a reference to the Play State
- Line 33 - set this Manager's _canvas to Play's towerLayer Sprite
- Line 35 - pass the towerData (which came from the towers.json file) in and set the tower data
- Line 37-39 - set up our Signals
- Line 42 - the Play State calls update on all of its Managers, likewise, the TowerManager loops through all of the towers in its _towers array and calls update on each one
- Line 53 - that is one sad, empty destroy() method. You can get the idea of what should be here from the enemies tutorial. It would be a loop through the _towers, calling destroy on each one, removing from the stage, and cleaning up Signals and such.
- Line 55 - this gives us a function where I can pass in a specific Tower instance and have that removed from TowerManager's management. This happens when a user Sells a Tower.
- Line 64 - when the Tower is removed, dispatch onTowerRemoved Signal to let CollisionManager know that there is one less Tower to loop through.
- Line 67 - createNewTower() gets called when the user clicks on a Tower icon on the HUD.
- Line 68 - _currentTower is a reference to the Tower the player last clicked on, so since clicking on a Tower causes the range ring to show up, if there's already a _currentTower, I want to call deactivate() on that tower so its range ring hides.
- Line 72 - create a new Tower class and pass in the towerData for this specific towerID, and pass in a reference to this TowerManager so the Tower can get back to the Manager. We'll discuss passing the Manager in later...
- Line 73 - set the sound data
- Line 74-75 - pos is the x/y position that the player clicked to click the Tower icon on the HUD, so I want to set the new Tower's x/y to pos x/y minus half the Tower's width and height. This centers the Tower icon on the mouse point.
- Line 76 - set the Tower to activated = true
- Line 77 - update _currentTower with this new tower
- Line 79 - now, normally I would add this tower to _canvas. I actually do that later as we'll see. But for now I want to add this Tower instance directly to Play. If I called _canvas.addChild(tower) then the Tower would actually be behind the HUD. Adding the tower to Play allows the Tower to be on top of all that.
- Line 80 - ...touch events... meh. Add an event listener to Play to listen for TOUCH and we'll see what we do on the next line...
- Line 83 - towerFollowMouse handles the TouchEvent for when the player is Touching the stage. It may be easier to think of this as a MouseEvent.MOUSE_MOVE handler, because that's exactly what I'm doing, except I'm having to use TouchEvents here. I had used MouseEvent.MOUSE_MOVE in an earlier iteration, but when I would place the Tower object with a click, somehow that click would hang around and when I added event listeners on the brand new Tower object, those would get triggered. Yeah... MouseEvents and TouchEvents do not play nice at all together. (**Update 8/16/13 - just found out about a new TouchProcessor class currently in the GitHub repo that will make this better! There's hope! So mostly ignore the bummed-out tone regarding touch events!)
- Line 84-85 - this gets a Touch Object from the event and if there is actually a touch object...
- Line 87 - I'm going to switch on touch.phase. Having to do a switch on Touch.phase would be like if you just got one single MouseEvent for MOUSE_DOWN, CLICK, MOUSE_UP, MOUSE_MOVE, and all the other MouseEvent events. Anything you do with the Mouse immediately throws the same MouseEvent and then you have to go through conditional logic to figure out which damn event was thrown, and what you want to do about it, instead of being able to listen to one specific state of the MouseEvent and just react on that. TouchEvents, if you can't tell yet, are not my favorite thing. If you are NOT developing for a phone or table (which, yes, is why Starling is here), they are a bit of a pain to get used to and work around. I realize Starling is from Sparrow and its roots are in phone development. Anyways, moving on.
- Line 88 - TouchPhase.BEGAN happens when your mouse is down. There is no concept of "MOUSE_UP" in Starling, so we don't know if the user has clicked and held down the mouse button, and they are dragging the Tower icon, or if they just clicked once to create the Tower and now they've let off the mouse button and they're just moving the mouse. This only allows the player to click and let off, then move their mouse to place a Tower. You cannot drag the Tower icon around the stage. But oh well, minor use case ignored. I will probably want to spend more time trying to get this to work for both cases just in case people like playing one way or the other.
- Line 89 - call our Map.checkCanPlaceTower() and pass in the TouchEvent's position Point
- Line 90 - if the Object that comes back has canPlace set to true, then we can place the Tower here.
- Line 91 - remove the touch listener so it doesnt fire again during placeTower
- Line 92 - stop the event from leaving this function
- Line 93 - place the tower
- Line 103 - HOVER is sortof like MOUSE_MOVE
- Line 104 - get the touch position Point
- Line 105-106 - update the _currentTower's x/y and a class _p Point object's x/y to the touch location position minus half the width/height again to keep the Tower icon centered on the mouse.
- Line 109 - calling Map.checkCanPlaceTower() again and passing that Boolean "canPlace" property into the Tower.enterFrameTowerPlaceCheck(). As we'll see later, that handles if the range ring is red and we cant place the Tower in this tile, or if the range ring is blue and we can place it here.
Quick break... then continuing with TowerManager.as
public function removeEnemyFromTowers(e:Enemy):void {
Config.log('TowerManager', 'removeEnemyFromTowers', "Removing Enemy " + e.uid + " from Towers");
var len:int = _towers.length;
for(var i:int = 0; i < len; i++) {
_towers[i].removeEnemyInRange(e);
}
onTowerManagerRemovingEnemy.dispatch(e);
}
public function addListenerToDeactivateTower(t:Tower):void {
if(_currentTower != t) {
// handle previous _currentTower
Config.log('TowerManager', 'addListenerToDeactivateTower', 'TowerManager.addListenerToDeactivateTower() -- deactivating old tower');
_currentTower.deactivate();
}
_currentTower = t;
Config.log('TowerManager', 'addListenerToDeactivateTower', "TowerManager.addListenerToDeactivateTower() -- adding listener: " + play.map.width + ", " + play.map.height);
play.map.addEventListener(TouchEvent.TOUCH, _onMapClicked);
}
public function placeTower(p:Point):void {
Config.totals.towersPlaced++;
var xOffset:int = int((play.map.tileWidth - (_currentTower.halfWidth << 1)) >> 1) + play.mapOffsetX;
var yOffset:int = int((play.map.tileHeight - (_currentTower.halfHeight << 1)) >> 1) + play.mapOffsetY;
_currentTower.x = p.x + xOffset;
_currentTower.y = p.y + yOffset;
_canvas.addChild(_currentTower);
play.map.placedTower(_currentTower);
_towers.push(_currentTower);
_currentTower.init();
_currentTower.onFiring.add(play.bulletMgr.onTowerFiring);
onTowerAdded.dispatch(_currentTower);
}
public function sellTower(t:Tower):void {
// Add half the cost back to currentGold This can be modified later!
Config.changeCurrentGold(int(t.cost >> 1));
destroyTower(t);
}
private function _onMapClicked(evt:TouchEvent):void {
var touch:Touch = evt.getTouch(play.map, TouchPhase.BEGAN);
if (touch)
{
var localPos:Point = touch.getLocation(play.map);
Config.log('TowerManager', '_onMapClicked', "mapClicked! " + play.map.width + " -- " + localPos.x);
// if we clicked anywhere but the tower
if(touch.target != _currentTower) {
play.hudMgr.hideTowerData(_currentTower);
_currentTower.deactivate();
play.map.removeEventListener(TouchEvent.TOUCH, _onMapClicked);
}
}
}
public function getTowerCost(towerID:String, level:int):int {
return _towerData[towerID].levelData[level].cost;
}
private function _setTowerData(td:Object):void {
_towerData = {};
for each(var data:Object in td.towers) {
_towerData[data.id] = data;
}
}
// If TowerManager needs to do anything on Pause/Resume
public function onGamePaused():void {}
public function onGameResumed():void {}
}
}
- Line 1 - gets called by Tower when the Tower checks to see if its next shot will kill the Enemy, if it will, it calls TowerManager.removeEnemyFromTowers() so the Enemy gets removed from other Tower objects so they don't try to shoot too.
- Line 5 - loop through all _towers and call removeEnemyInRange() on all of them.
- Line 7 - dispatch that Signal so CollisionManager knows there's one less Enemy to be accounting for in the loops
- Line 10 - addListenerToDeactivateTower is called when a Tower has been clicked. When a Tower is clicked, it becomes activated and its range ring is shown
- Line 14 - if there was a _currentTower already and it wasn't this t:Tower passed in, then deactivate the old tower
- Line 16 - update _currentTower with the new tower passed in
- Line 18 - set an event listener on the Map for a TouchEvent. This is very important. I want the Touch listener specifically placed on the Map object because I want Upgrade and Sell buttons off to the side. If I placed the Touch listener on Play, then anywhere I clicked would trigger that handler. So, I'm only listening on Map, and I can click anywhere I want outside of Map
- Line 21 - this function actually places a Tower... cements it into place in a specific tile.
- Line 22 - update our running towersPlaced totals count
- Line 24-25 - Take the tileWidth/tileHeight of the tiles, subtract the width/height of just the tower image since the _currentTower.width is currently expanded due to the Tower's range ring, get the halfWidth/halfHeight and multiply them by 2 (bitwise << 1). What's that spell? 42. No, so basically again I need an offset. I've got a Tower that is 20x20 px, a tile that is 24x24 px AND the 36px offset from the top-left corner of the map beginning at (36,36) not (0,0). So this calculates that all out and tells me the exact x/y coordinate to place the Tower icon so that it fits centered inside the tile. Wee.
- Line 27-28 - actually set the _currentTower's x/y to the coordinates passed in with p:Point plus offsets. p:Point in this case is actually the correct x,y coordinates for the top-left corner of a tile so we can just add the offset and know we're going to be all centered up.
- Line 30 - now that we're ready for the tower to be placed BEHIND the UI, add the tower to the towerLayer (_canvas) instead of Play
- Line 31 - call Map.placedTower and pass in the Tower instance. This, as we've seen, lets Map update it's map data so that another Tower cannot be placed onto this Tower.
- Line 32 - add this new Tower to the _towers array
- Line 33 - initialize the currentTower
- Line 34 - set up the ProjectileManager to handle when this tower is firing
- Line 35 - let managers know a new Tower has been added
- Line 40 - if a player has clicked on a Tower then clicked the Sell button, this function gets called. This should be better set up, but basically when you sell a Tower, I'm adding half of the Tower's cost back to the player's currentGold. This line right here could be pulled out into a whole new set of upgrades. "Salvage more when selling Towers" and each upgrade point saves you an extra 10% of the Tower's cost when you sell or something.
- Line 41 - destroy the tower.
- Line 44 - _onMapClicked() gets called after a player has clicked on a Tower, activating it, and we added an event listener listening for any touches to the Map.
- Line 48 - find where we clicked on the Map
- Line 51 - as long as we did not click on the same Tower we had previously activated,
- Line 52 - tell HudManager to clear the Tower's data
- Line 53 - deactivate the Tower
- Line 54 - remove the event listener from Map
- Line 59 - this is a public function that lets us get at the private _towerData object specifically for cost.
- Line 63 - takes the towerData Object from the JSON file and parses it into the local _towerData Object that the class uses.
- Line 71-72 - TowerManager doesn't need to do anything on pause/resume yet.
Alright... so now we're through the TowerManager.as. Let's look at the actual Tower.as.
Update 8/1/13 - updated Tower to actually use a proper abstracted TowerStrategy which we'll talk about later.
Tower.as
package com.zf.objects.tower
{
import com.zf.core.Assets;
import com.zf.core.Config;
import com.zf.core.Game;
import com.zf.managers.TowerManager;
import com.zf.objects.enemy.Enemy;
import com.zf.objects.tower.strategies.*;
import com.zf.states.Play;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Shape;
import flash.events.TimerEvent;
import flash.geom.Matrix;
import flash.utils.Timer;
import flash.utils.clearInterval;
import org.osflash.signals.Signal;
import starling.core.Starling;
import starling.display.Image;
import starling.display.MovieClip;
import starling.display.Sprite;
import starling.events.Touch;
import starling.events.TouchEvent;
import starling.events.TouchPhase;
public class Tower extends Sprite
{
public static const TOWER_STATE_NEW : int = 0;
public static const TOWER_STATE_INIT : int = 1;
public static const TOWER_STATE_READY : int = 2;
public static const TOWER_STATE_RELOAD : int = 3;
public static const TOWER_STATE_FIRING : int = 4;
public static const TOWER_ANIM_STATE_SAME:String = 'towerAnimStateSame';
public static const TOWER_STRAT_NEAREST:String = 'towerStrategyNearest';
// Sound states
public static const TOWER_SND_STATE_FIRE:String = 'onFire';
public var uid:int;
public var towerName:String;
public var index:int;
public var level:int;
public var range:Number;
public var damage:Number;
public var speed:Number;
public var dps:Number;
public var cost:int;
public var maxLevel:int;
public var halfWidth:Number;
public var halfHeight:Number;
public var state:int = TOWER_STATE_NEW;
public var onFiring:Signal;
public var bulletType:String;
public var bulletSpeed:int;
public var bulletWidth:int;
public var bulletHeight:int;
public var centerX:Number;
public var centerY:Number;
public var towerStrategy:ITowerStrategy;
public var levelData:Object;
public var nextDamage:Number;
public var onUpgrade:Signal;
// Range distance Squared
public var rangeSquared:Number;
// If this tower is currently clicked on
public var activated:Boolean = false;
protected var _animState:String;
protected var _animData:Object;
protected var _imageName:String;
protected var _bulletImageName:String;
protected var _rangeRing : Image;
protected var _badRangeRing : Image;
protected var _rangeRingDepthIndex:int;
protected var _reloadTimer:Timer;
protected var _soundData:Object;
// reload timer delay with game speed factored in
protected var _reloadTimerGameSpeedDelay:Number;
// Enemies in range
protected var _enemiesObj:Object;
protected var _enemies:Array;
protected var _currentRange:int;
protected var _lowestSqrt:Number;
// Range ring graphics options
protected var _rangeRingFillColor:int = 0X009999;
protected var _badRangeRingFillColor:int = 0XFF0000;
protected var _borderColor:int = 0X000000;
protected var _borderSize:int = 1;
protected var _mgr:TowerManager
private var _interval:uint;
public function Tower(towerData:Object, tMgr:TowerManager)
{
uid = Config.getUID();
_mgr = tMgr;
towerName = towerData.name;
index = towerData.name;
level = 1;
range = towerData.levelData[0].range;
damage = towerData.levelData[0].damage;
speed = towerData.levelData[0].speed;
cost = towerData.levelData[0].cost;
dps = towerData.levelData[0].dps;
maxLevel = towerData.maxLevel;
_imageName = towerData.imageName;
_bulletImageName = towerData.bulletImageName;
halfWidth = towerData.towerWidth >> 1;
halfHeight = towerData.towerHeight >> 1;
bulletType = towerData.bulletImageName;
bulletSpeed = towerData.bulletSpeed;
bulletWidth = towerData.bulletWidth;
bulletHeight = towerData.bulletHeight;
_updateStatsByUpgrades();
nextDamage = 0;
onUpgrade = new Signal(Tower);
onFiring = new Signal(Tower, Enemy);
_enemiesObj = {};
_enemies = [];
_soundData = {};
resetTowerAfterUpgrade();
_parseLevelData(towerData.levelData);
_setupAnimData();
_createRangeRing();
_createBadRangeRing();
setTowerStrategy(new TowerStrategyNearest());
}
protected function resetTowerAfterUpgrade():void {
rangeSquared = range * range;
// set up reloadTimer
_reloadTimerGameSpeedDelay = Config.currentGameSpeed * speed;
_reloadTimer = new Timer(_reloadTimerGameSpeedDelay, 1);
_reloadTimer.addEventListener(TimerEvent.TIMER_COMPLETE, reloadDoneReadyTower, false, 0, true);
}
protected function _parseLevelData(data:Array):void {
levelData = {};
for each(var tData:Object in data) {
levelData[tData.level] = tData;
}
}
public function init():void {
_interval = flash.utils.setInterval(_init, 100);
}
protected function _init():void {
// clear the stupid interval because of the touchevents
flash.utils.clearInterval(_interval);
hideRangeRing();
// let tower listen for game speed change
Config.onGameSpeedChange.add(onGameSpeedChange);
centerX = this.x + halfWidth;
centerY = this.y + halfHeight;
// since we dont need this anymore, null it out
_badRangeRing.visible = false;
_badRangeRing = null;
_addListeners();
state = TOWER_STATE_READY;
}
public function setTowerStrategy(strat:ITowerStrategy):void {
towerStrategy = strat;
}
- Line 114-132 - a whole lot of setting local vars from tower data
- Line 119-123 - setting range/damage/speed/cost/dps from level 1 data or levelData[0]
- Line 134 - we'll see this function later but basically it updates your stats based on the points you've added to upgrades
- Line 136 - nextDamage is the calculation of the next damage this tower's projectile will do, init it to 0
- Line 138-138 - set up our Signals
- Line 146 - resets variables now that upgrades have been applied
- Line 148 - handles new tower levelData
- Line 149 - like Enemy, we're setting up the animation data for the Tower. However, since the towers in this demo are not animated, this is kindof useless but the process remains for when I actually add fancy towers
- Line 150-151 - these functions actually draw the range ring circles around the Tower
- Line 153 - set the default towerStrategy to TowerStrategyNearest
- Line 156-163 - called when we've updated our core stats, this function recalculates rangeSquared and the reload timer delay speed
- Line 165-170 - parses the array of levelData objects into an object the Tower can use
- Line 172 - initializes the tower, lets the Tower know it has been placed so it can do any other setup it needs
- Line 173 - we have to call setInterval here because when you click to place the tower, then add touch event listeners, those touch event listeners somehow get triggered. I have to tell Flash to wait 100ms before really actually initializing the Tower and adding touch events to let those bubbling events clear out. No amount of stopImmediatePropagation or anything would help.
- Line 178 - when the 100ms is over, remove the interval
- Line 180 - hide the range ring. The Tower has been placed, the player can click the Tower again if they want the range ring.
- Line 183 - listen for Config's onGameSpeedChange Signal so the Tower can update. This should probably be at the Manager level so the manager can update all of its Towers.
- Line 189-190 - get rid of the bad range ring since the Tower is already properly placed.
- Line 193 - set the Tower's state to ready
- Line 196 - called to set or change the Tower's Strategy. So far this is all that needs to happen here, but you could also potentially have different visual states for your towers depending on where they're aiming. So that could be handled here too.
Splitting Tower.as up... and continuing...
public function update():void {
if(state == TOWER_STATE_READY && _enemies.length > 0) {
var targetEnemy:Enemy = towerStrategy.findEnemy(_enemies);
if(targetEnemy) {
_generateNextShotDamage();
if(targetEnemy.willDamageBanish(nextDamage)) {
_mgr.removeEnemyFromTowers(targetEnemy);
}
fireBullet(targetEnemy);
}
}
}
public function setSoundData(soundData:Array):void {
var len:int = soundData.length;
for(var i:int = 0; i < len; i++) {
_soundData[soundData[i].state] = soundData[i].soundId;
}
}
protected function _generateNextShotDamage():void {
nextDamage = damage;
}
protected function fireBullet(targetEnemy:Enemy):void {
state = TOWER_STATE_FIRING;
_playIfStateExists(TOWER_SND_STATE_FIRE);
onFiring.dispatch(this, targetEnemy);
reload();
}
protected function _setupAnimData():void {
_animData = {};
_animData[TOWER_ANIM_STATE_SAME] = new MovieClip(Assets.ta.getTextures(_imageName), 8);
_changeAnimState(TOWER_ANIM_STATE_SAME);
}
public function enterFrameTowerPlaceCheck(canPlace:Boolean):void {
if(canPlace) {
_rangeRing.visible = true;
_badRangeRing.visible = false;
} else {
_rangeRing.visible = false;
_badRangeRing.visible = true;
}
}
public function addEnemyInRange(e:Enemy, range:Number):void {
Config.log('Tower', 'addEnemyInRange', "Tower " + uid + " Adding Enemy " + e.uid + " in range");
_enemiesObj[e.uid] = e;
e.onBanished.add(removeEnemyInRange);
_enemies.push(e);
}
public function removeEnemyInRange(e:Enemy):void {
if(_enemiesObj[e.uid]) {
e.onBanished.remove(removeEnemyInRange);
// delete object version
delete _enemiesObj[e.uid];
// remove array version
var len:int = _enemies.length;
for(var i:int = 0; i < len; i++) {
if(e == _enemies[i]) {
_enemies.splice(i, 1);
}
}
}
}
public function canUpgrade():Boolean {
var canUp:Boolean = false;
if(level < maxLevel && levelData[level + 1].cost <= Config.currentGold) {
canUp = true;
}
return canUp;
}
- Line 2 - if the Tower is in the ready state and there are actual _enemies to shoot at...
- Line 3 - I'll talk more about the Strategy Pattern below when we look at the ITowerStrategy, but right here we can see an incredibly elegant solution to a Tower having multiple ways of finding an Enemy. At this point, we're basically sending the _enemies array to whichever ITowerStrategy is currently set for this Tower and it should Magically return us an Enemy based on whichever strategy the player has set for this Tower. In practice, in this demo, there is no way to change strategies. Sorry 🙁
- Line 5 - if the strategy returned us a valid target Enemy...
- Line 6 - calculate the next shot's damage
- Line 7-8 - if the next shot's damage will kill the enemy, call TowerManager.removeEnemyFromTowers()
- Line 10 - fire at the closestEnemy
- Line 15 - sets the sound data
- Line 22-24 - this is where calculations for the next shot's damage would happen taking into consideration any tower buffs/effects/etc assuming the tower does variable damage. For now, just setting nextDamage to the flat damage property of the tower
- Line 26 - function that actually fires the bullet
- Line 27 - set the Tower's state to firing
- Line 28 - play the onFire sound state if it exists
- Line 29 - dispatch this Tower and the enemy we're firing at through the onFiring Signal
- Line 30 - call reload()
- Line 33 - as we saw with Enemy, set up tower animation state data
- Line 33-39 - reset _animData, create a new MovieClip with the Tower texture, then change the animation state. Since there is only one state for the tower in the demo, that stays constant.
- Line 41 - gets called when a new Tower has been created but the player has not placed it yet. This happens every time the mouse moves until the player places the Tower.
- Line 43-44 - if we can place the Tower here, turn on the _rangeRing (blue shading) and turn off the _badRangeRing (red shading)
- Line 46-47 - if we can not place the Tower here, turn off the _rangeRing (blue shading) and turn on the _badRangeRing (red shading)
- Line 51 - addEnemyInRange gets called by the CollisionManager when it determines that an Enemy is within this Tower's range
- Line 58 - removeEnemyInRange gets called by CollisionManager when it determines that an Enemy that Was in range of the Tower is no longer within that range, so it removes the Enemy from the Tower's _enemies array
- Line 59 - doublecheck to make sure the Enemy is actually still being tracked by the Tower
- Line 60 - remove the Signal callback for when that Enemy gets banished
- Line 63 - delete the Enemy object from _enemiesObj
- Line 66-71 - loops through the _enemies array looking for the Enemy to remove and removes it
- Line 75 - Checks if we can upgrade this tower
- Line 78-79 - is the Tower's level less than maxLevel and is the cost of the next highest Tower less than or equal to the player's currentGold, if so, canUp[grade] is set true
Ok, one final break and let's finish up Tower.as...
public function upgrade():void {
level++;
range = levelData[level].range;
damage = levelData[level].damage;
speed = levelData[level].speed;
dps = levelData[level].range;
cost = levelData[level].cost;
// apply upgrade bonuses
_updateStatsByUpgrades();
// remove (add a negative cost) gold to currentGold in Config
Config.changeCurrentGold(-cost);
// redraw the range ring
_createRangeRing();
resetTowerAfterUpgrade();
onUpgrade.dispatch(this);
}
public function hasEnemyInRange(e:Enemy):Boolean {
return (_enemiesObj[e.uid]);
}
public function showRangeRing():void {
_rangeRing.visible = true;
}
public function hideRangeRing():void {
_rangeRing.visible = false;
}
protected function _changeAnimState(newState:String):void {
// make sure they are different states before removing and adding MCs
if(_animState != newState) {
// remove the old MovieClip from juggler
Starling.juggler.remove(_animData[_animState]);
// remove the old MovieClip from this Sprite
removeChild(_animData[_animState]);
_animState = newState;
// add the new MovieClip to the Juggler
Starling.juggler.add(_animData[_animState]);
// add the new MovieClip to the Sprite
addChild(_animData[_animState]);
}
}
protected function _addListeners():void {
addEventListener(TouchEvent.TOUCH, _onTowerSelected);
state = TOWER_STATE_READY;
}
protected function _removeListeners():void {
removeEventListener(TouchEvent.TOUCH, _onTowerSelected);
}
public function destroy():void {
// remove the old MovieClip from juggler
Starling.juggler.remove(_animData[_animState]);
// remove the old MovieClip from this Sprite
removeChild(_animData[_animState]);
}
protected function reload():void {
state = TOWER_STATE_RELOAD;
_reloadTimer.reset();
// If game speed has changed, update the delay after the tower is done reloading
_reloadTimer.delay = _reloadTimerGameSpeedDelay;
_reloadTimer.start();
}
public function onGameSpeedChange():void {
// update the game speed timer
_reloadTimerGameSpeedDelay = int(speed / Config.currentGameSpeed);
_reloadTimer.delay = _reloadTimerGameSpeedDelay - _reloadTimer.currentCount;
}
protected function reloadDoneReadyTower(evt:TimerEvent):void {
state = TOWER_STATE_READY;
}
public function activate():void {
activated = true;
showRangeRing();
_mgr.addListenerToDeactivateTower(this);
_mgr.play.hudMgr.showTowerData(this);
}
public function deactivate():void {
activated = false;
hideRangeRing();
_addListeners();
}
private function _updateStatsByUpgrades():void {
range = Config.currentGameSOData.applyUpgradeToValue(range, 'towerRng');
speed = Config.currentGameSOData.applyUpgradeToValue(speed, 'towerSpd');
damage = Config.currentGameSOData.applyUpgradeToValue(damage, 'towerDmg');
}
protected function _onTowerSelected(evt:TouchEvent):void {
var touch:Touch = evt.getTouch(this, TouchPhase.BEGAN);
if (touch)
{
_removeListeners();
activate();
}
}
protected function _playIfStateExists(state:String):void {
if(_soundData.hasOwnProperty(state)) {
Game.soundMgr.playFx(_soundData[state], Config.sfxVolume);
}
}
protected function _createRangeRing():void {
if(contains(_rangeRing)) {
_rangeRing.removeFromParent(true);
}
var _s:Shape = new Shape();
_s.graphics.lineStyle(1);
_s.graphics.beginFill(_rangeRingFillColor);
_s.graphics.lineStyle(_borderSize , _borderColor);
_s.graphics.drawCircle(0, 0, range);
var matrix:Matrix = new Matrix();
matrix.tx = _s.width >> 1;
matrix.ty = _s.height >> 1;
var _bmd:BitmapData = new BitmapData(_s.width << 1, _s.height << 2) + halfWidth;
_rangeRing.y = -(_rangeRing.height >> 2) + halfHeight;
_rangeRingDepthIndex = numChildren - 1;
_rangeRing.touchable = false;
addChildAt(_rangeRing, _rangeRingDepthIndex);
}
protected function _createBadRangeRing():void {
var _s:Shape = new Shape();
_s.graphics.lineStyle(1);
_s.graphics.beginFill(_badRangeRingFillColor);
_s.graphics.lineStyle(_borderSize, _borderColor);
_s.graphics.drawCircle(0, 0, range);
var matrix:Matrix = new Matrix();
matrix.tx = _s.width >> 1;
matrix.ty = _s.height >> 1;
var _bmd:BitmapData = new BitmapData(_s.width << 1, _s.height << 2) + halfWidth;
_badRangeRing.y = -(_badRangeRing.height >> 2) + halfHeight;
_badRangeRing.touchable = false;
addChildAt(_badRangeRing , numChildren - 1);
}
}
}
- Line 1 - upgrade() is called when we can upgrade our Tower
- Line 2-7 - update the Tower's stats by the new level
- Line 16 - redraw the new rangeRing with the new range value
- Line 21 - checks if this Tower is holding onto a reference of a specific Enemy
- Line 25 - makes the _rangeRing visible
- Line 29 - hides the _rangeRing
- Line 33 - we saw this in the Enemy class. It just changes our animation state.
- Line 50 - adds any internal event listeners needed
- Line 55 - removes any internal event listeners added
- Line 59 - destroys the Tower
- Line 66 - gets called right after the Tower fired at an Enemy, it sets the Tower's state to reload and resets/starts the reload timer
- Line 75 - handles what should happen with the Tower when the game speed changes
- Line 77 - takes the Tower's speed (say 1000ms) and divides it by the currentGameSpeed (0.5, 1, or 2). So if we speed up to double time, it would take 1000ms / 2 which gives the Tower a 500ms delay between shots
- Line 82 - the reload timer has finished, set the Tower's state back to ready
- Line 85 - activate() gets called when the player clicks on this Tower. Set activated to true, show the range ring, and add event listeners to the Map for when we should deactivate(). It also calls the HUDManager and tells it to display this Tower info.
- Line 92 - deactivates the range ring and adds the listeners back so this Tower can be clicked on again
- Line 98 - runs current stats past any upgrade bonuses
- Line 104 - handle what needs to happen when users click on the tower, remove the touch listeners and activate the Tower
- Line 113 - we saw from Enemy that this checks to see if we have a specific sound state, and if we do, play it.
- Line 119 - this draws the _rangeRing
- Line 120-121 - if _rangeRing already exists (has already been drawn) remove it
- Line 124-128 - create a new flash.display.Shape and draw a circle
- Line 130-132 - this lets us create a new transform Matrix to set the x and y of the new Shape at half the width and height. Basically this lets us center the Shape onto the Tower
- Line 134-135 - draw a new BitmapData object from the Shape using the Matrix we created
- Line 137-139 - _rangeRing is a new Image from a Bitmap (passing in the BitmapData Object we just drew), set it's name property and set the alpha to 0.4 so it's a bit transparent
- Line 141-142 - again lets us center the new Image we created on the Tower
- Line 144-147 - set the depth so the _rangeRing gets pushed behind the Tower, set touchable to false so you can't click the _rangeRing and add that _rangeRing to this Tower's Sprite
- Line 150 - does the exact same thing as the last class with a red color. I should combine these into one single function and just pass in the color I want the range ring to be
So there is how the Towers are made and managed. Now let's turn our attention to a really fun topic, Design Patterns!
Strategy Design Pattern
Design Patterns are a tool. They are a way for you, as the developer, to use battle-tested patterns of how to structure your code so that you can get the most reusability, extensibility, and other -ity words that you'll find in the definitions of "Object Oriented Programming". Being that Design Patterns are a tool, if you ignore them, you're none too bright and you may be wasting your time when perhaps the wheel has already been invented. Like a tool though, you don't use a hammer when trying to cut down a tree. That's what chainsaws or axes are for. The sign of a solid developer is knowing when to use appropriate design patterns, and almost more importantly, when Not to use design patterns. Be well-versed in Design Patterns and know when and when not to use them.
If you overuse them...
Getting down to brass tacks here, we're specifically using the Strategy Pattern for our Tower's algorithm to find enemies. I'll explain what the code looked like before abstracting it out using the Strategy Pattern. The towerStrategy variable was a String, it held constants like this:
public static const TOWER_STRAT_NEAREST:String = 'towerStrategyNearest';
public var towerStrategy:String = TOWER_STRAT_NEAREST;
Already off to an inelegant start. Then when we got to the update() and actually went to look for an enemy...
public function update():void {
if(state == TOWER_STATE_READY && _enemies.length > 0) {
var closestEnemy:Enemy,
tmpEnemy:Enemy;
closestDist:Number = -1,
len:int = _enemies.length;
if(towerStrategy == TOWER_STRAT_NEAREST) {
for(var i:int =0; i < len; i++) {
tmpEnemy = _enemies[i];
if(closestDist == -1) {
closestDist = tmpEnemy.totalDist;
closestEnemy = tmpEnemy;
} else if(tmpEnemy.totalDist < closestDist) {
closestDist = tmpEnemy.totalDist;
closestEnemy = tmpEnemy;
}
}
} else if(towerStrategy == TOWER_STRAT_FARTHEST) {
} else if(towerStrategy == TOWER_STRAT_MOST_HP) {
} else if(towerStrategy == TOWER_STRAT_LEAST_HP) {
}
... ...
}
}
When we have just one single tower strategy (which you may very well have!) it makes sense not to jump through all the hoops of creating an Interface and implementing that interface in classes. It's a bit of extra work and unnecessary. However, you can clearly see above that as soon as we add 2 or more strategies, your update() function is about to get incredibly cluttered and difficult to maintain. Enter the Strategy Pattern...
The intent of the Strategy Pattern is to be able to define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients (Towers) that use it.
ITowerStrategy.as
ITowerStrategy.as is found at com.zf.objects.tower.strategies and defines the Interface that all tower strategies should have.
package com.zf.objects.tower.strategies
{
import com.zf.objects.enemy.Enemy;
public interface ITowerStrategy
{
function findEnemy(enemies:Array):Enemy;
}
}
- Line 7 - we're simply creating a new interface here with one function. Every ITowerStrategy implementation will have a findEnemy function that takes a single enemies array as a param.
Now we get to the implementation...
TowerStrategyNearest.as
package com.zf.objects.tower.strategies
{
import com.zf.objects.enemy.Enemy;
public class TowerStrategyNearest implements ITowerStrategy
{
public function TowerStrategyNearest() {
}
public function findEnemy(enemies:Array):Enemy {
var targetEnemy:Enemy,
len:int = enemies.length,
tmpEnemy:Enemy,
closestDist:Number = -1;
for(var i:int =0; i < len; i++) {
tmpEnemy = enemies[i];
if(closestDist == -1) {
closestDist = tmpEnemy.totalDist;
targetEnemy = tmpEnemy;
} else if(tmpEnemy.totalDist < closestDist) {
closestDist = tmpEnemy.totalDist;
targetEnemy = tmpEnemy;
}
}
return targetEnemy;
}
}
}
- Line 7 - nothing really at all to the constructor
- Line 10 - here's our interface function
- Line 16 - loop through all the enemies
- Line 18 - if closestDist hasnt been initialized properly yet...
- Line 19-20 - might as well start here, set closestDist to this enemy's totalDist and make this the closestEnemy
- Line 22-23 - if this enemy is closer to escaping than our previous closestEnemy, make this new tmpEnemy the closestEnemy/Dist to beat
- Line 26 - return whichever enemy we found from our loop
So let's step back from the code and look at this from a project perspective. Why is this useful or helpful at all? Well, 1) as previously stated, this cleans up our Tower.update() function considerably. There's no longer a massive, nasty if/else if/else if (or switch) so there's no way for the logic to get confusing. Each Strategy's specific algorithm for finding a target enemy is encapsulated within it's own class. And 2) Unit Tests.
You can easily Unit Test this class' findEnemy() function infinitely easier than if you were trying to test Tower.update() with its nasty if/else lines. Think about how important this aspect of your code is; a Tower's ability to select the right Enemy that the user wants. I ran into this case when I started this project: my Tower Nearest strategy was returning the Enemy that was by-pixel closer to the actual tower, than by-distance closer to escaping. While testing my demo it drove me nuts! I told the Tower to attack the Enemy that was nearest to escaping, and here it was shooting 4 enemies behind the leader because that Enemy was nearer to the Tower.
If you've read through comments on TD games on public sites like Kongregate or Armor Games, you've probably seen countless comments like "towers wont shoot at the correct enemies!". This is a fairly common issue for devs to get wrong because we don't realize how intensely important this is. Your player's focus is 100% on that first Tower once they place it on the Map. That Tower is the only thing keeping them from losing the game, or getting an achievement or high score. They're entirely invested in that Tower and since TD games are about "strategy", your Tower properly doing exactly what is expected is paramount.
So, adding Unit Tests around these findEnemy functions will give you an easy way to test if your Tower will actually return the correct enemy to fire upon in the actual game. This saves you from having to playtest countless hours to make sure your tower is firing properly in every given scenario. Just Unit Test the code with a GOOD test, and you'll save yourself a lot of time debugging and playtesting, and you'll save your players from having to leave nasty comments on your game. 🙂
Feel free to check out the finished product of my AS3 Starling TD Demo.
You can also find all of the code used in srcview.
Or you can download the whole project zipped up.
Or check out the repo on Bitbucket
Thanks for reading and I hope this was helpful!
Travis
5 Comments