AS3 Starling Tower Defense Tutorial – Part 1 – External File Loading

I’ve finally finished (well… 95%) with my AS3 Starling Game Engine Demo so I can finally start writing tutorials on how you can create a Tower Defense (TD) style game using Starling. Since I have so much info planned, and there’s so much code, and I’m a developer, and I think there’s a vast lack of intermediate/advanced AS3 tutorials on the web, this will probably not be the best tutorial series if you are a beginning coder. None of this stuff is incredibly complex or “expert” or anything, but I’ll probably skip over a lot of the very basics with AS3 in general and Starling in specific.

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

**Updated 8/9/13 added the ZFLoader.as class and commentary since I removed the old posts from my old engine series.

If you’re a beginner, I would encourage you to use the resources I myself used to learn both AS3 (over the last 6ish years) and Starling (over the past 2 months):

Goals

I really want this stressed heavily up front. I am not a designer. My photoshop skills extend about as far as T-Rex arms. I am a developer that loves to dabble in Photoshop. This does not make my dabbles “good.” My lady is the artsy designer type and has not had anything to do with this tutorial series or game demo except to mock my dropshadow/glow SKILLZ! The point of this demo is to demonstrate the following codemonkey goals. This demo is not really meant to be something fun to play yet. Minimal balancing has been done. This is simply getting the code chops together and tutorials done so that you can learn a few things and take this knowledge (and even code) and build something that IS fun and is an actual enjoyable game experience. I will be taking this very code and using it to start a TD game myself (with the lady actually making things pretty and stylish).

Please be kind regarding the tragic state of my UI. It’s functional, damnit! πŸ˜€

In creating this tutorial, I had a few goals that I wanted to achieve from this game demo tutorial. And I have accomplished nearly all of them and Will accomplish these by the last tutorial.

Topics Covered

In this post I’ll cover:

Game Project Structure

Every project has its own structure. I haven’t come across any real “Best Practices” when it comes to structuring a game project in AS3. A lot of tutorials show snippets of game projects, or small classes but in a very loose (if not non-existent) OOP structure.

Here’s the general structure of my project inside the src/ folder and some general notes on why:

There are many other folders inside the src/ folder but those are all 3rd party libs that I’ll point out eventually.

Notes Before We Begin

I’ve tried to copy the code in the examples straight from the code you can find where it is hosted at BitBucket – ZFStarlingTutorial. For formatting purposes I have shortened tabs to 3 spaces and there’s a good chance that I may post functions and code out of the order in which they actually appear in the file to better group together functions that share the topic I’m discussing. Feel free to check out that whole repo to see the code in its (sometimes-changing) entirety. Also, some code shown may have lines missing from the core codebase found at the repo because what I’m discussing in the tutorial may have nothing to do with those lines of code, so I may remove them to try to keep this massive beast as fairly concise as possible.

Let’s Do This!

One of the things that tripped me up the hardest when making the transition to Starling from regular AS3 was External Loading. I got a trial subscription to Lynda to check out Lee Brimlow’s Starling game tutorial. That tutorial had everything being [Embed] tag loaded. There was no way to handle progress or really manage what you’re loading at that point. It was great for the tutorial, and that single series really jumpstarted my Starling skills and got me rolling along. But hey, we’re fancy developers wanting to make hardcore games right? Right?! So we need a way to load assets, to get the progress of those loading assets, and other fancy things like that.

I’ve been a huge fan of GreenSock’s TweenMax/Lite library since way back, and any chance I get to include some of his libs into my code, I go for it. Why reinvent the wheel right? LoaderMax is his loading lib that he’s created, and it’s a huge time-saver. I wrote a wrapper called ZFLoader that basically is like a loader manager sort of class. I will explain the ZFLoader below.

Game.as

com/zf/core/Game.as is the core class that begins the Game. It holds the game-wide ZFLoader instance, SharedObjectManager instance, SoundManager instance, and handles State for the whole game demo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public function Game() {
   super();
   soundMgr = new SoundManager();		
   addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
}
 
private function onAddedToStage(evt:Event):void {
   removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
 
   so.init();
 
   // set the initial state
   changeState(GAME_LOAD_STATE);
 
   _addUpdateListener();
}
 
public function changeState(state:int):void {
   var removed:Boolean = false;
 
   if(currentState != null) {
      _removeUpdateListener();
      currentState.destroy();
      removeChild(Sprite(currentState));
      currentState = null;
      removed = true;	
   }
 
   switch(state) {
      case GAME_LOAD_STATE:
         currentState = new GameLoad(this);
         break;
 
      case MENU_STATE:
         currentState = new Menu(this);
         break;
 
      case MAP_SELECT_STATE:
         currentState = new MapSelect(this);
         break;
 
      case GAME_LOAD_STATE:
         currentState = new GameLoad(this);
         break;
 
      case MAP_LOAD_STATE:
         currentState = new MapLoad(this);
         break;
 
      case PLAY_STATE:
         currentState = new Play(this);
         break;
 
      case GAME_OVER_STATE:
         currentState = new GameOver(this);
         break;
   }
 
   addChild(Sprite(currentState));
 
   if(removed) {
      // Add update listeners back
      _addUpdateListener();
      removed = false;
   }
}

GameLoad.as

com/zf/states/GameLoad.as is implements IZFState, which is just a simple implementation I created to specify that classes implementing IZFState will all have update() and destroy() functions. The GameLoad State handles loading my initial game assets, sound assets, etc. Anything that I need to have from Frame 1 until the user closes their browser gets handled here. This includes non-map-specific sounds, button textures, fonts, backgrounds, and further JSON data files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public function GameLoad(game:Game) {
   _game = game;
   addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
}
 
private function onAddedToStage(evt:Event):void {
   removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
 
   _progBar = new ProgressBar(500, 35);
   _progBar.x = 150;
   _progBar.y = 400;
   addChild(_progBar);
 
   Game.zfLoader.onProgress.add(onProgress);
   Assets.onInitialLoadComplete.add(onLoadComplete);
 
   Assets.loadInitialAssets();
}
 
public function onProgress(ratio:Number):void {
   _progBar.ratio = ratio;
}
 
public function onLoadComplete():void {
   Game.zfLoader.onProgress.remove(onProgress);
   Assets.onInitialLoadComplete.remove(onLoadComplete);
 
   Assets.init();
   _progBar.removeFromParent(true);
   _progBar = null;
 
   _game.changeState(Game.MENU_STATE);
}

I really wish I had saved or bookmarked wherever I got the ProgressBar code from. I know I copy/pasted it from some example somewhere, but I don’t remember now where it was from. Apologies for not giving credit where it’s due.

A quick note about the changeState function, were this going to be a more robust engine (as you might build on top of this code), you may or may not want to add valid States that a State may or may not change to. For example, from this GameLoad state, I would not want to allow myself to ever try to _game.changeState(Game.GAME_OVER_STATE); I shouldn’t be able to switch to a GameOver State, I haven’t even played the game yet nor even seen a title menu! So in my State classes I might have an allowableStates array or something similar just to help ensure I don’t accidentally try to jump to a state I shouldn’t be able to get to.

For more on State Machines and AS3:

ZFLoader.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package com.zf.loaders
{
   import com.greensock.events.LoaderEvent;
   import com.greensock.loading.DataLoader;
   import com.greensock.loading.ImageLoader;
   import com.greensock.loading.LoaderMax;
   import com.greensock.loading.MP3Loader;
   import com.greensock.loading.XMLLoader;
   import com.greensock.loading.display.ContentDisplay;
   import com.zf.core.Config;
   import com.zf.utils.FileUtils;
 
   import org.osflash.signals.Signal;
 
public class ZFLoader
{
   private var className:String = 'ZFLoader';
 
   private static var instance	 : ZFLoader;
   private static var allowInstantiation	: Boolean;
 
   public var queue:LoaderMax;
   public var onProgress:Signal;
   public var onComplete:Signal;
 
   private var callBackArray:Array;
 
   public static function getInstance():ZFLoader {
      if (instance == null) {
         allowInstantiation = true;
         instance = new ZFLoader();
         allowInstantiation = false;
      }
      return instance;
   }
 
   public function ZFLoader() {
      if (!allowInstantiation) {
         throw new Error("Error: Instantiation failed: Use ZFLoader.getInstance() instead of new.");
      } else {
         initQueue();
      }
   }
 
   private function initQueue():void  {
      onProgress = new Signal();
      onComplete = new Signal();
      queue =  new LoaderMax({
         name:"mainQueue",
         onProgress:onProgressHandler,
         onComplete:onCompleteHandler,
         onError:onErrorHandler,
         autoLoad:true
      });
      callBackArray = [];
   }
 
   public function addToLoad(path:String, cb:Function, id:String = '', includeAssetPath:Boolean = true, startQueueLoad:Boolean = true, opts:Object = null):void  {
      var fileName:String = FileUtils.getFilenameFromPath(path),
          ext:String = FileUtils.getExtFromFilename(path);
      // if id was not supplied, use the filename minus extension
      if(id == '') {
         id = fileName;
      }
      // Set up the fullPath for the asset
      var fullPath:String = FileUtils.getFullPath(path, includeAssetPath, ext);
 
      var useRawContent:Boolean = false;
      if(ext == Config.IMG_JPG
         || ext == Config.IMG_GIF
         || ext == Config.IMG_PNG) 
      {
         useRawContent=true;
      }
 
      // handle callback queue
      callBackArray.push({
         'fileName': fileName,
         'cb': cb,
         'id': id,
         'useRawContent': useRawContent,
         'options': opts
      });
 
      Config.log(className, 'addToLoad', "Adding callback function for " + path + " to callBackArray index: " + (callBackArray.length - 1 ).toString());
      Config.log(className, 'addToLoad', "FileName: " + fileName + " || FullPath: " + fullPath + " || ext: " + ext);
 
      switch(ext) {
         // EXTENSIONS
         case Config.DATA_XML:
         case Config.DATA_PEX:
            queue.append(new XMLLoader(fullPath, {'name':id}));
            break;
 
         case Config.IMG_JPG:
         case Config.IMG_PNG:
         case Config.IMG_GIF:
            queue.append(new ImageLoader(fullPath, {'name':id}));
            break;
 
         case Config.DATA_JSON:
         case Config.DATA_FNT:
            queue.append(new DataLoader(fullPath, {'name':id}));
            break;
 
         case Config.DATA_MP3:
            queue.append(new MP3Loader(fullPath, {'name':id, 'autoPlay': false}));
            break;
      }
 
      if(startQueueLoad) {
         queue.load();
      }
   }
 
   public function addCallbackToQueueEvent(eventName:String, callback:Function):void {
      queue.addEventListener(eventName, callback);
   }
 
   public function removeCallbackToQueueEvent(eventName:String, callback:Function):void {
      queue.removeEventListener(eventName, callback);
   }
 
   public function onCompleteHandler(event:LoaderEvent):void {
      Config.log(className, 'onCompleteHandler', "Beginning to process " + event.target);
 
      // Array of Objects used to temporarily store things that have not fully been loaded yet
      // and are not ready for callback
      var nextPassArray:Array = [];
      // item will hold the data from callBackArray that we're processing
      var item:Object;
 
      while(callBackArray.length > 0)  {
         // remove the item from the array so we dont try to process it again
         // save it in item so we can process it there
         item = callBackArray.shift();
 
         Config.log(className, 'onCompleteHandler', "Processing fileName: " + item.id);
 
         // holds the content we've just loaded. may be an image (ContentDisplay) or xml or other data type
         var contentData:* = queue.getContent(item.id);
 
         // if contentData has not loaded yet, save the item and continue
         if((contentData is ContentDisplay && contentData.rawContent == undefined) || contentData == undefined)
         {
            Config.log(className, 'onCompleteHandler', "Moving " + item.id + " to the Next Pass");
            nextPassArray.push(item);
         }
         else
         {
            var data:*;
            if(item.useRawContent) {
               data = contentData.rawContent;
            } else {
               data = contentData;
            }
            item.cb(new CallBackObject(item.id, data, item.options));
         }
      }
      callBackArray = nextPassArray;
 
      if(callBackArray.length == 0) {
         onComplete.dispatch();
         Config.log(className, 'onCompleteHandler', event.target + " is complete!");
      }
   }
 
   public function onProgressHandler(event:LoaderEvent):void {
      Config.log(className, 'onProgressHandler', "progress: " + event.target.progress);
      onProgress.dispatch(event.target.progress);
   }
 
   public function onErrorHandler(event:LoaderEvent):void {
      Config.logError(className, 'onErrorHandler', "error occured with " + event.target + ": " + event.text);
   }
}
}

That’s the loading process. Now let’s go look at Assets and see what’s happening there.

Assets.as

com/zf/core/Assets.as is the core class that handles my demo’s art, sound, font, and JSON data assets. In mobile dev, this might be your R (Resources) class. Right off the bat, you’ll notice it’s filled with loads of public static var‘s. Yup. Before I cleaned this file up, I actually had double the number of variables because I was loading in the jpg/png assets into a Bitmap variable, then later in init() I was converting them to a Texture class. Thankfully I realized that I could skip the duplicate memory overhead and we’ll see in a bit how I saved some RAM.

If you scroll up just a bit, you’ll remember that from GameLoad.as, when that State gets added to stage, it calls Assets.loadInitialAssest(). It’s a very short function:

1
2
3
4
public static function loadInitialAssets():void {
   Game.zfLoader.addToLoad('initialAssets.json', onInitialAssetsLoadComplete);
   Game.zfLoader.addToLoad('sounds.json', onInitialSoundsLoadComplete);
}

I have two main initial files that act as a sort of “manifest” of assets to load. I’m experimenting with TreeFortress’ SoundAS sound manager and it has its own way to load sounds into its internal variables. SoundAS has a load() function and it will load your sound assets for you! However, at this time, it only has an onSoundLoadComplete and an onSoundLoadError Signal that it dispatches. They have not added any onSoundProgress type Signal, so I had no easy way to tie in my progress bar with SoundAS loading my assets. So, I’ll simply load them myself here then stuff them into SoundAS.

initialAssets.json and sounds.json (found at src/assets/initialAssets.json & src/assets/sounds.json) are simple JSON files that look like this:

initialAssets.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
   "files": [
      {
         "id": "logo",
         "convertToTexture": true,
         "file": "assets/images/logo.png"
      },
	  {
         "id": "atlas",
         "file": "assets/images/atlas.png"
      },
      {
         "id": "atlasXML",
         "file": "assets/images/atlas.xml"
      }
}

sounds.json

1
2
3
4
5
6
7
8
9
10
11
{
   "files": [
      {
         "id": "ding1",
         "file": "assets/sounds/ding1.mp3"
      },
      {
         "id": "escape1",
         "file": "assets/sounds/escape1.mp3"
      }
}

These are simply files that consist more or less of an array of “id” and “file” metadata. In initialAssets you’ll also see the extra “convertToTexture” key on several of the items, but not all. This is part of my optimization to not have to keep Bitmap versions of the files.

When initialAssets.json is done loading, it calls onInitialAssetsLoadComplete(). Likewise, when sounds.json finishes, it calls onInitialSoundsLoadComplete():

From Assets.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public static function onInitialAssetsLoadComplete(cbo:CallBackObject):void {
   var initAssets:Object = Utils.JSONDecode(cbo.data);
   _itemsToLoad = initAssets.files.length;
   var obj:Object;
   for(var i:int = 0; i < _itemsToLoad; i++) {
      obj = initAssets.files[i];
      Game.zfLoader.addToLoad(obj.file, onAssetLoadComplete, obj.id, false, false, obj)
   }
   Game.zfLoader.queue.load();
}
 
public static function onInitialSoundsLoadComplete(cbo:CallBackObject):void {
   var sndAssets:Object = Utils.JSONDecode(cbo.data);
   _soundsToLoad = sndAssets.files.length;
 
   var obj:Object;
   for(var i:int = 0; i < _soundsToLoad; i++) {
      obj = sndAssets.files[i];
      Game.zfLoader.addToLoad(obj.file, onSoundAssetLoadComplete, obj.id, false, false)
   }
   Game.zfLoader.queue.load();
}
 
public static function onAssetLoadComplete(cbo:CallBackObject):void {
   _itemsToLoad--;
   Config.log('Assets', 'onAssetLoadComplete', 'LoadComplete: ' + cbo.name + " -- _itemsToLoad: " + _itemsToLoad);
 
   if(cbo.options != null 
      && cbo.options.hasOwnProperty('convertToTexture') && cbo.options.convertToTexture) 
   {
      // Add a T to the end of the file id, and auto convert it from bitmap
      Assets[cbo.name + 'T'] = Texture.fromBitmap(cbo.data);
   } 
   else 
   {
      Assets[cbo.name] = cbo.data;
   }
 
   if(_itemsToLoad == 0) {
      _itemLoadComplete = true;
      _checkReady();
   }
}
 
public static function onSoundAssetLoadComplete(cbo:CallBackObject):void {
   _soundsToLoad--;
   Config.log('Assets', 'onSoundAssetLoadComplete', 'LoadComplete: ' + cbo.name + " -- Size: " + cbo.data.bytesTotal + " -- _soundsToLoad: " + _soundsToLoad);
   Game.soundMgr.addSound(cbo.name, cbo.data);
 
   if(_soundsToLoad == 0) {
      _soundLoadComplete = true;
      _checkReady();
   }
}
 
public static function init():void {
   ta = new TextureAtlas(Texture.fromBitmap(atlas), XML(atlasXML));
 
   TextField.registerBitmapFont(new BitmapFont(Texture.fromBitmap(calistoMT), XML(calistoMTXML)));
   TextField.registerBitmapFont(new BitmapFont(Texture.fromBitmap(wizztaB), XML(wizztaXML)));
 
   towerData = Utils.JSONDecode(towerDataJSON);
   mapData = new MapData(Utils.JSONDecode(mapSelectJSON));
}

Before my Madden-esque play-by-play (line-by-line) breakdown of this block of code, a quick note about the CallBackObject class. It can be found in com/zf/loaders/CallBackObject.as and it is simply a ‘struct’ sort of data structure. I just wanted a very clearly-defined class that would have a name, data, and options member so I could call them later. BOOM! Back in the game…

Wrapping Up

Whew! We’ve finished loading all the assets we’ll need to actually start the game. There is still some more loading after a user has selected a map to play, but it’s only map-specific data. Tile data and enemy wave stuff. All of our sounds, spritesheets, fonts, and a vast majority of the art assets and heavy lifting background-sized jpg/png files have been loaded here. It’s smooth sailing with hopefully a very limited amount of loading/processing in the future so the Player can jump straight into maps and start killing badguys!

This was probably the most boring way to kick off this tutorial series. You know you came for Tower and Enemies! That’s the real meat of a TD game. Soon…. soon… I’m glad we got this out of the way because it was tricky trying to learn Starling and figure out how to use LoaderMax to load assets and get it to play nice with Starling. But now that that’s done… we’ll get into the good stuff next time!

Again, 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

Share

AS3 Starling Tower Defense Tutorial – Part 2 – Game States

In my previous post on External Loading using LoaderMax in Starling, I began going through the first State of the game, the GameLoad State. This post will go through each game state in detail as to why it’s there and what it does. Also, I show off and talk about a few UI components that I’ve created as they come up in the various States.

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

**Updated 8/9/2013 – added the Upgrade State

So, the following game states can currently be found in the com/zf/states/ folder:

  • GameLoad – the very first state! This is in charge of loading all of the initial assets and sounds and was outlined in the previous post
  • GameOver – this state provides post-battle/map feedback to players. Did they win? Lose? How many enemies did they kill? etc…
  • MapLoad – this state is in charge of loading map-specific assets which I’ll cover later
  • MapSelect – this state displays whatever “Map/Level Select” info, it also holds the Upgrades button to take you to the Upgrades State (when finished)
  • Menu – this state is your main Title/Menu type state. It’s the first screen the player gets to after the main game loads
  • Play – this state holds the actual action. It’s the biggest and most complex of the states.
  • Test – this is just a dummy state I created when I wanted just a blank IZFState Sprite to load to. It provides a quick way to jump straight to an empty screen so you can test UI components by themselves without having to go all the way through all the screens
  • Upgrades – this state allows the player to upgrade various stats and such

So first off, having a separate game state basically gives me a way to compartmentalize “scenes” or “screens” or whatever you want to call them; States. I gave a description of each state above so hopefully you have a better idea of how I’ve grouped my States. Again, since GameLoad was discussed last post, I’m going to skip it here.

GameOver.as

The following are the only really relevant functions in the State. As we saw from GameLoad, in the constructor we keep a reference to the main Game object, and add an event listener so we know when this State gets added to the stage. At that point, onAddedToStage runs and really sets up the state. This is the pattern I’ve used for all of the states.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private function onAddedToStage(evt:Event):void {
   removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
 
   _gameStateBkgd = new GameStateBkgd('Game Over');
   addChild(_gameStateBkgd);
 
   _tryAgainBtn = new Button(Assets.tryAgainBtnT);
   _tryAgainBtn.x = 500;
   _tryAgainBtn.y = 400;
   _tryAgainBtn.addEventListener(Event.TRIGGERED, _onTryAgainTriggered);
   addChild(_tryAgainBtn);
 
   _gameStatsText = new ScrollText();
   _gameStatsText.x = 100;
   _gameStatsText.y = 120;
   _gameStatsText.width = 200;
   _gameStatsText.height = 400;
   _gameStatsText.isHTML = true;
   addChild(_gameStatsText);
   _gameStatsText.text = Config.totals.toHtml();
}
 
private function _onTryAgainTriggered(evt:Event):void {
   _game.changeState(Game.MAP_SELECT_STATE);
}

GameStateBackground component

The whole file? Yup… imports and all! So I got tired of adding an Image to hold the background, and a TextField to hold the title on every State, so being the resourceful and OOP-minded codemonkey that I am, I pulled that code into its own class to reuse it. Genius!… unless you’ve taken even one CompSci 101 course and then this is just basic stuff.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.zf.ui.gameStateBkgd {
   import com.zf.core.Assets;
 
   import starling.events.Event;
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.text.TextField;
 
   public class GameStateBkgd extends Sprite
   {
      private var _bkgd:Image;
      private var _title:TextField;
      private var _titleText:String;
 
      public function GameStateBkgd(titleText:String) {
         _titleText = titleText;
         addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
      }
 
      public function onAddedToStage(e:Event):void {
         removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
 
         _bkgd = new Image(Assets.gameStateBkgdT);
         addChild(_bkgd);
 
         _title = new TextField(700, 125, _titleText, "Wizzta", 100, 0xCC2323, true);
         _title.pivotX = 0;
         _title.pivotY = 0;
         _title.x = -50;
         _title.y = -10;
         addChild(_title);
      }
 
      public function destroy():void {
         _bkgd.removeFromParent(true);
         _bkgd = null;
 
         _title.removeFromParent(true);
         _title = null;
      }
   }
}

MapLoad

So, GameLoad loads the main game at the very beginning. Why have a MapLoad? What does it do?

There are arguments for and against this State even being here. Wait, let me back up. So, the GameLoad State exists to run after the player selects a Map to play. So Player clicks on map1’s icon, that icon sets Config.selectedMap and tells Game to changeState to MapLoad. As we’ll see in the code, MapLoad then takes Config.selectedMap and loads a specific map JSON file. The map JSON file contains all of the metadata about map1 including number of rows and columns, tile width/height (so it can change map to map), Enemy wave data, enemy groups and types, and all of the tile data needed to generate the map.

So, there are arguments for and against this State even being here. Why not load all the map JSON files in at the very beginning with GameLoad to save the user some time for each map? Sure, I could. You could pretty easily update the code to load all that stuff during GameLoad and do away with this State altogether. The flipside of this argument is that I’ve got roughly 35Kb+ worth of JSON data in one of those files. Times however many maps I have. Say even at 10 maps, you’re looking at a over a third of a Mb of raw data parsing into Objects that are residing in memory which the user may never even need. This way, only the current map that’s about to be played gets loaded into memory.

A great compromise between these two sides would be to load the map’s JSON file only when the user selects to play that map and then Save that map JSON object somewhere. That way if the user finishes the map and clicks try again and selects the same map, you have already loaded the file once, the map JSON object is still “cached” somewhere in your code, so just reuse it. I just thought about this right now and I like it. But, I didn’t write the code that way. I wrote it to load the map JSON file every time. See, here’s a way you can already make your own version better!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private function onAddedToStage(evt:Event):void {
   removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
 
   _gameStateBkgd = new GameStateBkgd('Loading...');
   addChild(_gameStateBkgd);
 
   _progBar = new ProgressBar(500, 35);
   _progBar.x = 150;
   _progBar.y = 400;
   addChild(_progBar);
 
   _playBtn = new Button(Assets.playMapBtnT);
   _playBtn.pivotX = _playBtn.width >> 1;
   _playBtn.x = 400;
   _playBtn.y = 450;
   _playBtn.alpha = 0.2;
   addChild(_playBtn);
 
   Game.zfLoader.onProgress.add(onProgress);
   Game.zfLoader.onComplete.add(onLoadComplete);
 
   // Load the map data
   var map:String = Config.PATH_JSON + 'maps/' + Config.selectedMap + '.json';
   Game.zfLoader.addToLoad(map, handleMapData, Config.selectedMap, false);
}
 
public function onLoadComplete():void {
   Game.zfLoader.onProgress.remove(onProgress);
   Game.zfLoader.onComplete.remove(onLoadComplete);
   Config.log('GameLoad', 'onLoadComplete', "MapLoad Complete");
   _playBtn.alpha = 1;
   _playBtn.addEventListener(Event.TRIGGERED, _onPlayMapTriggered);
}
 
public function onProgress(ratio:Number):void {
   Config.log('MapLoad', 'onProgress', "LOADING :: " + ratio * 100);
   _progBar.ratio = ratio;
}
 
public function handleMapData(cbo:CallBackObject):void {
   Config.currentMapData = Utils.JSONDecode(cbo.data);
}
 
private function _onPlayMapTriggered(evt:Event):void {
   _game.changeState(Game.PLAY_STATE);
}
 
public function destroy():void {
   _gameStateBkgd.destroy();
   _gameStateBkgd = null;
 
   _progBar.removeFromParent(true);
   _progBar = null;
 
   _playBtn.removeEventListener(Event.TRIGGERED, _onPlayMapTriggered);
   _playBtn.removeFromParent(true);
   _playBtn = null;
}

MapSelect.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public function onAddedToStage(e:Event):void {
   removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
 
   _gameStateBkgd = new GameStateBkgd('Map Select');
   addChild(_gameStateBkgd);
 
   _map1 = new MapSelectIcon(Assets.mapData.getMapById('map1'), 'misc/mapSelect_map1');
   _map1.x = 100;
   _map1.y = 120;
   _map1.onClick.add(onMapSelectClicked);
   _map1.onHover.add(onMapSelectHovered);
   addChild(_map1);
 
   _map2 = new MapSelectIcon(Assets.mapData.getMapById('map2'), 'misc/mapSelect_map2');
   _map2.x = 100;
   _map2.y = 300;
   _map2.onClick.add(onMapSelectClicked);
   _map2.onHover.add(onMapSelectHovered);
   addChild(_map2);
 
   _mapText = new ScrollText();
   _mapText.x = 95;
   _mapText.y = 475;
   _mapText.width = 400;
   _mapText.height = 200;
   _mapText.isHTML = true;
   addChild(_mapText);
 
   _gameStatsText = new ScrollText();
   _gameStatsText.x = 400;
   _gameStatsText.y = 120;
   _gameStatsText.width = 200;
   _gameStatsText.height = 400;
   _gameStatsText.isHTML = true;
   addChild(_gameStatsText);
   _gameStatsText.text = Config.currentGameSOData.toHtml();
 
   _upgradesBtn = new Button(Assets.upgradesBtnT);
   _upgradesBtn.x = 525;
   _upgradesBtn.y = 425;
   _upgradesBtn.addEventListener(Event.TRIGGERED, onUpgradesBtnClicked);
   addChild(_upgradesBtn);
}
 
private function onUpgradesBtnClicked(evt:Event):void {
   _game.changeState(Game.UPGRADES_STATE);
}
 
public function onMapSelectClicked(map:Object):void {
   Config.addGameAttempted();
   Config.selectedMap = map.id;
   _game.changeState(Game.MAP_LOAD_STATE);
}
 
public function onMapSelectHovered(map:Object):void {
   if(map.id != _activeMapId) {
      _activeMapId = map.id;
      _activeMap = map;
      _updateMapText();
   }
}
 
private function _updateMapText():void {
   var fontOpenTag:String = '<font color="#FFFFFF" size="22">';
   var fontCloseTag:String = '</font>';
   var txt:String = fontOpenTag + 'Map Title: ' + _activeMap.title + fontCloseTag + '<br>' + fontOpenTag + 'Desc: ' + _activeMap.desc + fontCloseTag; 
   _mapText.text = txt;
}

MapSelectIcon.as

com.zf.ui.mapSelectIcon.MapSelectIcon.as is a component I wrote to hold map data, an image, a textfield, and some logic. I pass in the map data Object, and a texture name to the constructor to kick things off.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.zf.ui.mapSelectIcon
{
   import com.zf.core.Assets;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.events.Touch;
   import starling.events.TouchEvent;
   import starling.events.TouchPhase;
   import starling.text.TextField;
 
public class MapSelectIcon extends Sprite
{
   public var onHover:Signal;
   public var onClick:Signal;
 
   private var _icon:Image;
   private var _data:Object;
   private var _tf:TextField;
 
   public function MapSelectIcon(data:Object, textureName:String) {
      _data = data;
 
      _icon = new Image(Assets.ta.getTexture(textureName));
      addChild(_icon);
 
      _tf = new TextField(200, 60, _data.title, 'Wizzta', 32, 0xFFFFFF);
      _tf.x = -45;
      _tf.y = 100;
      addChild(_tf);
 
      onHover = new Signal(Object);
      onClick = new Signal(Object);
 
      addEventListener(TouchEvent.TOUCH, _onTouch);
   }
 
   public function destroy():void {
      onHover.removeAll();
      onClick.removeAll();
 
      _icon.removeFromParent(true);
      _icon = null;
 
      _tf.removeFromParent(true);
      _tf = null;
 
      removeFromParent(true);
   }
 
   private function _onTouch(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(this);
      if(touch) {
         switch(touch.phase) {
               case TouchPhase.BEGAN:
                  onClick.dispatch(_data);
                  break;
 
               case TouchPhase.HOVER:
                  onHover.dispatch(_data);
                  break;
            }
         }
      }
   }
}

Menu.as

The Menu State is my “Title” main menu type screen where the user is presented with my fancy logo and new/load game options.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private function onAddedToStage(evt:Event):void {
   removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
 
   _bkgd = new Image(Assets.bkgdT);
   addChild(_bkgd);
 
   _logo = new Image(Assets.logoT);
   _logo.x = 75;
   _logo.y = 75;
   addChild(_logo);
 
   _game1 = new LoadGameButton('Game 1', 'game1');
   _game1.x = 50;
   _game1.y = 450;
   _game1.onGameSelected.add(_onGameSelected);
   addChild(_game1);
 
   _game2 = new LoadGameButton('Game 2', 'game2');
   _game2.x = 300;
   _game2.y = 450;
   _game2.onGameSelected.add(_onGameSelected);
   addChild(_game2);
 
   _game3 = new LoadGameButton('Game 3', 'game3');
   _game3.x = 525;
   _game3.y = 450;
   _game3.onGameSelected.add(_onGameSelected);
   addChild(_game3);
}
 
private function _onGameSelected():void {
   _game.changeState(Game.MAP_SELECT_STATE);
}

LoadGameButton.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
public class LoadGameButton extends Sprite
{
   public var onGameSelected:Signal;
 
   private var _gameNameTF:TextField;
   private var _gameName:String;
   private var _gameId:String;
   private var _gameData:ZFGameData;
   private var _texture:Texture;
   private var _gameExists:Boolean;
   private var _loadGameBtn:Button;
   private var _deleteGameBtn:Button;
 
   public function LoadGameButton(gameName:String, gameId:String)  {
      _gameName = gameName;
      _gameId = gameId;
 
      var o:Object = Game.so.getGameData(_gameId);
      _gameExists = !_isEmptyGameDataObject(o);
 
      if(_gameExists) {
         _gameData = ZFGameData.fromObject(o);
         _texture = Assets.loadGameBtnT;
      } else {
         _texture = Assets.newGameBtnT;
      }
 
      onGameSelected = new Signal();
 
      _loadGameBtn = new Button(_texture);
      _loadGameBtn.addEventListener(Event.TRIGGERED, _onClick);
      addChild(_loadGameBtn);
 
      _gameNameTF = new TextField(150, 30, _gameName, 'Wizzta', 40, 0xFFFFFF);
      _gameNameTF.y = -40;
      addChild(_gameNameTF);
 
      if(_gameExists) {
         _addDeleteGameBtn();
      }
   }
 
   private function _addDeleteGameBtn():void {
      _deleteGameBtn = new Button(Assets.deleteGameBtnT);
      _deleteGameBtn.x = 130;
      _deleteGameBtn.y = 105;
      addChild(_deleteGameBtn);
      _deleteGameBtn.addEventListener(Event.TRIGGERED, _onDeleteGameBtnClicked);
   }
 
   private function _onClick(evt:Event):void {
      setCurrentGameData();
      onGameSelected.dispatch();
   }
 
   public function setCurrentGameData():void {
      Config.currentGameSOID = _gameId;
      if(_gameExists) {
         Config.currentGameSOData = _gameData;
      } else {
         Game.so.createGameData(Config.currentGameSOID, true);
      }
   }
 
   public function destroy():void {
      _gameNameTF.removeFromParent(true);
      _gameNameTF = null;
      _loadGameBtn.removeEventListener(Event.TRIGGERED, _onClick);
      onGameSelected.removeAll();
 
      if(_deleteGameBtn) {
         _removeDeleteGameBtn();
      }
   }
 
   private function _removeDeleteGameBtn():void {
      _deleteGameBtn.removeFromParent(true);
      _deleteGameBtn.removeEventListener(Event.TRIGGERED, _onDeleteGameBtnClicked);
      _deleteGameBtn = null;
   }
 
   private function _onDeleteGameBtnClicked(evt:Event):void {
      Game.so.createGameData(_gameId, true);
      _removeDeleteGameBtn();
      _loadGameBtn.removeFromParent(true);
      _loadGameBtn.removeEventListener(Event.TRIGGERED, _onClick);
      _loadGameBtn = new Button(Assets.newGameBtnT);
      addChild(_loadGameBtn);
      _loadGameBtn.addEventListener(Event.TRIGGERED, _onClick);
   }
 
   private function _isEmptyGameDataObject(obj:Object):Boolean {
      var isEmpty:Boolean=true;
 
      for (var s:String in obj) {
         isEmpty = false;
         break;
      }
 
      // If the object has data, see if it has Relevant data
      if(!isEmpty && obj.mapsAttempted == 0) {
         isEmpty = true;
      }
      return isEmpty;
   }
}

Test.as

Test is just a basic shell of a IZFState. This allows me to go straight from GameLoad and changeState to Test, instead of going to Menu. I can add any new UI components or whatever here so I can see them on the stage and test them. This came in really handy when I was working on the in-game GameOptionsPanel component that you can see if you actually go through the menus, start a game, then click the options button. The Test State makes things really fast and easy to test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test extends Sprite implements IZFState
{
   private var _game:Game;
 
   public function Test(g:Game)
   {
      _game = g;
      addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }
 
   private function onAddedToStage(evt:Event):void {
      removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }
 
   public function update():void {
   }
 
   public function destroy():void {
   }
}

And finally we get to see the Upgrade state!

Upgrade.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package com.zf.states
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.ui.gameStateBkgd.GameStateBkgd;
   import com.zf.ui.upgradeOption.UpgradeOption;
   import com.zf.ui.upgradeOption.UpgradeOptionVO;
 
   import starling.display.Button;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.text.TextField;
 
public class Upgrades extends Sprite implements IZFState
{
   private var _game:Game;
   private var _mapSelectBtn:Button;
   private var _gameStateBkgd:GameStateBkgd;
   private var _upgradeOptions:Array;
   private var _ptsTotal:int;
   private var _ptsSpent:int;
   private var _ptsAvail:int;
   private var _labelTF:TextField;
   private var _ptsTF:TextField;
   private var _resetBtn:Button;
 
   public function Upgrades(game:Game) {
      _game = game;
      addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }
 
   public function onAddedToStage(evt:Event):void {
      removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
 
      var upgrades:Object = Game.so.getGameDataProperty('upgrades');
 
      _ptsTotal = upgrades.ptsTotal;
      _ptsSpent = upgrades.ptsSpent;
      _ptsAvail = _ptsTotal - _ptsSpent;
 
      _gameStateBkgd = new GameStateBkgd('Upgrades');
      addChild(_gameStateBkgd);
 
      _labelTF = new TextField(200, 35, 'Available Points', 'Wizzta', 30, 0xFFFFFF);
      _labelTF.x = 550;
      _labelTF.y = 90;
      addChild(_labelTF);
 
      _ptsTF = new TextField(200, 35, _ptsAvail.toString(), 'Wizzta', 30, 0xFFFFFF);
      _ptsTF.x = 650;
      _ptsTF.y = 90;
      addChild(_ptsTF);
 
      if(_ptsTotal > 0) {
         _resetBtn = new Button(Assets.resetUpgradesBtnT);
         _resetBtn.x = 700;
         _resetBtn.y = 135;
         _resetBtn.addEventListener(Event.TRIGGERED, onResetBtnClicked);
         addChild(_resetBtn);
      }
      _upgradeOptions = [];
 
      _mapSelectBtn = new Button(Assets.mapSelectBtnT);
      _mapSelectBtn.x = 525;
      _mapSelectBtn.y = 450;
      _mapSelectBtn.addEventListener(Event.TRIGGERED, onMapSelectBtnClicked);
      addChild(_mapSelectBtn);
 
      addUpgradeOptions([
         upgrades.towerSpd,
         upgrades.towerRng,
         upgrades.towerDmg
      ]);
   }
 
   public function addUpgradeOptions(opts:Array):void {
      var len:int = opts.length,
          uo:UpgradeOption,
          vo:UpgradeOptionVO,
          shouldEnable:Boolean = (_ptsAvail > 0);
 
      for(var i:int = 0; i < len; i++) {
         vo = new UpgradeOptionVO(opts[i]);
         uo = new UpgradeOption(vo.id, vo.label, vo.totalRanks, vo.bonusPerRank, shouldEnable);
         uo.x = 100;
         uo.y = 200 + (100*i);
         addChild(uo);
         uo.currentRanks = vo.currentRanks;
         _upgradeOptions.push(uo);
      }
 
      if(shouldEnable) {
         toggleUpgradeOptions(true);
      }
   }
 
   public function toggleUpgradeOptions(enable:Boolean):void {
      var len:int = _upgradeOptions.length;
      for(var i:int = 0; i < len; i++) {
         if(enable) {
            _upgradeOptions[i].optionChanged.add(onOptionChange);
            _upgradeOptions[i].enable();
         } else {
            _upgradeOptions[i].optionChanged.remove(onOptionChange);
            _upgradeOptions[i].disable();
         }
      }
   }
 
   public function resetUpgradeOptionsToZero():void {
      var len:int = _upgradeOptions.length;
      for(var i:int = 0; i < len; i++) {
         _upgradeOptions[i].enable()
         _upgradeOptions[i].currentRanks = 0;
         // since we created a new button, have to re-add listener
         _upgradeOptions[i].optionChanged.add(onOptionChange);
      }
   }
 
   public function onOptionChange(addedRank:Boolean, opt:Object):void {
      if(addedRank) {
         _ptsSpent++;
      } else {
         _ptsSpent--;
      }
      _ptsAvail = _ptsTotal - _ptsSpent;
 
      _updatePtsAvailTF();
 
      Config.currentGameSOData.updateFromUpgrades(opt, _ptsTotal, _ptsSpent, _ptsAvail, true);
      if(_ptsAvail <= 0) {
         toggleUpgradeOptions(false);
      }
   }
 
   public function onResetBtnClicked(e:Event):void {
      _ptsSpent = 0;
      _ptsAvail = _ptsTotal;
      resetUpgradeOptionsToZero();
      _updatePtsAvailTF();
   }
 
   private function _updatePtsAvailTF():void {
      _ptsTF.text = _ptsAvail.toString();
   }
 
   private function onMapSelectBtnClicked(evt:Event):void {
      _game.changeState(Game.MAP_SELECT_STATE);
   }
 
   public function update():void {
   }
 
   public function destroy():void
   {
      _mapSelectBtn.removeFromParent(true);
      _mapSelectBtn = null;
 
      _ptsTF.removeFromParent(true);
      _ptsTF = null;
 
      _labelTF.removeFromParent(true);
      _labelTF = null;
 
      if(contains(_resetBtn)) {
         _resetBtn.removeFromParent(true);
         _resetBtn.removeEventListener(Event.TRIGGERED, onResetBtnClicked);
         _resetBtn = null
      }
 
      var len:int = _upgradeOptions.length;
      for(var i:int = 0; i < len; i++) {
         _upgradeOptions[i].destroy();
      }
 
      _gameStateBkgd.destroy();
   }
}
}

So, we’ve reached the end of our post on States! Yay! What’s that? We missed one? The Play State? Oh… whoops… yeah, that sucker is large enough for it’s own post :). Next time I’ll discuss the Play state, and it will lead us into the good stuff! Enemies! Towers! GOOOOLD!

Feel free to check out the actual ZFStarlingTDDemo I created.

Thanks for reading, I hope this was helpful.
-Travis

Share

AS3 Starling Tower Defense Tutorial – Part 3 – Play State and Config

Last post we got into the various game States that I use in my AS3 Starling Tower Defense game demo. I left out Play because it’s pretty big and needed some cleaning up. Now I’ve got it all cleaned up and ready to go! Also, I had intended this post to get into Map Tiling at the end, but this ran long and I wanted to show the Config file since that’s used in so many places. So Map Tiling will come later. This is the Play State and my Config class.

Quick note… I’ve removed all of the doc blocks from functions to conserve space here… Comment your code!

Also, 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

Play.as

The Play State (com.zf.states.Play.as) is the main point of the game. It’s the State where a map gets drawn, enemies spawn, towers get placed, and the player hopefully has a great time. After a player has selected a map from the MapSelect State, we know the map ID they chose and the MapLoad state actually loads in the map JSON file and gets it ready for Play to do something with it.

Play is also the creator and maintainer of most all of the “Manager” classes used in the game. We’ll start off with the constructor and the addedToStage functions. Since this is such an important class, I included pretty much everything here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package com.zf.states
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.managers.*;
   import com.zf.objects.map.Map;
 
   import flash.display.Stage;
   import flash.geom.Point;
   import flash.utils.getTimer;
 
   import starling.animation.Juggler;
   import starling.core.Starling;
   import starling.display.Sprite;
   import starling.events.Event;
 
public class Play extends Sprite implements IZFState
{
   public static var zfJuggler:Juggler;
 
   public static const GAME_OVER_HP	 : int = 0;
   public static const GAME_OVER_ENEMIES : int = 1;
   public static const GAME_OVER_QUIT	 : int = 2;
 
   public static const GAME_STATE_PAUSE : int = 0;
   public static const GAME_STATE_PLAY	: int = 1;
   public static const GAME_STATE_END	: int = 2;
   public static const GAME_STATE_OVER	: int = 3;
 
   public static var gameState	   : int = GAME_STATE_PLAY;
   public static var gameOverState : int = -1;
 
   // Public Managers & Objects
   public var wpMgr	: WaypointManager;
   public var towerMgr	: TowerManager;
   public var keyMgr	: KeyboardManager;
   public var bulletMgr : BulletManager;
   public var enemyMgr	: EnemyManager;
   public var hitMgr	: CollisionManager;
   public var hudMgr    : HudManager;  
   public var map	: Map;
   public var ns	: Stage;
 
   public var mapOffsetX:int = 36;
   public var mapOffsetY:int = 36;
   public var endGameOnNextFrame:Boolean = false;
 
   //Public state values
   public var isPaused:Boolean = false;
   public var currentGold:int = 100;
 
   public var mapLayer:Sprite;
   public var enemyLayer:Sprite;
   public var towerLayer:Sprite;
   public var hudLayer:Sprite;
   public var topLayer:Sprite;
 
   private var _game:Game;
 
   // Game Conditions
   private var _gameCondEndOfWaves:Boolean;
   private var _gameCondEndOfEnemies:Boolean;
 
   private var _zfMgrs:Vector.<IZFManager>;
   private var _isGameStartPause:Boolean;
 
   public function Play(g:Game) {
      _game = g;
 
      zfJuggler = new Juggler();
 
      _zfMgrs = new Vector.<IZFManager>();
 
      // reset Config variables for new game
      Config.resetForNewGame();
 
      _gameCondEndOfWaves = false;
      _gameCondEndOfEnemies = false;
 
      addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }
 
   private function onAddedToStage(evt:Event):void {
      removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
 
      Starling.juggler.add(zfJuggler);
 
      ns = Starling.current.nativeStage;
 
      keyMgr = new KeyboardManager(this, ns);
      keyMgr.onPause.add(onPauseEvent);
 
      mapLayer = new Sprite();
      addChild(mapLayer);
 
      enemyLayer = new Sprite();
      addChild(enemyLayer);
 
      towerLayer = new Sprite();
      addChild(towerLayer);
 
      hudLayer = new Sprite();
      addChild(hudLayer);
 
      topLayer = new Sprite();
      addChild(topLayer);
 
      enemyMgr = new EnemyManager(this);
      _zfMgrs.push(enemyMgr);
 
      hudMgr = new HudManager(this);
      _zfMgrs.push(hudMgr);
 
      wpMgr = new WaypointManager(this);
 
      towerMgr = new TowerManager(this , Assets.towerData);
      _zfMgrs.push(towerMgr);
 
      map = new Map(Config.currentMapData, wpMgr, mapOffsetX, mapOffsetY);
      map.x = mapOffsetX;
      map.y = mapOffsetY;
      mapLayer.addChild(map);
 
      bulletMgr = new BulletManager(this);
      _zfMgrs.push(bulletMgr);
 
      hitMgr = new CollisionManager(this);
      _zfMgrs.push(hitMgr);
 
      // Set up enemy data
      enemyMgr.handleNewMapData(Config.currentMapData.enemyData);
 
      /**
       * Set up signals listeners
       */
      enemyMgr.onEnemyAdded.add(hitMgr.onEnemyAdded);
      enemyMgr.onEnemyRemoved.add(hitMgr.onEnemyRemoved);
      towerMgr.onTowerAdded.add(hitMgr.onTowerAdded);
      towerMgr.onTowerRemoved.add(hitMgr.onTowerRemoved);
      towerMgr.onTowerManagerRemovingEnemy.add(hitMgr.onEnemyRemoved);
 
      hudMgr.endOfHP.add(onEndOfHP);
      enemyMgr.endOfEnemies.add(onEndOfEnemies);
 
      // set the current start HP
      Config.changeCurrentHP(map.startHP, false);
 
      // set the current start Gold from map data
      Config.changeCurrentGold(map.startGold, false);
 
      hudMgr.showNextWaveButtons();
 
      // update hud ui
      hudMgr.updateUI();
 
      // this is the initial pause at the start of the game
      _isGameStartPause = true;
 
      // pause the game so nothing happens until we resume
      onGamePaused();
   }

Alrighty… about halfway done with Play. It sets up the whole playable fun part so the setup is pretty intense. I’m also taking a little break here because I imagine if you’re trying to find the notes on a line number and you have to scroll down, then scroll all the way back up for the next line of code, then back down for the commentary, I bet that’s pretty irritating. I’ll try to keep the sections short. The following is still from com.zf.states.Play.as.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
   public function onPauseEvent(fromOptions:Boolean = false):void {
      if(!_isGameStartPause || (_isGameStartPause && !fromOptions)) {
         Config.log('Play', 'onPauseKeyPressed', "Play.onPauseKeyPressed() == " + isPaused);
         // the game has started from a means other than options saving so set this to false
         _isGameStartPause = false;
 
         isPaused = !isPaused;
         if(isPaused) {
            onGamePaused();
         } else {
            onGameResumed();
         }
      }
   }
 
   public function onGamePaused():void {
      isPaused = true;
      gameState = GAME_STATE_PAUSE;
      Config.log('Play', 'pauseGame', "Removing juggler");
      Starling.juggler.remove(zfJuggler);
 
      var len:int = _zfMgrs.length;
      for(var i:int = 0; i < len; i++) {
         _zfMgrs[i].onGamePaused();
      }
   }
 
   public function onGameResumed():void {
      isPaused = false;
      gameState = GAME_STATE_PLAY;
      Config.log('Play', 'resumeGame', "Adding Juggler");
      Starling.juggler.add(zfJuggler);
 
      var len:int = _zfMgrs.length;
      for(var i:int = 0; i < len; i++) {
         _zfMgrs[i].onGameResumed();
      }
   }
 
   public function onQuitGameFromOptions():void {
      handleGameOver(GAME_OVER_QUIT);
   }
 
   public function update():void {
      if(endGameOnNextFrame) {
         // handle game over before getting back into another round of updates
         handleGameOver(Config.gameOverCondition);
      } else if(!isPaused && gameState != GAME_STATE_OVER) {
         // keeps track of how long the game has been active
         Config.activeGameTime = getTimer() - Config.pausedGameTime;
 
         enemyMgr.update();
         bulletMgr.update();
         hitMgr.update();
         towerMgr.update();
         hudMgr.update();
      } else if(isPaused && gameState != GAME_STATE_OVER) {
         // keeps track of how long the game has been paused
         Config.pausedGameTime = getTimer() - Config.activeGameTime;
      }
   }
 
   public function canCreateTower(towerID:String, level:int = 0):Boolean {
      var canCreate:Boolean = false,
          towerCost:int = towerMgr.getTowerCost(towerID, level);
 
      // CHECK CRITERIA FOR CREATING A TOWER
      if(towerCost <= Config.currentGold) {
         canCreate = true;
      }
 
      return canCreate;
   }
 
   public function createTower(towerID:String, pos:Point, level:int = 0):void {
      // HANDLE ACTUALLY CREATING THE TOWER
      // remove gold value
      Config.changeCurrentGold(-towerMgr.getTowerCost(towerID, level))
      hudMgr.updateUI();
 
      towerMgr.createNewTower(towerID, pos);
   }

Whew… almost done with com.zf.states.Play.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
   public function handleGameOver(gameOverCondition:int):void {
      switch(gameOverCondition) {
         case GAME_OVER_HP:
            Config.log('Play', 'handleGameOver', "LOST GAME LOSER!");
            break;
 
         case GAME_OVER_ENEMIES:
            // set that we won
            Config.totals.mapsWon = 1;
            // Add an upgrade point for the Upgrades screen
            Config.addTotalUpgradePoint();
            Config.log('Play', 'handleGameOver', "GAME WON WINNER!");
            break;
 
         case GAME_OVER_QUIT:
            // player quit
            Config.log('Play', 'handleGameOver', 'Quit Game');
            break;
      }
 
      // Add map totals to currentGameSOData
      Config.currentGameSOData.updateFromTotals(Config.totals);
 
      // Set SharedObject with game data and save
      Game.so.setGameData(Config.currentGameSOData, '', true);
 
      gameOverState = gameOverCondition;
      _changeGameState(GAME_STATE_OVER);
   }
 
   public function onEndOfHP():void {
      Config.log('Play', 'onEndOfHP', "Play onEndOfHP");
      Config.gameOverCondition = GAME_OVER_HP;
      endGameOnNextFrame = true;
   }
 
   public function onEndOfEnemies():void {
      Config.log('Play', 'onEndOfEnemies', "Play onEndOfEnemies");
      Config.gameOverCondition = GAME_OVER_ENEMIES;
      endGameOnNextFrame = true;
   }
 
   private function _changeGameState(st:int):void {
      Config.log('Play', '_changeGameState', "Game Changing State to: " + st);
      gameState = st;
      if(gameState == GAME_STATE_OVER) {
         _game.changeState(Game.GAME_OVER_STATE);
      }
   }
 
   public function destroy():void {
      Config.log('Play', 'destroy', "Play.destroy()");
 
      // remove all added listeners first
      _removeListeners();
      _removeManagers();
      _removeLayers();
      trace(Config.totals.toString());
   }
 
   private function _removeListeners():void {
      Config.log('Play', '_removeListeners', "Play._removeListeners()");
      keyMgr.onPause.remove(onPauseEvent);
      enemyMgr.onEnemyAdded.remove(hitMgr.onEnemyAdded);
      enemyMgr.onEnemyRemoved.remove(hitMgr.onEnemyRemoved);
      towerMgr.onTowerAdded.remove(hitMgr.onTowerAdded);
      towerMgr.onTowerRemoved.remove(hitMgr.onTowerRemoved);
      towerMgr.onTowerManagerRemovingEnemy.remove(hitMgr.onEnemyRemoved);
      hudMgr.endOfHP.remove(onEndOfHP);
      enemyMgr.endOfEnemies.remove(onEndOfEnemies);
   }
 
   private function _removeManagers():void {
      Config.log('Play', '_removeManagers', "Play._removeManagers()");
      map.destroy();
      map = null;
 
      keyMgr.destroy();
      keyMgr = null;
 
      wpMgr.destroy();
      wpMgr = null;
 
      // Handles Bullet, Collision, Enemy, Hud, Tower Managers
      var len:int = _zfMgrs.length;
      for(var i:int = len - 1; i >= 0; i--) {
         _zfMgrs[i].destroy();
         _zfMgrs[i] = null;
         _zfMgrs.splice(i, 1);
      }
   }		
 
   private function _removeLayers():void {
      Config.log('Play', '_removeLayers', "Play._removeLayers()");
      Starling.juggler.remove(zfJuggler);
      zfJuggler = null;
      removeChild(mapLayer);
      removeChild(enemyLayer);
      removeChild(towerLayer);
      removeChild(hudLayer);
   }
}

Ok, we’re done with Play. We’re not going to get to Map Tiling in this post as I’d originally hoped, but I will post Config.as here so we can take a quick peek at that.

Config.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
package com.zf.core
{
   import com.zf.utils.ZFGameData;
 
   import org.osflash.signals.Signal;
 
   public class Config
   {
      //PATH CONSTANTS
      public static const PATH_ASSETS         : String = "assets/";
      public static const PATH_JSON           : String = Config.PATH_ASSETS + "json/";
      public static const PATH_XML            : String = Config.PATH_ASSETS + "xml/";
      public static const PATH_SCENES         : String = Config.PATH_JSON + "scenes/";
      public static const PATH_IMG            : String = Config.PATH_ASSETS + "images/";
      public static const PATH_SOUNDS         : String = Config.PATH_ASSETS + "sounds/";
 
      // dataType constants
      public static const DATA_XML            : String = 'xml';
      public static const DATA_JSON           : String = 'json';
      public static const DATA_PEX            : String = 'pex';
      public static const DATA_FNT            : String = 'fnt';
      public static const DATA_MP3            : String = 'mp3';
      public static const IMG_PNG             : String = 'png';
      public static const IMG_JPG             : String = 'jpg';
      public static const IMG_GIF             : String = 'gif';
 
      public static const GAME_SPEED_UP	  : String = 'up';
      public static const GAME_SPEED_DOWN : String = 'down';
 
      // Signals
      public static var currentWaveChanged : Signal = new Signal(int);
      public static var currentHPChanged   : Signal = new Signal(int);
      public static var currentGoldChanged : Signal = new Signal(int);
      public static var onGameSpeedChange  : Signal = new Signal();
 
      // Game Data
      public static var currentGameSOID   : String = 'game1';
      public static var currentGameSOData : ZFGameData = new ZFGameData();
 
      // Timer Config
      public static var activeGameTime		: Number = 0;
      public static var pausedGameTime		: Number = 0;
 
      // Volume Config
      public static var musicVolume		: Number = 0.75;
      public static var sfxVolume		: Number = 0.75;
      public static const DEFAULT_SOUND_VOLUME 	: Number = 0.75;
 
      // Map Config
      public static var selectedMap	 : String = 'map1';
      public static var currentMapNumber : int = 1;
      public static var currentMapData	 : Object = {};
 
      // Game Speed Config
      public static var currentGameSpeed : Number;
 
      // Maximum number of waves this map
      public static var maxWave	: int;
 
      // What is the game over condition code
      public static var gameOverCondition : int;
 
      // Keeps track of total number of things
      public static var totals : Totals;
 
      // Debugger Config
      public static var debugMode      : Boolean = false;
      public static var debugVerbose   : Boolean = true;
 
      // Current Map Stats
      private static var _currentGold  : int;
      private static var _currentHP    : int;
      private static var _currentWave  : int;
 
      // Valid game speeds config
      private static var _gameSpeeds : Array = [0.5, 1, 2];
      private static var _speedIndex : uint = 1;
 
      // Unique ID
      private static var _currentUID:int = 0;
 
      public function Config() {
         Config.resetForNewGame();
      }
 
      public static function resetForNewGame():void {
         totals = new Totals();
 
         _currentGold = 0;
         _currentHP = 0;
         _currentWave = 0;
 
         // Speeds
         _speedIndex = 1;
         currentGameSpeed = 1;
      }
 
      public static function getUID():int {
         return ++_currentUID;
      }
 
      public static function changeGameSpeed(speedChangeDir:String):void {
         var speedChanged:Boolean = false;
 
         if(speedChangeDir == GAME_SPEED_UP) 
         {
            // check if we can speed up any
            if(_speedIndex < _gameSpeeds.length - 1) 
            {
               _speedIndex++;
               speedChanged = true;
            }
         } 
         else if(speedChangeDir == GAME_SPEED_DOWN) 
         {
            // check if we can slow down any
            if(_speedIndex > 0) 
            {
               _speedIndex--;
               speedChanged = true;
            }
         }
 
         // only dispatch event if the speed changed
         if(speedChanged) {
            // set currentGameSpeed
            currentGameSpeed = _gameSpeeds[_speedIndex];
            trace('currentGameSpeed changed: ' + currentGameSpeed);
            // dispatch new speed
            onGameSpeedChange.dispatch();
         }
      }
 
      public static function changeCurrentGold(g:int, addToCurrent:Boolean = true):void {
         if(addToCurrent) {
            _currentGold += g;
         } else {
            _currentGold = g;
         }
         currentGoldChanged.dispatch(_currentGold);
      }
 
      public static function get currentGold():int { 
         return _currentGold 
      }
 
      public static function set currentGold(g:int):void { 
         _currentGold = g;
      }
 
      public static function changeCurrentHP(hp:int, addToCurrent:Boolean = true):void {
         if(addToCurrent) {
            _currentHP += hp;
         } else {
            _currentHP = hp;
         }
         currentHPChanged.dispatch(_currentHP);
      }
 
      public static function get currentHP():int { 
         return _currentHP; 
      }
 
      public static function set currentHP(hp:int):void { 
         _currentHP = hp;
      }
 
      public static function changeCurrentWave(w:int, addToCurrent:Boolean = true):void {
         if(addToCurrent) {
            _currentWave += w;
         } else {
            _currentWave = w;
         }
         currentWaveChanged.dispatch(_currentWave);
      }
 
      public static function get currentWave():int { 
         return _currentWave; 
      }
 
      public static function set currentWave(w:int):void { 
         _currentWave = w;
      }
 
      public static function addGameAttempted():void {
         currentGameSOData.mapsAttempted++;
         Game.so.setGameDataProperty(currentGameSOData.mapsAttempted, 'mapsAttempted', '', true);
      }
 
      public static function addTotalUpgradePoint():void {
         currentGameSOData.upgrades.ptsTotal++;
         Game.so.setGameDataProperty(currentGameSOData.upgrades.ptsTotal, 'upgrades.ptsTotal', '', true);
      }
 
      public static function saveGameOptions():void {
            var opts:Object = {
               musicVolume: musicVolume,
               sfxVolume: sfxVolume
            };
         Game.so.setGameOptions(opts, true);
      }
 
      public static function log(klass:String, fn:String, msg:String, level:int = 0, verbose:Boolean = false):void  {
         if(Config.debugMode)  {
            var levelText:String = '';
            if(level == 0) {
               levelText = 'INFO';
            } else if(level == 1) {
               levelText = 'WARNING';
            } else if(level == 2) {
               levelText = 'ERROR';
            }
            var classNameFn:String = "[" + klass + "." + fn + "]";
            levelText = "[" + levelText + "]";
 
            if(!verbose || (Config.debugVerbose && verbose)) {
               trace(classNameFn + ' ' + levelText + ' => ' + msg);
            }
         }
      }
 
      public static function logError(klass:String, fn:String, msg:String):void  {
         Config.log(klass, fn, msg, 2);
      }
 
      public static function logWarning(klass:String, fn:String, msg:String):void  {
         Config.log(klass, fn, msg, 1);
      }
   }
}

So, there it is… the Play State! This post was originally going to include Map Tiling, but we’ll look at that next time. This was pretty hefty and it’s a good place to stop for now. Until next time, thanks for reading and I hope this was helpful!

Also, 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

-Travis

Share

AS3 Starling Tower Defense Tutorial – Part 4 – Map Tiling

Map Tiling in AS3 was the first post of my short-lived AS3 Game Engine series before I found Starling. While the core tile drawing code remains mostly the same, there have been some changes. We saw from yesterday’s post that the Play State creates an instance of the Map class and passes it the map JSON data Object. Let’s take a look at the JSON file that holds the map data, then we’ll look at the Map class and see what it does.

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

I’m not sure if I’ve mentioned this yet, but I am using TexturePacker to create my spritesheets/tilesheets (is there a difference? semantics?). TexturePacker makes creating these tilesheets stupid easy and quick. If you can save your png images into a folder, and if you can remember that folder so you can find that very same folder again… you can make tilesheets using TexturePacker. They have tons of tutorials for using the app, and Lee Brimlow makes a three-part excellent spritesheet/basics-of-blitting tutorials over at GotoAndLearn (part1, part2, part3)

Map JSON Data

We’re going to look at the map data from the second map I made. Here’s a screenshot of the map actually in the game, all drawn out, tile-by-tile.

map2.jpg

These are all tiles I made out of 24×24 tiles in Photoshop. Here’s a sample of the tiles that are in my tile sheet.

tiles.png

This has clearly been enlarged and is a little pixelated, but it’s 24×24 pixel art, there’s really not a whole lot of room for smooth lines. This image is here to show that I’ve basically grouped my tiles in groups of 3. For example, the first three tiles are all the same road piece, left-to-right, but with varying “dirt” sorts of pixels along the road piece. This lets me have the long straight roads for the enemies to travel down, and every tile doesn’t look like the exact same tile. Sure, only 3 variations isn’t really a lot of difference. You could make curved pieces, or “broken road” sorts of pieces, anything really.

Then there’s the single grass tile. Hey, it works right? The single green tile, as we’ll see in the JSON data is our background tile. I set the Map tiler up to take a single background image and make a whole “layer” of this same tile as the background layer. Any further layers are kept in the JSON array below in “tiles” each “layer” consists of however many “row”s there are on the screen. So looking at the “row” info below you can clearly see how it syncs up with the actual drawn map image above. This is a “row” of data, the first two tiles are {} empty, so we draw nothing for this layer in those tiles. The third tile in this row is a vertical road piece. If you go back to the map image above and look in the top left corner, 2 green tiles in, you see a vertical road piece. I’ve structured my JSON data and set up my Map class so that if there is nothing in a tile (“{}”) we skip that tile so we do not overwrite the background in any way. This lets me add layers and layers of detail if I wanted. Which I clearly don’t take advantage of here in this tutorial as there is just a single “layer” on top of the background that just shows a simple road. Rock tiles, trees, level details could all be easily added in subsequent layers to provide an infinite amount of customization to each map.

I won’t go into too much more detail about the JSON data, but I just want to make sure you can look at the JSON here and visualize at least “something” of a structure that you should expect to see in the actual drawn out map. Here are the first three rows of the map, you can look at the image above and come back to the data. The first row has a single vertical road piece in it. The second row has a single vertical road piece in it. Then the third row has the connecting road pieces for the vertical road to connect with the long horizontal road we’re building here.

Quick note about the way I’ve named my textures and images here. You’ll notice the “src” attribute contains something that looks like ‘roads/road1_202’. If you’ve downloaded the project zip file, or checked it out from my bitbucket repo you’ll see that you can go to src/assets/images/tiles and I’ve got all my tile images that I want to go in my spritesheet. So when it references ‘roads/road1_202’ if you go to src/assets/images/tiles/roads/road1_202.png you’ll find the single 24x24px image tile that I’m referencing, and that tile gets drawn into the src/assets/images/atlas.png spritesheet. I named these files this way for a reason, so all of these brown dirt roads in this set belong to my “road1” (“road2” might be snow roads?) set and they’re numbered 1-7 for the different directions so road1_1 for the first direction and the last number is a variation number. So ‘roads/road1_103’ would be the road1 set, the first direction, and the 3rd shading variation of that horizontal tile. That’s just the way I did it, and why I did it that way. Feel free to adopt your own file naming conventions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
{
"mapName": "Second Map!",
"numRows": 22,
"numCols": 22,
"numLayers": 1,
"bkgdSrc": "misc/grass1_001",
"drawBkgd": 1,
"tileWidth": 24,
"tileHeight": 24,
"startHP": 10,
"startGold": 15,
"enemyWaveData": { ....removed.... },
"enemyData": { ....removed.... },
"tiles": [{
  "layer": [{
    "row": [
      {},
      {},
      { "src": "roads/road1_202", "isWalkable": 1, "wp": 1, "wpGroup": "wpGroup1", "wpIndex": 1, "sp": 1, "spDirection": "up" },
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {}
    ]},
    {
    "row": [
      {},
      {},
      { "src": "roads/road1_201", "isWalkable": 1, "groupStartIcon": "t" },
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {}
    ]},
    {
    "row": [
      {},
      {},
      { "src": "roads/road1_503", "isWalkable": 1, "wp": 1, "wpGroup": "wpGroup1", "wpIndex": 2 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_102", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_102", "isWalkable": 1 },
      { "src": "roads/road1_102", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_102", "isWalkable": 1 },
      { "src": "roads/road1_302", "isWalkable": 1, "wp": 1, "wpGroup": "wpGroup1", "wpIndex": 3 },
      {},
      {}
    ]},

So that’s an except from the src/assets/json/maps/map2.json file, feel free to check it out in its entirety in the project. Let’s move on to how that data is actually used.

Man, quick formatting note, my need for properly indented code is fighting with the fact that the site is only so wide. In the actual files in the project, the public class Map extends Sprite line is properly a tab inside of the package. But as that’d add even more tabs to the actual code (that we really actually care about) I’ve just dropped everything back a tab or two. /OCD-Disclaimer

Map.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package com.zf.objects.map
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.managers.WaypointManager;
 
   import flash.geom.Point;
   import flash.geom.Rectangle;
 
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.textures.Texture;
   import com.zf.objects.tower.Tower;
 
public class Map extends Sprite
{
   public var halfTileWidth:Number;
   public var halfTileHeight:Number;
   public var mapWidth:Number;
   public var mapHeight:Number;
   public var tileWidth:int;
   public var tileHeight:int;
   public var mapOffsetX:int;
   public var mapOffsetY:int;
 
   private var _mapData:Object;
   private var _tileData:Array;
   private var _wpMgr:WaypointManager;
 
   // Tile data flattened down without layers, aggregating isWalkable data
   private var _flatTileData:Array;
 
   private var _mapWidthOffset:Number;
   private var _mapHeightOffset:Number;
 
   public function Map(data:Object, wpMgr:WaypointManager, mOffsetX:int, mOffsetY:int) {
      _mapData = data;
      _wpMgr = wpMgr;
 
      mapOffsetX = mOffsetX;
      mapOffsetY = mOffsetY;
      tileWidth = _mapData.tileWidth;
      tileHeight = _mapData.tileHeight;
      halfTileWidth = _mapData.tileWidth >> 1;
      halfTileHeight = _mapData.tileHeight >> 1;
      mapWidth = _mapData.tileWidth * _mapData.numCols;
      mapHeight = _mapData.tileHeight * _mapData.numRows;
      _mapWidthOffset = mapWidth + mapOffsetX;
      _mapHeightOffset = mapHeight + mapOffsetY;
 
      // initial layer level
      _tileData = [];
      _flatTileData = [];
 
      _parseMapDataIntoTileData();
      _drawMap();
   }
 
   private function _parseMapDataIntoTileData():void {
      var rowCt:int = 0,
          colCt:int = 0,
          layerCt:int = 0,
          mapLayerCt:int = 0;
 
      if(_mapData.drawBkgd) {
         var data:Object = {"src": _mapData.bkgdSrc, "isWalkable": false };
         _createBkgdLayerTileData(data);
         layerCt++;
      } else {
         mapLayerCt = layerCt;
      }
 
      for(layerCt; layerCt <= _mapData.numLayers; layerCt++)
      {
         _tileData[layerCt] = [];
         for(rowCt = 0; rowCt < _mapData.numRows; rowCt++)
         {
            _tileData[layerCt][rowCt] = [];
            for(colCt = 0; colCt < _mapData.numCols; colCt++)
            {
               // map data doesnt have the bkgd layer
               var tileData2:TileData = new TileData(_mapData.tiles[mapLayerCt].layer[rowCt].row[colCt]);
 
               _tileData[layerCt][rowCt][colCt] = tileData2;
 
               if(_flatTileData[rowCt][colCt] is TileData)
               {
                  if(tileData2.isWalkable)
                  {
                     // if any tiledata is walkable, set the position to true
                     _flatTileData[rowCt][colCt].isWalkable = true;
                     _flatTileData[rowCt][colCt].srcImageName = tileData2.srcImageName;
                  }
               }
               else
               {
                  _flatTileData[rowCt][colCt] = tileData2;
               }
            }
         }
         mapLayerCt++;
      }
   }
 
   private function _createBkgdLayerTileData(data:Object):void {
      var rowCt:int = 0,
          colCt:int = 0;
 
      // create the first empty row array
      _tileData[0] = [];
 
      for(rowCt; rowCt < _mapData.numRows; rowCt++)
      {
         _tileData[0][rowCt] = [];
 
         if(_flatTileData[rowCt] == undefined) {
            _flatTileData[rowCt] = [];
         }
 
         for(colCt = 0; colCt < _mapData.numCols; colCt++)
         {
            var bkgdTile:TileData = new TileData(data);
            _tileData[0][rowCt][colCt] = bkgdTile;
 
            // get another tile for _flatTileData
            bkgdTile = new TileData(data);
            _flatTileData[rowCt][colCt] = bkgdTile;
         }
      }
   }

Taking a quick break so we can keep the scrolling down a little… Ok continuing on in Map.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
   private function _drawMap():void {
      var currentLayer:int = 0,
          maxLayers:int = _mapData.numLayers;
 
      if(_mapData.drawBkgd) {
         maxLayers++;
      }
 
      for(currentLayer; currentLayer < maxLayers; currentLayer++) {
         _drawLayer(currentLayer);
      }
 
      // flatten this map once it has been drawn
      this.flatten();
 
      // Map is done drawing and adding waypoints, so lets clean up WaypointManager
      _wpMgr.handleEndpointAndSort();
   }
 
   private function _drawLayer(layer:int):void {
      var srcImage:Image,
          destPt:Point = new Point(),
          destRect:Rectangle = new Rectangle(0, 0, _mapData.tileWidth, _mapData.tileHeight),
          rowCt:int = 0,
          colCt:int = 0,
          tileData:TileData,
          pt:Point = new Point();
 
      for(rowCt; rowCt < _mapData.numRows; rowCt++)
      {
         for(colCt = 0; colCt < _mapData.numCols; colCt++) 
         {
            tileData = _tileData[layer][rowCt][colCt];
 
            // if this tile does not have a source image name for this layer, skip drawing it
            if(tileData.srcImageName == '') {
               continue;
            }
 
            var tmpTexture:Texture = Assets.ta.getTexture(tileData.srcImageName),
		xOffset:int = (_mapData.tileWidth - tmpTexture.width) >> 1,
                yOffset:int = (_mapData.tileHeight - tmpTexture.height) >> 1,
                rect:Rectangle = new Rectangle(-xOffset, -yOffset, _mapData.tileWidth, _mapData.tileHeight),
                texture:Texture = Texture.fromTexture(tmpTexture, null, rect);
 
            srcImage = new Image(texture);
            srcImage.x = pt.x = (colCt * _mapData.tileWidth);
            srcImage.y = pt.y = (rowCt * _mapData.tileHeight);
 
            addChild(srcImage);
 
            if(tileData.isWaypoint) {
               pt.x += mapOffsetX;
               pt.y += mapOffsetY;
               _wpMgr.addWaypoint(tileData, pt, halfTileWidth, halfTileHeight);
            }
 
            if(tileData.groupStartIconDir != '') {
               // Adding mapOffsets again here because isWaypoint and groupStartIconDir
               // will never exist on the same tile
               pt.x += mapOffsetX;
               pt.y += mapOffsetY;
               _wpMgr.addGroupStartPosition(tileData.groupStartIconDir,
               pt, halfTileWidth, halfTileHeight);
            }
         }
      }
   }
 
   public function destroy():void {
      removeFromParent(true);
   }
 
   public function checkCanPlaceTower(p:Point):Object {
      var retObj:Object = {};
      retObj.canPlace = false;
      retObj.point = new Point(-1, -1);
      // make sure we clicked inside the actual map before further checks
      if(clickedInsideMap(p)) {
         // p is a reference to coordinates from the stage, we want to remove the offset
         // to get just the coordinates relative to the map itself for the array keys
         var rowCt:int = int((p.y - mapOffsetX) / _mapData.tileHeight),
             colCt:int = int((p.x - mapOffsetY) / _mapData.tileWidth),
             t:TileData = _flatTileData[rowCt][colCt];
 
         // If isWalkable == true or isTower == true, then canPlace = false, 
         // we cannot place tower on walkway/tower
         retObj.canPlace = ((_flatTileData[rowCt][colCt].isWalkable == false) 
                           && (_flatTileData[rowCt][colCt].isTower == false));
 
         if(retObj.canPlace) {
            retObj.point.x = colCt * _mapData.tileWidth;
            retObj.point.y = rowCt * _mapData.tileHeight;
         }
      }
      return retObj;
   }
 
   public function placedTower(t:Tower):void {
      var rowCt:int = int((t.y - mapOffsetX) / _mapData.tileHeight),
          colCt:int = int((t.x - mapOffsetY) / _mapData.tileWidth);
 
      _flatTileData[rowCt][colCt].isTower = true;
   }
 
   public function removeTower(t:Tower):void {
      var rowCt:int = int((t.y - mapOffsetX) / _mapData.tileHeight);
          colCt:int = int((t.x - mapOffsetY) / _mapData.tileWidth);
 
      _flatTileData[rowCt][colCt].isTower = false;
   }
 
   public function clickedInsideMap(p:Point):Boolean {
      var clickedMap:Boolean = false;
      if(p.x > 0 && p.x < _mapWidthOffset && p.y > 0 && p.y < _mapHeightOffset)  
      {
         clickedMap = true;
      }
      return clickedMap;
   }
 
   public function get paddedBounds():Rectangle {
      var paddedBoundsRect:Rectangle = new Rectangle();
      paddedBoundsRect.x -= tileWidth - mapOffsetX;
      paddedBoundsRect.y -= tileHeight - mapOffsetY;
      paddedBoundsRect.width = this.width + tileWidth + mapOffsetX;
      paddedBoundsRect.height = this.height + tileHeight + mapOffsetY;
      return paddedBoundsRect;
   }
 
   public function get startHP():int {
      return _mapData.startHP;
   }
 
   public function get startGold():int {
      return _mapData.startGold;
   }
}
}

So there’s the Map class, that’s how I handle map tile data from JSON to “blitting” from a tilesheet in Starling.

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

Until next time, thanks for reading and I hope this was helpful

-Travis

Share

AS3 Starling Tower Defense Tutorial – Part 5 – Enemies

It’s finally time to get into the good stuff! Β In this post we’ll be looking at enemies and how they get set up, run through, and knocked down. We’ll look at how I’ve created some JSON metadata in the map JSON file to set up the types of enemies the map will contain, the sounds to use for those enemies, the different enemy groups that can be in a single wave, how those groups are defined, and how individual enemies are created inside those groups. Let’s take a quick high-level view at how I’ve structured my data and my classes. We’ll get to the actual JSON in a sec.

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

JSON Data Structure

Enemy Data

I’m going to hit EnemyWaves first, since the other two relate more to each other. Enemy Wave data here doesn’t pertain to an actual class file in the project, but it is used by the classes as a sort of map/hash set. For example, when the user starts the game and the first wave tile at the bottom of the screen “spawns”, that wave tile has an id of “wave1”. So the EnemyManager class will go through and say, “Ok, ‘wave1’ needs to spawn, which groups should I use?”

1
2
3
4
{
   "id": "wave1",
   "groups":"group1",
},

When ‘wave1’ spawns, the EnemyManager will check those “groups” which correspond to the ids of the EnemyGroups below. So ‘wave1’ spawns and EnemyManager goes to find EnemyGroup id ‘group1’ and tells it to begin spawning.

An EnemyGroup is a collection of Enemy classes (and subclasses) that all share a common set of Waypoints for that map. If you have one road on a map, you’ve probably just got one set of waypoint data, “All enemies walk this path from (10,10) to (100,10) to…” then the above JSON data would be fine for your enemyWaves groups definition. But lets say you’ve got two roads on your map, and so there are two different sets of waypoints, one set is for the group that spawns on the left side of the screen, one set is for the group that spawns at the top of the screen.

1
2
3
4
{
   "id": "wave1",
   "groups":"group1a,group1b",
},

In Map 2 of my demo there are two roads, and two separate enemy groups. And the above JSON code is used to spawn the first wave. The EnemyGroup designated with an id “group1a” and “group1b” will be spawned when the wave “wave1” is spawned.

So we’ve got enemyTypes that correspond to com.zf.objects.enemy.EnemyType.as, enemyGroups that correspond to com.zf.objects.enemy.EnemyEnemyGroup.as, and inside of enemyGroups, enemies that correspond to com.zf.objects.enemy.Enemy.as as a base class. Let’s look at an enemy type excerpt from the JSON.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
   "id": "enemyA",
   "name": "Enemy A",
   "klass": "EnemyA",
   "sounds": [
      { 
         "state": "onEscape",
         "soundId": "escape1"
      },
      { 
         "state": "onBanish",
         "soundId": "banish1"
      }
   ]
},

Think of an EnemyType as the metadata outlining a specific sub-class of enemies. All enemies inherit from the base Enemy class. However, the above “enemyA” is going to use a specific sub-class of Enemy, com.zf.objects.enemy.types.EnemyA.as to be specific. Since that’s the “klass” attribute, we’re going to use the EnemyA class instead of the boring, old Enemy.as class. Though EnemyA extends Enemy, so we very much use most all of that class. We’ll see later.

So, an EnemyType definition lets us specify an internal id for the type, a name to display to the user for that type of enemy, a class to use for that type, and different sounds which consist of sound states. For example, as we’ll see, at the base Enemy class, when the Enemy escapes/leaks from the map, the base Enemy class will call a function that basically says, “play the ‘onEscape’ sound state if it exists.” So, in the case of enemyA, we’ve defined “onEscape” to be “escape1” so when enemyA escapes from the map, escape1 will play. If I did not include an “onEscape” state for the enemy type, then nothing would play. You’re right though, I could certainly define a base set of sounds for the base Enemy class to default to, but I did not for this tutorial.

Let’s look at the enemyGroups definitions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
   "id": "group1",
   "name": "Group 1",
   "spawnDelay": "800",
   "wave": "wave1",
   "waypointGroup": "wpGroup1",
   "enemies": [
      { 
         "typeId": "enemyA", 
         "level": 1 
      },
      { 
         "typeId": "enemyB", 
         "level": 1 
      },
      { "typeId": "enemyB", "level": 1 },
      { "typeId": "enemyA", "level": 1 },
      { "typeId": "enemyB", "level": 1 },
      { "typeId": "enemyA", "level": 1 }
   ]
},

So this EnemyGroup consists of 6 total enemies that will be spawning 0.8 seconds apart from each other. If you wanted a “swarm” level, you may have 30 enemies defined with a spawnDelay of “100” or less! You may want all of your groups to be of the same EnemyType, or mixed types… however you want to do it. The beauty of having all that information in this JSON file is that your designers, or your level editors, or your playtesters can open this file in a text editor and change the number of enemies, spawn delay, types of enemies, etc in every wave. Then they save the file and refresh their browser. This will save them YEARS of time so they can actually tweak these settings themselves and find the right balance. This saves you YEARS of time from having this crap hardcoded and mister level editor has to have you recompile a new SWF every time he wants the enemies to spawn “just a little faster”. You don’t know if that’s 200ms faster or 500ms faster so you split the difference at 375ms faster and he really meant a second faster. You find that out 4 builds later when he just says “can you just make it one second faster?” With the data in the file, everyone can take care of their own tweaking.

ALRIGHT! Enough about JSON data nonsense. Let’s get to the AS3! That’s what we all came here for anyways…

Enemy.as

My enemy graphics were taken from a spritesheet that can be found here. I have contacted Flipz, the artist who created the tilesheet, and he was cool enough to let me borrow his images.

This is the entire base Enemy class file. We’ll go through it step by step below. I’m going to try to cut it to ~100-line chunks to make it easier to digest and please remember my formatting is jacked up so we can see more of each line, and my doc blocks have been removed to save space.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package com.zf.objects.enemy
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.states.Play;
   import com.zf.ui.healthBar.HealthBar;
 
   import org.osflash.signals.Signal;
 
   import starling.display.MovieClip;
   import starling.display.Sprite;
 
public class Enemy extends Sprite
{
   public static const ENEMY_DIR_UP:String = 'enemyUp';
   public static const ENEMY_DIR_RIGHT:String = 'enemyRight';
   public static const ENEMY_DIR_DOWN:String = 'enemyDown';
   public static const ENEMY_DIR_LEFT:String = 'enemyLeft';
 
   public static const ENEMY_SND_STATE_BANISH:String = "onBanish";
   public static const ENEMY_SND_STATE_ESCAPE:String = "onEscape";
 
   // set default speed to 1
   public var speed:Number = 1;
   public var maxHP:Number = 10;
   public var currentHP:Number = 10;
   public var reward:int = 5;
   public var damage:int = 1;
   public var isEscaped:Boolean = false;
   public var isBanished:Boolean = false;
   public var willBeBanished:Boolean = false;
 
   public var onDestroy:Signal;
   public var onBanished:Signal;
   public var type:String = "Enemy";
   public var uid:int;
   public var totalDist:Number;
 
   protected var _animState:String;
   protected var _animData:Object;
   protected var _animTexturesPrefix:String;
   protected var _soundData:Object;
 
   protected var _distToNext:Number;
   protected var _currentWPIndex:int;
   protected var _waypoints:Array = [];
   protected var _healthBar:HealthBar;
 
   // enemy speed * currentGameSpeed
   protected var _enemyGameSpeed:Number;
   protected var _enemyGameSpeedFPS:int;
   protected var _enemyBaseFPS:int = 12;
 
   /**
    * This is an Enemy's "currentHP" factoring in all bullets currently
    * flying towards it. So if this is <= 0, the enemy may still be alive,
    * but bullets have already been spawned with it's name on it
    */
   protected var _fluxHP:Number;
 
   public function Enemy()
   {
      uid = Config.getUID();
 
      _setInternalSpeeds();
 
      _setupAnimData();
      _changeAnimState(ENEMY_DIR_RIGHT);
      pivotX = width >> 1;
      pivotY = height >> 1;
 
      _soundData = {};
 
      onDestroy = new Signal(Enemy);
      onBanished = new Signal(Enemy);
 
      // let enemy listen for game speed change
      Config.onGameSpeedChange.add(onGameSpeedChange);
   }
 
   public function init(wps:Array, dist:Number):void {
      isEscaped = false;
      isBanished = false;
      willBeBanished = false
      _waypoints = wps;
      totalDist = dist;
      _distToNext = _waypoints[0].distToNext;
 
      // clear the old animState
      _animState = '';
      // set new animState
      _changeAnimState(_waypoints[0].nextDir);
 
      // reset WP index
      _currentWPIndex = 0;
      x = _waypoints[0].centerPoint.x;
      y = _waypoints[0].centerPoint.y;
 
      // reset current and _flux back to maxHP
      currentHP = _fluxHP = maxHP;
 
      _healthBar = new HealthBar(this, currentHP, maxHP, 30);
      _healthBar.x = -20;
      _healthBar.y = -10;
      addChild(_healthBar);
}

Continuing Enemy.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public function update():void {
   _distToNext -= _enemyGameSpeed;
 
   totalDist -= _enemyGameSpeed;
 
   if(_distToNext <= 0) {
      _getNextWaypoint();
   }
 
   switch(_animState) {
      case ENEMY_DIR_UP:
         y -= _enemyGameSpeed;
         break;
 
      case ENEMY_DIR_RIGHT:
         x += _enemyGameSpeed;
         break;
 
      case ENEMY_DIR_DOWN:
         y += _enemyGameSpeed;
         break;
 
      case ENEMY_DIR_LEFT:
         x -= _enemyGameSpeed;
         break;
   }
}
 
public function takeDamage(dmgAmt:Number):void {
   Config.log('Enemy', 'takeDamage', uid + " is taking " + dmgAmt + " damage");
   if(!isEscaped) {
      Config.totals.totalDamage += dmgAmt;
 
      currentHP -= dmgAmt;
      if(_healthBar) {
         _healthBar.takeDamage(dmgAmt);
      }
 
      if(currentHP <= 0) {
         _handleBanished();
      }
 
      Config.log('Enemy', 'takeDamage', "Enemy " + uid + " has " + currentHP + " hp remaining");
   }
}
 
public function onGameSpeedChange():void {
   _removeAnimDataFromJuggler();
 
   _setInternalSpeeds();
 
   _setupAnimData();
 
   _changeAnimState(_animState, true);
}
 
public function willDamageBanish(dmg:Number):Boolean {
   // deal damage to _fluxHP to see if this
   // damage amount will banish enemy
   _fluxHP -= dmg;
   if(_fluxHP <= 0) {
      willBeBanished = true;
   }
   Config.log('Enemy', 'willDamageBanish', "Enemy " + uid + " _fluxHP " + _fluxHP + " and willBeBanished is " + willBeBanished);
   return willBeBanished;
}
 
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 _getNextWaypoint():void {
   _currentWPIndex++;
 
   if(_currentWPIndex < _waypoints.length - 1) {
      _changeAnimState(_waypoints[_currentWPIndex].nextDir);
      _distToNext = _waypoints[_currentWPIndex].distToNext;
   } else {
      _handleEscape();
   }
}
 
protected function _handleEscape():void {
   isEscaped = true;
   _playIfStateExists(ENEMY_SND_STATE_ESCAPE);
}
 
protected function _handleBanished():void {
   Config.log('Enemy', '_handleBanished', "Enemy " + uid + " is below 0 health and isBanished = true");
   isBanished = true;
   onBanished.dispatch(this);
   _playIfStateExists(ENEMY_SND_STATE_BANISH);
}

Finishing up Enemy.as…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
protected function _playIfStateExists(state:String):void {
   if(_soundData.hasOwnProperty(state)) {
      Game.soundMgr.playFx(_soundData[state], Config.sfxVolume);
   }
}
 
protected function _setupAnimData():void {
   //_animState = ''; // removed due to a speed change bug
   _animData = {};
 
   _animData[ENEMY_DIR_UP] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_t_'), _enemyGameSpeedFPS);
   _animData[ENEMY_DIR_RIGHT] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_r_'), _enemyGameSpeedFPS);
   _animData[ENEMY_DIR_DOWN] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_b_'), _enemyGameSpeedFPS);
   _animData[ENEMY_DIR_LEFT] = new MovieClip(Assets.ta.getTextures(_animTexturesPrefix + '_l_'), _enemyGameSpeedFPS);
}
 
protected function _changeAnimState(newState:String, forceChange:Boolean = false):void {
   // make sure they are different states before removing and adding MCs
   // unless foreceChange is true
   if(_animState != newState || forceChange) {
 
      // _animState == '' on subsequent play throughs since init doesn't get called again
      if(_animState != '') {
         _removeAnimDataFromJuggler();
      }
      _animState = newState;
 
      _addAnimDataToJuggler()
   }
}
 
protected function _removeAnimDataFromJuggler():void {
   // remove the old MovieClip from juggler
   Play.zfJuggler.remove(_animData[_animState]);
   // remove the old MovieClip from this Sprite
   removeChild(_animData[_animState]);
}
 
protected function _addAnimDataToJuggler():void {
   // add the new MovieClip to the Juggler
   Play.zfJuggler.add(_animData[_animState]);
   // add the new MovieClip to the Sprite
   addChild(_animData[_animState]);
}
 
protected function _setInternalSpeeds():void {
   _enemyGameSpeed = Config.currentGameSpeed * speed;
   _enemyGameSpeedFPS = int(Config.currentGameSpeed * _enemyBaseFPS);
   // make sure _enemyGameSpeedFPS is at least 1
   if(_enemyGameSpeedFPS < 1) {
      _enemyGameSpeedFPS = 1;
   }
}
 
protected function _updateSpeed(spd:Number):void {
   // change speed from child classes
   speed = spd;
   // Call _setInternalSpeeds to reset internal speeds
   _setInternalSpeeds();
}
 
public function destroy():void {
   onDestroy.dispatch(this);
   onDestroy.removeAll();
   _removeAnimDataFromJuggler();
   removeChild(_healthBar);
   _healthBar = null;
   removeFromParent(true);
   Config.log('Enemy', 'destroy', "+ + " + uid + " destroyed");
}
}
}

So now that we’ve discussed the base Enemy class, let’s look at how freakin simple it is to make more enemy classes.

*OOP Note* in this example tutorial/demo, I’ve created a subclass and all it does is change some properties of the parent class. This is pretty bad OOP. If all I am doing in a subclass (in this tutorial) is changing the speed of the Enemy, and the texture prefix, I should just pass those in to the parent class, Enemy’s constructor.
var enemy:Enemy = new Enemy(speed, texturePrefix); and then do things that way. However, I chose to go the subclass route because I’m already working on more in-depth actual subclasses with new functionality in my other personal game I’m working on. So… just wanted to get that out there that I realize this is an excessive/unnecessary use of hierarchy for this specific demo.

EnemyA.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.zf.objects.enemy.types {
   import com.zf.objects.enemy.Enemy;
 
   public class EnemyA extends Enemy {
      public function EnemyA() {
         super();
         // speed for EnemyA
         _updateSpeed(1.2);
      }
 
      override protected function _setupAnimData():void {
         _animTexturesPrefix = 'enemies/enemyA';
         super._setupAnimData();
      }
   }
}

Yup. That’s it! All of our Enemy subclasses can be that short of a file. Thanks, OOP!

That’s it! All the movement code, the damage taking, the dispatching Signals when the Enemy gets killed or gets added, or reaches a waypoint and needs to change directions, all of that data has already been done in Enemy.as. Just for fun let’s look at our second Enemy subclass… EnemyB (com.zf.objects.enemy.types.EnemyB.as)

EnemyB.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.zf.objects.enemy.types {
   import com.zf.objects.enemy.Enemy;
 
   public class EnemyB extends Enemy {
      public function EnemyB() {
         super();
      }
 
      override protected function _setupAnimData():void {
         _animTexturesPrefix = 'enemies/enemyB';
         super._setupAnimData();
      }
   }
}

I do even less in this file. EnemyB just inherits Enemy’s default speed of 1.0. This makes EnemyA slightly faster than EnemyB.

With the Enemy class (and subclasses) out of the way, let’s look at the individual pieces that come together to help Enemy. We’re going to look at EnemyType. and EnemyGroup.as. After we finish both of those files, we’ll look at EnemyManager.as to tie everything together, then we’ll head out for a beer.

EnemyType.as

EnemyType.as (com.zf.objects.enemy.EnemyType.as) is a fairly small file that from the surface looks like just a data object. But there is a very significant reason this is here and with the way I am loading classes from a String in metadata, the game would not be possible without this…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.zf.objects.enemy 
{
   import com.zf.objects.enemy.types.EnemyA;
   import com.zf.objects.enemy.types.EnemyB;
 
   public class EnemyType
   {
      public var id:String;
      public var name:String;
      public var fullClass:String;
      public var soundData:Array;
 
      public function EnemyType(i:String, n:String, fC:String, sD:Array) {
         id = i;
         name = n;
         fullClass = 'com.zf.objects.enemy.types.' + fC;
         soundData = sD;
      }
 
      // create dummy versions of each enemy type, these will never be used
      private var _dummyEnemyA:EnemyA;
      private var _dummyEnemyB:EnemyB;
   }
}

EnemyGroup.as

EnemyGroup.as (com.zf.objects.enemy.EnemyGroup.as) is a bit more involved, but still under 100 lines of code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.zf.objects.enemy
{
   import com.zf.core.Config;
   import com.zf.utils.GameTimer;
 
   import flash.events.TimerEvent;
 
   import org.osflash.signals.Signal;
 
public class EnemyGroup
{
   public var id:String;
   public var name:String;
   public var spawnDelay:Number;
   public var wave:String;
   public var waypointGroup:String;
   public var enemies:Array;
   public var enemyObjects:Array;
   public var spawnTimer:GameTimer;
   public var onSpawnTimerTick:Signal;
   public var onSpawnTimerComplete:Signal;
   public var isFinished:Boolean;
 
   public function EnemyGroup(i:String, n:String, wpGroup:String, sD:Number, w:String, e:Array)
   {
      id = i;
      name = n;
      waypointGroup = wpGroup;
      spawnDelay = sD;
      wave = w;
      enemies = e;
      enemyObjects = [];
      isFinished = false;
 
      spawnTimer = new GameTimer(id, spawnDelay, enemies.length);
      spawnTimer.addEventListener(TimerEvent.TIMER, _onSpawnTimer);
      spawnTimer.addEventListener(TimerEvent.TIMER_COMPLETE, _onSpawnTimerComplete);
 
      // dispatches the enemy being spawned
      onSpawnTimerTick = new Signal();
      onSpawnTimerComplete = new Signal();
   }
 
   public function startGroup():void {
      spawnTimer.start();
   }
 
   public function pauseGroup():void {
      if(!isFinished && spawnTimer.running) {
         spawnTimer.pause();
      }
   }
 
   public function resumeGroup():void {
      if(!isFinished && spawnTimer.paused) {
         spawnTimer.start();
      }
   }
 
   private function _onSpawnTimer(evt:TimerEvent):void {
      onSpawnTimerTick.dispatch(enemyObjects.pop(), waypointGroup);
   }
 
   private function _onSpawnTimerComplete(evt:TimerEvent):void {
      isFinished = true;
   }
 
   public function destroy():void {
      Config.log('EnemyGroup', 'destroy', "+++ EnemyGroup Destroying");
      spawnTimer.removeEventListener(TimerEvent.TIMER, _onSpawnTimer);
      spawnTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, _onSpawnTimerComplete);
      spawnTimer = null;
 
      var len:int = enemyObjects.length;
      for(var i:int = 0; i < len; i++) {
         enemyObjects[i].destroy();
      }
      enemies = null;
      enemyObjects = null;
      Config.log('EnemyGroup', 'destroy', "--- EnemyGroup Destroyed");
   }
}
}

EnemyManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package com.zf.managers
{
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.enemy.EnemyGroup;
   import com.zf.objects.enemy.EnemyType;
   import com.zf.states.Play;
   import com.zf.utils.GameTimer;
 
   import flash.events.TimerEvent;
   import flash.utils.getDefinitionByName;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.extensions.PDParticleSystem;
 
public class EnemyManager implements IZFManager
{
   public var play:Play;
   public var onEnemyAdded:Signal;
   public var onEnemyRemoved:Signal;
   public var endOfEnemies:Signal;
   public var enemiesLeft:int;
   public var activeEnemies:int;
   public var delayCount:int = 0;
   public var onSpawnWave:Signal;
 
   private var _enemies:Array;
   private var _canvas:Sprite;
 
   // Holds the current map's enemy groups
   private var _enemyGroups:Object;
 
   // Holds the current map's enemy types
   private var _enemyTypes:Object;
 
   private var _enemyWaves:Object;
 
   public function EnemyManager(playState:Play)
   {
      play = playState;
      _canvas = play.enemyLayer;
      _enemies = [];
      activeEnemies = 0;
 
      onEnemyAdded = new Signal(Enemy);
      onEnemyRemoved = new Signal(Enemy);
      endOfEnemies = new Signal();
      onSpawnWave = new Signal(String);
 
      _enemyGroups = {};
      _enemyTypes = {};
      _enemyWaves = {};
   }
 
   public function update():void {
      if(_enemies.length > 0) {
         var e:Enemy;
         var len:int = _enemies.length;
         for(var i:int = len - 1; i >= 0; i--) {
            e = _enemies[i];
            e.update();
            if(e.isEscaped) {
               _handleEnemyEscaped(e);
            }
         }
      }
   }
 
   public function onGamePaused():void {
      _pauseGroups();
   }
 
   public function onGameResumed():void {
      _resumeGroups();
   }
 
   private function _pauseGroups():void {
      for each(var group:EnemyGroup in _enemyGroups) {
         group.pauseGroup();
      }
   }
 
   private function _resumeGroups():void {
      for each(var group:EnemyGroup in _enemyGroups) {
         group.resumeGroup();
      }
   }

Continuing EnemyManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
   public function destroyEnemy(e:Enemy):void {
      var len:int = _enemies.length;
      for(var i:int = 0; i < len; i++) {
         if(e == _enemies[i]) {
            Config.log('Enemy', 'destroyEnemy', "Destroying Enemy " + e.uid);
            _enemies.splice(i, 1);
            e.destroy();
            e.removeFromParent(true);
         }
      }
   }
 
   private function _spawn(e:Enemy, wpGroup:String):void {
      var waypoints:Array = play.wpMgr.getWaypointsByGroup(wpGroup);
      var totalDist:Number = play.wpMgr.getRouteDistance(wpGroup);
 
      Config.totals.enemiesSpawned++;
 
      e.init(waypoints, totalDist);
 
      e.onBanished.add(handleEnemyBanished);
 
      _enemies.push(e);
 
      activeEnemies++;
 
      _canvas.addChild(e);
 
      onEnemyAdded.dispatch(e);
   }
 
   public function spawnWave(waveId:String):void {
      Game.soundMgr.playFx('ding1', Config.sfxVolume);
 
      onSpawnWave.dispatch(waveId);
 
      Config.changeCurrentWave(1);
 
      for each(var groupName:String in _enemyWaves[waveId]) {
         _enemyGroups[groupName].startGroup();
      }
   }
 
   private function _handleEnemyEscaped(e:Enemy):void {
      enemiesLeft--;
      activeEnemies--;
 
      Config.totals.enemiesEscaped++;
 
      Config.changeCurrentHP(-e.damage);
      destroyEnemy(e)
      onEnemyRemoved.dispatch(e);
 
      if(enemiesLeft <= 0) {
         endOfEnemies.dispatch();
      }
   }
 
   public function handleEnemyBanished(e:Enemy):void {
      Config.log('EnemyManager', 'handleEnemyBanished', 'Enemy ' + e.uid + " is being destroyed");
      enemiesLeft--;
      activeEnemies--;
 
      Config.totals.enemiesBanished++;
      Config.changeCurrentGold(e.reward);
 
      onEnemyRemoved.dispatch(e);
      destroyEnemy(e);
 
      if(enemiesLeft <= 0) {
         endOfEnemies.dispatch();
      }
   }

And finally, to finish EnemyManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
   public function handleNewMapData(data:Object):void {
      var type:Object,
          group:Object,
          enemyType:EnemyType,
          enemy:*;
 
      // Create all enemy types
      for each(type in data.enemyTypes) {
         _enemyTypes[type.id] = new EnemyType(type.id, type.name, type.klass, type.sounds);
      }
 
      Config.maxWave = 0;
 
      // Create enemy wave mappings
      for each(var wave:Object in data.enemyWaves) {
         _enemyWaves[wave.id] = [];
         if(wave.groups.indexOf(',') != -1) {
            var groups:Array = wave.groups.split(',');
            _enemyWaves[wave.id] = groups;
         } else {
            _enemyWaves[wave.id].push(wave.groups);
         }
         Config.maxWave++;
      }
 
      // Create all enemy groups
      for each(group in data.enemyGroups) {
         _enemyGroups[group.id] = new EnemyGroup(group.id, group.name, group.waypointGroup, group.spawnDelay, group.wave, group.enemies);
         _enemyGroups[group.id].onSpawnTimerTick.add(onGroupSpawnTimerTick);
         _enemyGroups[group.id].onSpawnTimerComplete.add(onGroupSpawnTimerComplete);
      }
      // Create all actual enemies
      for each(group in _enemyGroups) {
         for each(var enemyObj:Object in group.enemies) {
            // get the enemyType
            enemyType = _enemyTypes[enemyObj.typeId];
 
            // Creates a new enemy type from the fullClass name
            var newEnemy:Enemy = new (getDefinitionByName(enemyType.fullClass) as Class)();
            newEnemy.setSoundData(enemyType.soundData);
 
            enemiesLeft++;
 
            // push new enemy onto object array
            group.enemyObjects.push(newEnemy);
         }
 
         // reverse array
         group.enemyObjects.reverse();
      }
   }
 
   public function onGroupSpawnTimerTick(e:Enemy, wpGroup:String):void {
      Config.log('EnemyManager', 'onGroupSpawnTimerTick', "onGroupSpawnTimerTick: " + e);
      _spawn(e, wpGroup);
   }
 
   public function onGroupSpawnTimerComplete(e:Enemy):void {
      Config.log('EnemyManager', 'onGroupSpawnTimerComplete', "onGroupSpawnTimerComplete: " + e);
   }
 
   public function destroy():void {
      Config.log('EnemyManager', 'destroy', "EnemyManager Destroying");
      _enemyTypes = null;
 
      var group:Object;
      for each(group in _enemyGroups) {
         group.destroy();
      }
      _enemyGroups = null;
 
      var len:int = _enemies.length;
      for(var i:int = 0; i < len; i++) {
         _enemies[i].destroy();
      }
 
      _enemies = null;
 
      Config.log("EnemyManager", "destroy", "EnemyManager Destroyed");
   }
}
}

There we go… That’s the full run-down on Enemies in my code! Don’t forget to check out the HealthBar class I made! It’s pretty simple, just some rectangle drawing code.

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

Travis

Share

AS3 Starling Tower Defense Tutorial – Part 6 – Towers

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

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

Before we start, feel free to check out the finished product of my AS3 Starling TD Demo.
You can also find all of the code used in srcview.
Or you can download the whole project zipped up.
Or check out the repo on Bitbucket

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

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

JSON Data Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
   "towers": [
      {
         "id": "tower1",
         "index": 0,
         "name": "Blue Tower",
         "imageName": "towers/tower1_001",
         "bulletImageName": "bullets/sparkle_blue_001",
         "bulletSpeed": 12,
         "bulletWidth": 32,
         "bulletHeight": 32,
         "towerWidth": 20,
         "towerHeight": 20,
         "maxLevel": 3,
         "sounds": [
            { 
               "state": "onFire",
               "soundId": "shot1"
            }
         ],
         "levelData": [
            {
               "level": 1,
               "range": 75,
               "damage": 5,
               "speed": 1500,
               "dps": 3.33,
               "cost": 10
            },
            {
               "level": 2,
               "range": 150,
               "damage": 10,
               "speed": 1250,
               "dps": 8,
               "cost": 30
            },
            {
               "level": 3,
               "range": 200,
               "damage": 20,
               "speed": 900,
               "dps": 22.22,
               "cost": 50
            }
         ]
      }
   ]
}

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

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

TowerManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.zf.managers
{
   import com.zf.core.Config;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.tower.Tower;
   import com.zf.states.Play;
 
   import flash.geom.Point;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Sprite;
   import starling.events.Touch;
   import starling.events.TouchEvent;
   import starling.events.TouchPhase;
 
public class TowerManager implements IZFManager
{
   public var play:Play;
   public var onTowerRemoved:Signal;
   public var onTowerAdded:Signal;
   public var onTowerManagerRemovingEnemy:Signal;
 
   private var _towers:Array;
   private var _towerData:Object;
   private var _currentTower:Tower;
   private var _p:Point = new Point();
   private var _isDragging:Boolean = false;
   private var _canvas:Sprite;
 
   public function TowerManager(playState:Play, towerData:Object) {
      play = playState;
      _canvas = play.towerLayer;
      _towers = [];
      _setTowerData(towerData);
 
      onTowerAdded = new Signal(Tower);
      onTowerRemoved = new Signal(Tower);
      onTowerManagerRemovingEnemy = new Signal(Enemy);
   }
 
   public function update():void {
      if(_towers.length > 0) {
         var t:Tower,
             len:int = _towers.length;
         for(var i:int = len - 1; i >= 0; i--) {
            t = _towers[i];
            t.update();
         }
      }
   }
 
   public function destroy():void {}
 
   public function destroyTower(t:Tower):void {
      var len:int = _towers.length;
      for(var i:int = 0; i < len; i++) {
         if(t == _towers[i]) {
            _towers.splice(i, 1);
            t.destroy();
            t.removeFromParent(true);
         }
      }
      onTowerRemoved.dispatch(t);
   }
 
   public function createNewTower(towerID:String, pos:Point):void {
      if(_currentTower) {
         _currentTower.deactivate();
      }
 
      var tower:Tower = new Tower(_towerData[towerID], this);
      tower.setSoundData(_towerData[towerID].sounds);
      tower.x = pos.x - tower.halfWidth;
      tower.y = pos.y - tower.halfHeight;
      tower.activated = true;
      _currentTower = tower;
 
      play.addChild(tower);
      play.addEventListener(TouchEvent.TOUCH, towerFollowMouse);
   }
 
   public function towerFollowMouse(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(play);
      if(touch)
      {
         switch(touch.phase) {
            case TouchPhase.BEGAN:
               var checkObject:Object = play.map.checkCanPlaceTower(_p);
               if(checkObject.canPlace) {
                  play.removeEventListener(TouchEvent.TOUCH, towerFollowMouse);
                  evt.stopImmediatePropagation();
                  placeTower(checkObject.point);
               }
               break;
 
            case TouchPhase.ENDED:
               break;
 
            case TouchPhase.MOVED:
               break;
 
            case TouchPhase.HOVER:
               var tPos:Point = touch.getLocation(play);
               _currentTower.x = _p.x = tPos.x - _currentTower.halfWidth;
               _currentTower.y = _p.y = tPos.y - _currentTower.halfHeight;
 
               // Check if we can place, then update the tower image accordingly
               _currentTower.enterFrameTowerPlaceCheck(play.map.checkCanPlaceTower(_p).canPlace);
               break;
         }
      }
   }

Quick break… then continuing with TowerManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
   public function removeEnemyFromTowers(e:Enemy):void {
      Config.log('TowerManager', 'removeEnemyFromTowers', "Removing Enemy " + e.uid + " from Towers");
      var len:int = _towers.length;
      for(var i:int = 0; i < len; i++) {
         _towers[i].removeEnemyInRange(e);
      }
      onTowerManagerRemovingEnemy.dispatch(e);
   }
 
   public function addListenerToDeactivateTower(t:Tower):void {
      if(_currentTower != t) {
         // handle previous _currentTower
         Config.log('TowerManager', 'addListenerToDeactivateTower', 'TowerManager.addListenerToDeactivateTower() -- deactivating old tower');
         _currentTower.deactivate();
      }
      _currentTower = t;
      Config.log('TowerManager', 'addListenerToDeactivateTower', "TowerManager.addListenerToDeactivateTower() -- adding listener: " + play.map.width + ", " + play.map.height);
      play.map.addEventListener(TouchEvent.TOUCH, _onMapClicked);
   }
 
   public function placeTower(p:Point):void {
      Config.totals.towersPlaced++;
 
      var xOffset:int = int((play.map.tileWidth - (_currentTower.halfWidth << 1)) >> 1) + play.mapOffsetX;
      var yOffset:int = int((play.map.tileHeight - (_currentTower.halfHeight << 1)) >> 1) + play.mapOffsetY;
 
      _currentTower.x = p.x + xOffset;
      _currentTower.y = p.y + yOffset;
 
      _canvas.addChild(_currentTower);
      play.map.placedTower(_currentTower);
      _towers.push(_currentTower);
      _currentTower.init();
      _currentTower.onFiring.add(play.bulletMgr.onTowerFiring);
      onTowerAdded.dispatch(_currentTower);
   }
 
   public function sellTower(t:Tower):void {
      // Add half the cost back to currentGold This can be modified later!
      Config.changeCurrentGold(int(t.cost >> 1));
      destroyTower(t);
   }
 
   private function _onMapClicked(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(play.map, TouchPhase.BEGAN);
      if (touch)
      {
         var localPos:Point = touch.getLocation(play.map);
         Config.log('TowerManager', '_onMapClicked', "mapClicked! " + play.map.width + " -- " + localPos.x);
         // if we clicked anywhere but the tower
         if(touch.target != _currentTower) {
            play.hudMgr.hideTowerData(_currentTower);
            _currentTower.deactivate();
            play.map.removeEventListener(TouchEvent.TOUCH, _onMapClicked);
         }
      }
   }
 
   public function getTowerCost(towerID:String, level:int):int {
      return _towerData[towerID].levelData[level].cost;
   }
 
   private function _setTowerData(td:Object):void {
      _towerData = {};
      for each(var data:Object in td.towers) {
         _towerData[data.id] = data;
      }
   }
 
   // If TowerManager needs to do anything on Pause/Resume
   public function onGamePaused():void {}
   public function onGameResumed():void {}
}
}

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

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

Tower.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package com.zf.objects.tower
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.managers.TowerManager;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.tower.strategies.*;
   import com.zf.states.Play;
 
   import flash.display.Bitmap;
   import flash.display.BitmapData;
   import flash.display.Shape;
   import flash.events.TimerEvent;
   import flash.geom.Matrix;
   import flash.utils.Timer;
   import flash.utils.clearInterval;
 
   import org.osflash.signals.Signal;
 
   import starling.core.Starling;
   import starling.display.Image;
   import starling.display.MovieClip;
   import starling.display.Sprite;
   import starling.events.Touch;
   import starling.events.TouchEvent;
   import starling.events.TouchPhase;
 
public class Tower extends Sprite
{
   public static const TOWER_STATE_NEW	  : int = 0;
   public static const TOWER_STATE_INIT	  : int = 1;
   public static const TOWER_STATE_READY  : int = 2;
   public static const TOWER_STATE_RELOAD : int = 3;
   public static const TOWER_STATE_FIRING : int = 4;
 
   public static const TOWER_ANIM_STATE_SAME:String = 'towerAnimStateSame';
 
   public static const TOWER_STRAT_NEAREST:String = 'towerStrategyNearest';
 
   // Sound states
   public static const TOWER_SND_STATE_FIRE:String = 'onFire';
 
   public var uid:int;
 
   public var towerName:String;
   public var index:int;
   public var level:int;
   public var range:Number;
   public var damage:Number;
   public var speed:Number;
   public var dps:Number;
   public var cost:int;
   public var maxLevel:int;
 
   public var halfWidth:Number;
   public var halfHeight:Number;
   public var state:int = TOWER_STATE_NEW;
   public var onFiring:Signal;
   public var bulletType:String;
   public var bulletSpeed:int;
   public var bulletWidth:int;
   public var bulletHeight:int;
   public var centerX:Number;
   public var centerY:Number;
   public var towerStrategy:ITowerStrategy;
   public var levelData:Object;
   public var nextDamage:Number;
 
   public var onUpgrade:Signal;
 
   // Range distance Squared
   public var rangeSquared:Number;
 
   // If this tower is currently clicked on
   public var activated:Boolean = false;
 
   protected var _animState:String;
   protected var _animData:Object;
   protected var _imageName:String;
   protected var _bulletImageName:String;
 
   protected var _rangeRing  : Image;
   protected var _badRangeRing : Image;
   protected var _rangeRingDepthIndex:int;
 
   protected var _reloadTimer:Timer;
 
   protected var _soundData:Object;
 
   // reload timer delay with game speed factored in
   protected var _reloadTimerGameSpeedDelay:Number;
 
   // Enemies in range
   protected var _enemiesObj:Object;
   protected var _enemies:Array;
 
   protected var _currentRange:int;
   protected var _lowestSqrt:Number;
 
   // Range ring graphics options
   protected var _rangeRingFillColor:int = 0X009999;
   protected var _badRangeRingFillColor:int = 0XFF0000;
   protected var _borderColor:int = 0X000000;
   protected var _borderSize:int = 1;
 
 
   protected var _mgr:TowerManager
 
   private var _interval:uint;
 
   public function Tower(towerData:Object, tMgr:TowerManager)
   {
      uid              = Config.getUID();
      _mgr             = tMgr;
      towerName        = towerData.name;
      index            = towerData.name;
      level            = 1;
      range            = towerData.levelData[0].range;
      damage           = towerData.levelData[0].damage;
      speed            = towerData.levelData[0].speed;
      cost             = towerData.levelData[0].cost;
      dps	       = towerData.levelData[0].dps;
      maxLevel	       = towerData.maxLevel;
      _imageName       = towerData.imageName;
      _bulletImageName = towerData.bulletImageName;
      halfWidth        = towerData.towerWidth >> 1;
      halfHeight       = towerData.towerHeight >> 1;
      bulletType       = towerData.bulletImageName;
      bulletSpeed      = towerData.bulletSpeed;
      bulletWidth      = towerData.bulletWidth;
      bulletHeight     = towerData.bulletHeight;
 
      _updateStatsByUpgrades();
 
      nextDamage = 0;
 
      onUpgrade = new Signal(Tower);
      onFiring = new Signal(Tower, Enemy);
 
      _enemiesObj = {};
      _enemies = [];
 
      _soundData = {};
 
      resetTowerAfterUpgrade();
 
      _parseLevelData(towerData.levelData);
      _setupAnimData();
      _createRangeRing();
      _createBadRangeRing();
 
      setTowerStrategy(new TowerStrategyNearest());
   }
 
   protected function resetTowerAfterUpgrade():void {
      rangeSquared = range * range;
 
      // set up reloadTimer
      _reloadTimerGameSpeedDelay = Config.currentGameSpeed * speed;
      _reloadTimer = new Timer(_reloadTimerGameSpeedDelay, 1);
      _reloadTimer.addEventListener(TimerEvent.TIMER_COMPLETE, reloadDoneReadyTower, false, 0, true);
   }
 
   protected function _parseLevelData(data:Array):void {
      levelData = {};
      for each(var tData:Object in data) {
         levelData[tData.level] = tData;
      }
   }
 
   public function init():void {
      _interval = flash.utils.setInterval(_init, 100);
   }
 
   protected function _init():void {
      // clear the stupid interval because of the touchevents
      flash.utils.clearInterval(_interval);
 
      hideRangeRing();
 
      // let tower listen for game speed change
      Config.onGameSpeedChange.add(onGameSpeedChange);
 
      centerX = this.x + halfWidth;
      centerY = this.y + halfHeight;
 
      // since we dont need this anymore, null it out
      _badRangeRing.visible = false;
      _badRangeRing = null;
 
      _addListeners();
      state = TOWER_STATE_READY;
   }
 
   public function setTowerStrategy(strat:ITowerStrategy):void {
      towerStrategy = strat;	
   }

Splitting Tower.as up… and continuing…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
   public function update():void {
      if(state == TOWER_STATE_READY && _enemies.length > 0) {
         var targetEnemy:Enemy = towerStrategy.findEnemy(_enemies);
 
         if(targetEnemy) {
            _generateNextShotDamage();
            if(targetEnemy.willDamageBanish(nextDamage)) {
               _mgr.removeEnemyFromTowers(targetEnemy);
            }
            fireBullet(targetEnemy);
         }
      }
   }
 
   public function setSoundData(soundData:Array):void {
      var len:int = soundData.length;
      for(var i:int = 0; i < len; i++) {
         _soundData[soundData[i].state] = soundData[i].soundId;
      }
   }
 
   protected function _generateNextShotDamage():void {
      nextDamage = damage;
   }
 
   protected function fireBullet(targetEnemy:Enemy):void {
      state = TOWER_STATE_FIRING;
      _playIfStateExists(TOWER_SND_STATE_FIRE);
      onFiring.dispatch(this, targetEnemy);
      reload();
   }
 
   protected function _setupAnimData():void {
      _animData = {};
 
      _animData[TOWER_ANIM_STATE_SAME] = new MovieClip(Assets.ta.getTextures(_imageName), 8);
 
      _changeAnimState(TOWER_ANIM_STATE_SAME);
   }
 
   public function enterFrameTowerPlaceCheck(canPlace:Boolean):void {
      if(canPlace) {
         _rangeRing.visible = true;
         _badRangeRing.visible = false;
      } else {
         _rangeRing.visible = false;
         _badRangeRing.visible = true;
      }
   }
 
   public function addEnemyInRange(e:Enemy, range:Number):void {
      Config.log('Tower', 'addEnemyInRange', "Tower " + uid + " Adding Enemy " + e.uid + " in range");
      _enemiesObj[e.uid] = e;
      e.onBanished.add(removeEnemyInRange);
      _enemies.push(e);
   }
 
   public function removeEnemyInRange(e:Enemy):void {
      if(_enemiesObj[e.uid]) {
         e.onBanished.remove(removeEnemyInRange);
 
         // delete object version
         delete _enemiesObj[e.uid];
 
         // remove array version
         var len:int = _enemies.length;
         for(var i:int = 0; i < len; i++) {
            if(e == _enemies[i]) {
               _enemies.splice(i, 1);
            }
         }
      }
   }
 
   public function canUpgrade():Boolean {
      var canUp:Boolean = false;
 
      if(level < maxLevel && levelData[level + 1].cost <= Config.currentGold) {
         canUp = true;
      }
 
      return canUp;
   }

Ok, one final break and let’s finish up Tower.as…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
   public function upgrade():void {
      level++;
      range = levelData[level].range;
      damage = levelData[level].damage;
      speed = levelData[level].speed;
      dps = levelData[level].range;
      cost = levelData[level].cost;
 
      // apply upgrade bonuses
      _updateStatsByUpgrades();
 
      // remove (add a negative cost) gold to currentGold in Config
      Config.changeCurrentGold(-cost);
 
      // redraw the range ring
      _createRangeRing();
      resetTowerAfterUpgrade();
      onUpgrade.dispatch(this);
   }
 
   public function hasEnemyInRange(e:Enemy):Boolean {
      return (_enemiesObj[e.uid]);
   }
 
   public function showRangeRing():void {
      _rangeRing.visible = true;
   }
 
   public function hideRangeRing():void {
      _rangeRing.visible = false;
   }
 
   protected function _changeAnimState(newState:String):void {
      // make sure they are different states before removing and adding MCs
      if(_animState != newState) {
         // remove the old MovieClip from juggler
         Starling.juggler.remove(_animData[_animState]);
         // remove the old MovieClip from this Sprite
         removeChild(_animData[_animState]);
 
         _animState = newState;
 
         // add the new MovieClip to the Juggler
         Starling.juggler.add(_animData[_animState]);
         // add the new MovieClip to the Sprite
         addChild(_animData[_animState]);
      }
   }
 
   protected function _addListeners():void {
      addEventListener(TouchEvent.TOUCH, _onTowerSelected);
      state = TOWER_STATE_READY;
   }
 
   protected function _removeListeners():void {
      removeEventListener(TouchEvent.TOUCH, _onTowerSelected);
   }
 
   public function destroy():void {
      // remove the old MovieClip from juggler
      Starling.juggler.remove(_animData[_animState]);
      // remove the old MovieClip from this Sprite
      removeChild(_animData[_animState]);
   }
 
   protected function reload():void {
      state = TOWER_STATE_RELOAD;
      _reloadTimer.reset();
      // If game speed has changed, update the delay after the tower is done reloading
      _reloadTimer.delay = _reloadTimerGameSpeedDelay;
 
      _reloadTimer.start();
   }
 
   public function onGameSpeedChange():void {
      // update the game speed timer
      _reloadTimerGameSpeedDelay = int(speed / Config.currentGameSpeed);
      _reloadTimer.delay = _reloadTimerGameSpeedDelay - _reloadTimer.currentCount;
   }
 
   protected function reloadDoneReadyTower(evt:TimerEvent):void {
      state = TOWER_STATE_READY;
   }
 
   public function activate():void {
      activated = true;
      showRangeRing();
      _mgr.addListenerToDeactivateTower(this);
      _mgr.play.hudMgr.showTowerData(this);
   }
 
   public function deactivate():void {
      activated = false;
      hideRangeRing();
      _addListeners();
   }
 
   private function _updateStatsByUpgrades():void {
      range = Config.currentGameSOData.applyUpgradeToValue(range, 'towerRng');
      speed = Config.currentGameSOData.applyUpgradeToValue(speed, 'towerSpd');
      damage = Config.currentGameSOData.applyUpgradeToValue(damage, 'towerDmg');
   }
 
   protected function _onTowerSelected(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(this, TouchPhase.BEGAN);
      if (touch)
      {
         _removeListeners();
         activate();
      }
   }
 
   protected function _playIfStateExists(state:String):void {
      if(_soundData.hasOwnProperty(state)) {
         Game.soundMgr.playFx(_soundData[state], Config.sfxVolume);
      }
   }
 
   protected function _createRangeRing():void {
      if(contains(_rangeRing)) {
         _rangeRing.removeFromParent(true);
      }
 
      var _s:Shape = new Shape();
      _s.graphics.lineStyle(1);
      _s.graphics.beginFill(_rangeRingFillColor);
      _s.graphics.lineStyle(_borderSize , _borderColor);
      _s.graphics.drawCircle(0, 0, range);
 
      var matrix:Matrix = new Matrix();
      matrix.tx = _s.width >> 1;
      matrix.ty = _s.height >> 1;
 
      var _bmd:BitmapData = new BitmapData(_s.width << 1, _s.height << 2) + halfWidth;
      _rangeRing.y = -(_rangeRing.height >> 2) + halfHeight;
 
      _rangeRingDepthIndex = numChildren - 1;
      _rangeRing.touchable = false;
 
      addChildAt(_rangeRing, _rangeRingDepthIndex);
   }
 
   protected function _createBadRangeRing():void {
      var _s:Shape = new Shape();
      _s.graphics.lineStyle(1);
      _s.graphics.beginFill(_badRangeRingFillColor);
      _s.graphics.lineStyle(_borderSize, _borderColor);
      _s.graphics.drawCircle(0, 0, range);
 
      var matrix:Matrix = new Matrix();
      matrix.tx = _s.width >> 1;
      matrix.ty = _s.height >> 1;
 
      var _bmd:BitmapData = new BitmapData(_s.width << 1, _s.height << 2) + halfWidth;
      _badRangeRing.y = -(_badRangeRing.height >> 2) + halfHeight;
      _badRangeRing.touchable = false;
 
      addChildAt(_badRangeRing , numChildren - 1);
   }
}
}

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

Strategy Design Pattern

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

If you overuse them…

Factory Pattern Ski Instructor

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

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

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

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

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

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

ITowerStrategy.as

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

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

Now we get to the implementation…

TowerStrategyNearest.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.zf.objects.tower.strategies
{
   import com.zf