Starling AS3 Animated Bitmap Builder

So I’ve been pretty deep building onto my Starling TD tutorial code to make an actual TD game, and every once in a while you have to just make something small and fun. I wrote a little Bitmap builder class where you pass in an image, a layer to draw it on, and some params, and the class takes apart your image in pixel “chunks” and redraws those chunks back onto the draw layer.

Take a look at the DEMO before checking out the code. Change some settings and click the “Draw” button in the bottom right.

You can also download the project folder.

I’m going to skip the boring setup part of the code and just jump right into the actual utility.

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
package com.zf.utils
{
   import flash.geom.Rectangle;
 
   import starling.animation.Transitions;
   import starling.core.Starling;
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.textures.SubTexture;
   import starling.textures.Texture;
 
   public class BitmapBuilder
   {
      public static const BUILD_BOTTOM_UP:String = 'bottomUp';
      public static const BUILD_TOP_DOWN:String = 'topDown';
      public static const START_BOTTOM:String = 'bottom';
      public static const START_RANDOM:String = 'random';
 
      private static var image:Image;
      private static var buildType:String;
      private static var callBack:Function;
      private static var chunkSize:int;
      private static var canvas:Sprite;
      private static var pieceTransition:String;
      private static var totalTime:Number;
      private static var startDir:String;
      private static var pieces:Array;
      private static var pieceStartAlpha:Number;
      private static var pieceStartRotation:Number;
      private static var pieceStartScaleX:Number;
      private static var pieceStartScaleY:Number;
      private static var piecesComplete:int;
 
      public function BitmapBuilder() {}
 
      /**
       * BitmapBuilder's only public function. It gets called to parse and draw the Sprite in pieces.
       * 
       * @param {Image} img The Image we're pulling apart piece by piece to redraw
       * @param {Sprite} layer The Sprite to draw things to
       * @param {Object} [params=null] The params for the starting values of the build
       */
      public static function build(img:Image, layer:Sprite, params:Object = null):void {
         image = img;
         canvas = layer;
 
         // setup individual params and their defaults if no values were passed in
         callBack = params.onComplete || null;
         buildType = params.buildType || BUILD_BOTTOM_UP;
         pieceTransition = params.transition || Transitions.LINEAR;
         totalTime = params.time || 3;
         startDir = params.startDirection || START_BOTTOM;
         chunkSize = params.chunkSize || 5;
         pieceStartAlpha = params.pieceStartAlpha || 0;
         pieceStartRotation = params.pieceStartRotation || 0;
         pieceStartScaleX = params.pieceStartScaleX || 1;
         pieceStartScaleY = params.pieceStartScaleY || 1;
 
         pieces = [];
         piecesComplete = 0;
 
         var w:int = image.width,
             h:int = image.height,
             wCt:int = 0,
             hCt:int = 0,
             rect:Rectangle,
             piece:Image,
             imgT:Texture = image.texture;
 
         while(hCt < h) 
         {
            while(wCt < w) 
            {
               rect = new Rectangle(wCt, hCt, chunkSize, chunkSize);
               piece = new Image(new SubTexture(imgT, rect));
               pieces.push(piece);
 
               wCt += chunkSize;
            }
 
            wCt = 0;
            hCt += chunkSize;
         }
         _draw();
      }
 
      /**
       * Actually does the drawing of the bitmap onto the canvas
       */
      private static function _draw():void {
         var len:int = pieces.length,
             w:int = image.width,
             h:int = image.height,
             xStart:int,
             yStart:int,
             xEnd:int,
             yEnd:int,
             rows:int = Math.ceil(h / chunkSize),
             cols:int = Math.ceil(w / chunkSize),
             row:int,
             col:int,
             pieceTime:Number = totalTime / len,
             time:Number;
 
         for(var i:int = 0; i < len; i++) {
            row = i % rows;
            col = int(i / cols);
 
            // get the end x/y position of the chunk based on the row/col
            xEnd = row * chunkSize;
            yEnd = col * chunkSize;
 
            // get the start position of the chunk based on the startDirection
            if(startDir == START_BOTTOM) {
               xStart = xEnd;
               yStart = rand(h - (h / 4), h + (h / 4));
            } else if(startDir == START_RANDOM) {
               xStart = rand((w / 2) - w, w + (w / 2));
               yStart = rand((h / 2) - h, h + (h / 2));
            }
 
            // add piece to draw layer
            canvas.addChild(pieces[i]);
 
            // set piece starting values
            pieces[i].x = xStart;
            pieces[i].y = yStart;
            pieces[i].scaleX = pieceStartScaleX;
            pieces[i].scaleY = pieceStartScaleY;
            pieces[i].rotation = pieceStartRotation;
            pieces[i].alpha = pieceStartAlpha;
 
            time = pieceTime * i;
            if(buildType == BUILD_BOTTOM_UP) {
               time = totalTime - time;
            } 
 
            Starling.juggler.tween(pieces[i], time, {
                  transition: pieceTransition,
                  x: xEnd,
                  y: yEnd,
                  scaleX: 1,
                  scaleY: 1,
                  rotation: 0,
                  alpha: 1, 
                  onComplete: onPieceTweenComplete
            });
         }
      }
 
      /**
       * Callback handler for when each individual piece completes its Tween
       */
      private static function onPieceTweenComplete():void {
         piecesComplete++;
         if(piecesComplete == pieces.length && callBack) {
            callBack()
         }
      }
 
      /**
       * Generates a random number between min and max
       * 
       * @param {Number} min The minimum value for the random number
       * @param {Number} min The maximum value for the random number
       */
      private static function rand(min:Number, max:Number):Number {
         return (Math.floor(Math.random() * (max - min + 1)) + min);
      }
   }
}
  • Line 48-57 – From the main Game.as class, we’re setting a lot of values in the UI for how we want this tree to animate. In these lines we’re setting our internal values from those user-set values (or using defaults if a user didn’t selecting something, or cleared out a text input)
  • Line 48 – callback is the “onComplete” callback function from Game.as when we complete drawing the bitmap
  • Line 49 – buildType is how we’re wanting to rebuild the image, from the top down, or from the bottom up
  • Line – pieceTransition is the standard Transistion type (“linear”, “easeIn”, “easeOut”, etc) to use to animate the pieces
  • Line – totalTime is the total time the animation should take. The lower the number, the faster the pieces will animate in.
  • Line – startDir is which direction the pieces should animate in from
  • Line – chunkSize is the number of pixels we’re batching together in a “chunk”. If you had a 50×50 image and chunk was set to 10 (meaning 10px by 10px per “chunk”) you’d have a total of 25 chunks that were going to be animated. The lower you set this, the more pieces will be animating, but the lower you set this, the more you’re going to see performance issues. There’s a fine line between a cool number of pieces, and poor performance
  • Line 54-57 – starting alpha, rotation, and scale for each piece
  • Line 70-83 – looping over the rows/columns and adding the new pieces to our array to draw
  • Line 105-148 – looping over the pieces to add them to the stage and animate
  • Line 106-107 – getting the proper row and column position of the piece
  • Line 138-147 – adding the piece to the Starling Juggler and tweening to the proper end position/scale/rotation/alpha

That’s pretty much it! Make sure you check out the demo to see the finished product!

The tree pixel art image used comes from Mpaeva at Society6 and is used with permission by the artist. Feel free to check out his fine work there!

Share

AS3 Starling Timer vs. DelayedCall

As a long-time AS3 coder, I’m very used to using AS3’s Timer class. For my Starling game tutorial, I even extended it and created the GameTimer class where I added the ability to pause the timer and some other things. As I’ve been working more on my game based on my tutorial engine, I’ve realized that I’ve made a huge mistake in the way I coded the Timers. Namely, in Starling… well…

Where we're going, we dont need... Timers

The Scenario

Let’s say you’re implementing an enemy spawn timer into your game. You’ve got a number of enemies in the wave (say, 5) and a set amount of time (say, 1500ms) that you want to wait in between spawning each enemy. Without Starling, you would be using some variation of AS3’s Timer class, and your implementation would look something like the following:

1
2
3
4
5
6
7
8
9
10
11
// create a new Timer instance that has a 1500ms delay and repeats 5 times
var timer:Timer = new Timer(1500, 5);
 
// a new enemy spawns every time onTimerTick gets called
timer.addEventListener(TimerEvent.TIMER, onTimerTick);
 
// to keep track of when the whole enemy wave is done spawning
timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimerComplete);
 
// when ready to start the next wave...
timer.start();

While this would work perfectly fine in your Starling game, Starling has a secret Timer class built-in called DelayedCall. I call it “secret” because it exists in the starling.animation package (our spawn timer isn’t animated why would we use something from animation?) and doesn’t immediately jump out at you when looking over the API docs that this secret class can be used as a timer, but it’s exactly what the class is written for.

So first, why would you not want to just use Timer? Let’s use a band analogy. You’re the guitarist in a kickass rock band. Way to go! Now, the drummer is going to keep a steady beat (all drummers do, right?) that everyone else in the band is going to follow for the song. You, however, decide that you’re going to start a metronome exactly in time with that first drum beat, and you’re only going to listen to the metronome. Sure, at first you’ll probably be exactly in sync. But what happens when you’ve practiced that sweet break where the music stops, then kicks back in but not on the same beat your metronome is set to? Now you’re screwed. The drummer keeps the time man, all the ladies in the audience know that… if you start wailing on your guitar off beat, it doesn’t matter how good that solo it is… you’re off beat man.

Starling’s IAnimatable interface ensures that you can hear that steady drum beat to keep everything in your game in sync. Adding your own Timer class is you being that guitarist that’s got his own time on the metronome… sure it’s in sync now, but what happens in that mobile app when a user clicks the home button, then goes back to the game later? Did your timer stop? Did you make sure you stopped every separate timer when your app lost context? Did you leave the gas on?

There’s no need to worry if you implement IAnimatable in your classes and I’ll be happy to show you how! Let’s look at that same enemy spawn timer but using Starling’s DelayedCall class.

1
2
3
4
5
6
7
8
9
10
11
12
// create a new DelayedCall instance that has a 1.5s delay 
// and uses the function onTimerTickDC as it's callback (onTimerTickDC gets called every 1.5s)
var timerDC:DelayedCall = new DelayedCall(onTimerTickDC, 1.5);
 
// set the repeatCount of the DelayedCall to 5
timerDC.repeatCount = 5;
 
// to keep track of when the whole enemy wave is done spawning
timerDC.addEventListener(Event.REMOVE_FROM_JUGGLER, onTimerCompleteDC);
 
// when ready to start the next wave...
Starling.juggler.add(timerDC);

You may notice a few things right off that are different.

A final difference to note is that in the DelayedCall constructor, you can pass in an Array of args as the 3rd argument. Those args will then get passed to the callback function. I have not found a use for these so I didn’t include it in the list, but if you’ve got a use for callback args, I’d love to hear it!

I wrote a little project file if you’d like to see the code in action. You can find that here. Also, as I’m learning more Starling and writing better code, I’ll be going back through my tutorial to update it to the more “proper” ways to do things.

Share

AS3 Starling Tower Defense Tutorial – Part 11 – Conclusion

We made it! Ten blog posts written in one month, averaging about 5,200 words per post (though most of it is code) is definitely a new record for me. I’m glad to be done with this phase of the code. My plan was always to get this demo working, write a tutorial series so that other people can do this easier, then take the same codebase and continue building with it to make a few games. So now it’s time to get to make a few games! Woo!

I wanted to wrap up the series going through some things I learned along the way. Maybe “postmortem” would be a better title but that sounded a little pretentious since I just made a little demo, not a real game like the big boys. I’ll get there though soon enough.

Thoughts on Starling

I had started a tutorial series before this on creating a straight AS3 game engine. I had made it through about 3 posts, discussing loading assets, enemies, and generating your tile map by blitting “by hand” (well, without Starling). It was going in a good direction then I remembered something called Starling and that it was supposed to make games faster, better, more FPS, less visual lag, all GPU-jacked-up and a hot new engine on the block. I stopped that series (and even removed it from the site) because I wanted people to focus on this series; I wanted to focus on this series. I wanted to take Starling for a spin and see how it did, then I wanted to write about it because telling Biscuit, my kitty, all about flattening Sprites and game ticks with the Juggler, he was not amused. Maybe he was? I dunno, he kindof has the same face all the time. Smile Kitty, damn you! Anyways…

Being a Developer, I know there’s a learning curve with any new language, engine, or library that you’re wanting to try out. For some of these libraries, the learning curve is stupid easy. Greensock’s Tweening and Loading libraries, for example, I can’t praise enough. He’s got excellent documentation, he’s built tools that Give you the code to paste if you want something to move on your screen, you really couldn’t ask for more. Sometimes the learning curve is steep… Assembly anyone? I guess it’s incredibly subjective saying something has a steep or shallow learning curve. If I had come fresh to AS3 and Starling, learning both at the same time, I’m sure the difficulty would’ve gone up exponentially. However, I was pleasantly surprised at how easy Starling was to incorporate into my project.

If you were reading closely throughout the series, you may have caught a few snarky remarks at Starling’s expense. Starling is growing and evolving, it’s relatively new, and all things aren’t exactly the way I personally was hoping for them to be. Oh well, get over it dude, because the positives of what Starling does for me outnumber those few quirks and gaps in functionality that I was commenting on.

It’s funny, last night I was checking out Starling’s github repo, just browsing through the commit log to see what the latest stuff was. It was impeccable timing as the latest commit said something about HOVER events. What?! I was just posting on how this was a pain, and clearly they’re actively working on adding new features and fixing bugs daily. These guys are on it. The community forums are thriving and they’ve got some great mods and users over there that make sure things get answered and people aren’t just hanging.

So, come on… what’s the real conclusion? Should I use Starling for my project?

The bottom line with any new library, tech, app, etc is that they are all tools. If what Starling does solves your project’s problems, then it sounds like it’s the right tool for your project. If your project is to make a 3D game then Starling is not right for your project. If your project is to make a platformer game, sure Starling can help, but there’s also CitrusEngine that builds on TOP of Starling, and has a lot of built-in functionality for platformer games.

Realistic Expectations

I went into Starling with an unrealistic expectation about just what a GPU-accelerated Flash game would be like. I bought Particle Designer and started making some cool particle effects, nothing crazy, just 200 particles for a single bullet’s particle effect. Yeah… so with about 8 Towers on the stage shooting every second or so, 8-15 (200 particles each) bullets on stage at a time, with ~30 enemies being animated on the screen, I was incredibly disappointed to find that I was running around 4-8 FPS. That wasn’t some failure of Starling’s, but an unrealistic expectation of how things were going to handle. I scaled the bullet particles down about 90% to about 20-30 particles, and things obviously bounced back. Now, mind you, most of my “benchmark” tests were done in debugging mode with tracing to the console still turned on. Obviously not the ideal environment for testing things like that as once you turn off tracing and get the SWF into a browser, things change dramatically. But still, do not expect any engine to do everything for you and solve all your problems and keep you warm at night. Read up on what Starling does. Download and use Starling. Work through some tutorials. Starling gets a LOT of things right, in my opinion. Sure I may take issue with this little inconsistency or a little “why didn’t they add this functionality”, but at the end of the day, the reason I 100% completed everything I set out to do in this tutorial series is because I enjoyed working with Starling.

It was familiar and easy to work with. It gave me a very easy way to run a game consistently at 50-60 FPS with a healthy amount of entities rendering on the stage. It gave me a very easy way to handle assets via the TextureAtlas. I was really happy using the Texture Packer app and TextureAtlas. The package hierarchy was built exactly like Flash. You don’t realize how important that is to new folks incorporating Starling into their existing codebase. It just makes sense and makes things easy. Starling sped up my development by a decent amount. It handled a lot of things that I had previously written in my old engine, or was going to write, but now, didn’t need to.

I guess the point is, don’t think that because it’s GPU-accelerated that you’re home free, that you can code sloppily and the GPU will have your back. Because it won’t. You be nice to Starling… don’t feed it 2,000 bullet particles plus 30 enemies animating and expect that next game tick to be a happy one. It will puke right on your new pants, man.

Things I Got Wrong

As more people read the series, I’m sure I’ll have more things to add and update here.

KeyboardEvents

After going through Starling’s docs (not sure how I missed this before), Starling does have a KeyboardEvent class. Apparently starling.display.Stage WILL dispatch keyUp and keyDown events as you would expect them to and in the same manner as you’re accustomed in AS3. I have not used them, nor gone back to incorporate them into my demo, though for the next project I may update that. I don’t think there’s anything wrong with having a flash event listener on the flash stage listening for a keyDown, but just to keep it all simple, might as well use Starling’s keyDown/keyUp.

TouchEvents

TouchEvents were a tough sell for me starting Starling. I wanted my Flash MouseEvents. They worked. They did what I expected them to do. It took me a day or two of working with Starling to stop complaining about how TouchEvents were different, and just shut up and use them. You’re in Starling’s world… deal with it. And as I was mentioning before, it made me really excited to see the new TouchProcessor class in the Repo. There are no docs posted for it yet but if you know where to look you can find it. It *should be* the perfect blend of Starling/Sparrow TouchEvents with classic Flash MouseEvents. I’m really excited about this as it’s being written to be quite extensible.

Apps Used

I have to give a few shoutouts to a few Apps that just absolutely go hand-in-hand with easy Starling development. You see them in almost every tutorial, but they just make things oh so much easier.

Special Thanks

So I think that’s it for now. I may update this post as things change or I realize I did more stuff wrong.

Thanks, and I hope this series was helpful!

-Travis

Share

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

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

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

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

CollisionManager.as

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

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

Ok, so maybe not that serious. But that’s kindof the idea behind this CMgr. I want one single class that handles my math. To me, having every tower running through a list of all enemies (or enemies running through towers, or both!) on every game tick seems like an OK idea. But what if all those enemies and towers never did any calculations until they actually had valid targets? I realize math in Flash is not the biggest bottleneck. But it is smart to optimize where you can. So, what is the CMgr doing every game tick? To the Code!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package com.zf.managers
{
   import com.zf.core.Config;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.tower.Tower;
   import com.zf.states.Play;
 
public class CollisionManager implements IZFManager
{
   private var _play:Play;
   private var _enemies:Array;
   private var _towers:Array;
 
   public function CollisionManager(playState:Play)
   {
      _play = playState;
      _enemies = [];
      _towers = [];
   }
 
   public function update():void {
      checkEnemiesInRange();
   }
 
   public function checkEnemiesInRange():void {
      var eLen:int = _enemies.length,
          tLen:int = _towers.length;
 
      // If there are no enemies or no towers, abort the check
      if(eLen == 0 || tLen == 0) {
         return;
      }
 
      var tDistX:Number,
          tDistY:Number,
          dist:Number;
 
      for(var eCt:int = 0; eCt < eLen; eCt++)
      {
         for(var tCt:int = 0; tCt < tLen; tCt++)
         {
            tDistX = _enemies[eCt].x - _towers[tCt].x;
            tDistY = _enemies[eCt].y - _towers[tCt].y;
 
            // save some cycles not square rooting the value
            dist = tDistX * tDistX + tDistY * tDistY;
 
            if(dist < _towers[tCt].rangeSquared)
            {
               // check if enemy uid is in tower's enemies array
               if(!_towers[tCt].hasEnemyInRange(_enemies[eCt])) {
                  // add enemy to tower
                  _towers[tCt].addEnemyInRange(_enemies[eCt]);
               }
            }
            else
            {
               // enemy not in range of tower, check tower to see
               // if it used to have the enemy in range, and remove
               if(_towers[tCt].hasEnemyInRange(_enemies[eCt])) {
                  // Tell the tower to remove the enemy
                  _towers[tCt].removeEnemyInRange(_enemies[eCt]);
               }
            }
         }
      }
   }
 
   public function onEnemyAdded(e:Enemy):void {
      Config.log('CollisionManager', 'onEnemyAdded', "Adding Enemy " + e.uid);
      _enemies.push(e);
   }
 
   public function onEnemyRemoved(e:Enemy):void {
      Config.log('CollisionManager', 'onEnemyRemoved', "Removing Enemy " + e.uid);
      var len:int = _enemies.length;
      for(var i:int = 0; i < len; i++) {
         if(e == _enemies[i]) {
            _enemies.splice(i, 1);
            Config.log('CollisionManager', 'onEnemyRemoved', "Enemy " + e.uid + " Removed");
         }
      }
   }
 
   public function onTowerAdded(t:Tower):void {
      _towers.push(t);
   }
 
   public function onTowerRemoved(t:Tower):void {
      var len:int = _towers.length;
      for(var i:int = 0; i < len; i++) {
         if(t == _towers[i]) {
            _towers.splice(i, 1);
         }
      }
   }
 
   public function destroy():void {
      _enemies = null;
      _towers = null;
      _play = null;
   }
 
   public function onGamePaused():void {}
   public function onGameResumed():void {}
}
}

So now let’s look at the BulletManager.

BulletManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package com.zf.managers
{
   import com.leebrimelow.starling.StarlingPool;
   import com.zf.core.Config;
   import com.zf.objects.bullet.Bullet;
   import com.zf.objects.enemy.Enemy;
   import com.zf.objects.tower.Tower;
   import com.zf.states.Play;
 
   import flash.geom.Point;
   import flash.geom.Rectangle;
   import flash.utils.Dictionary;
 
   import org.osflash.signals.Signal;
 
public class BulletManager implements IZFManager
{
   public var onBulletHitEnemy:Signal;
   public var delayCount:int = 0;
   public var play:Play;
 
   private var _bullets:Array;
   private var _pool:StarlingPool;
   private var _mapBounds:Rectangle;
   private var _enemies:Object;
 
   // Reusable Point variables so we're not constantly doing new Point
   private var _p1:Point = new Point(0,0);
   private var _p2:Point = new Point(0,0);
 
   public function BulletManager(playState:Play)
   {
      play = playState;
      _bullets = [];
      _enemies = {};
      _pool = new StarlingPool(Bullet, 40);
      _mapBounds = play.map.paddedBounds;
      onBulletHitEnemy = new Signal(Bullet);
   }
 
   public function update():void {
      if(_bullets.length > 0 && delayCount % 2 == 0) {
         var b:Bullet,
             len:int = _bullets.length;
 
         for(var i:int = len - 1; i >= 0; i--) {
            b = _bullets[i];
 
            // doublecheck that this bullet still exists
            if(b != null) {
               b.update();
 
               if(b.hitEnemy) {
                  bulletHitEnemy(b);
               } else if(b.isDestroyed) {
                  destroyBullet(b);
               }
            } else {
               // if it doesnt, we need to update our len
               len = _bullets.length;
            }
         }
      }
      delayCount++;
   }
 
   public function bulletHitEnemy(b:Bullet):void {
      b.destObj.takeDamage(b.damage);
      onBulletHitEnemy.dispatch(b);
      destroyBullet(b);
   }
 
   public function destroyBullet(b:Bullet):void {
      var len:int = _bullets.length,
          tLen:int,
          enemyBulletArr:Array;
 
      for(var i:int = 0; i < len; i++)
      {
         if(b == _bullets[i])
         {
            // First remove the reference of the bullet firing at the enemy
            enemyBulletArr = _enemies[b.destObj.uid];
 
            // check if the enemy has already been removed or destroyed
            if(enemyBulletArr) {
               tLen = enemyBulletArr.length;
               for(var j:int = 0; j < tLen; j++) {
                  if(b == enemyBulletArr[j]) {
                     enemyBulletArr.splice(j, 1);
                  }
               }
            }
 
            b.destroy();
            _bullets.splice(i, 1);
            b.removeFromParent(true);
            _pool.returnSprite(b);
         }
      }
   }
 
   public function onTowerFiring(t:Tower, e:Enemy):void {
      var b:Bullet = _pool.getSprite() as Bullet;
      b.init(t, e, play);
      play.addChild(b);
      b.x = t.centerX;
      b.y = t.centerY;
 
      _bullets.push(b);
      _addListenerOnEnemy(e, b);
 
      Config.totals.bulletsFired++;
   }
 
   private function _addListenerOnEnemy(e:Enemy, b:Bullet):void {
      if(!_enemies[e.uid]) {
         _enemies[e.uid] = [];
         e.onDestroy.add(onEnemyDestroyed);
      }
 
      _enemies[e.uid].push(b);
   }
 
   public function onEnemyDestroyed(e:Enemy):void {
      e.onDestroy.remove(onEnemyDestroyed);
 
      for each(var b:Bullet in _enemies[e.uid]) {
         destroyBullet(b);
      }
 
      delete _enemies[e.uid];
   }
 
   public function get activeBullets():String {
      return _bullets.length.toString();
   }
 
   public function destroy():void {
      _bullets = null;
      _enemies = null;
      _pool = null;
      onBulletHitEnemy.removeAll();
   }
 
   public function onGamePaused():void {}
   public function onGameResumed():void {}
}
}

HudManager.as

HudManager (com.zf.managers.HudManager.as) is in charge of the UI during the Play State. It manages the options panel, various TextFields (HitPoints, Wave number, Gold, Tower Info, etc), and the Tower select Buttons. Let’s look at the code…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package com.zf.managers
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.objects.tower.Tower;
   import com.zf.states.Play;
   import com.zf.ui.buttons.nextWaveButton.NextWaveButton;
   import com.zf.ui.gameOptions.GameOptionsPanel;
   import com.zf.ui.text.ZFTextField;
   import com.zf.ui.towerSelect.TowerSelectIcon;
   import com.zf.ui.waveTile.WaveTileBar;
 
   import flash.geom.Point;
   import flash.text.TextFormat;
 
   import feathers.controls.ScrollText;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.textures.Texture;
 
public class HudManager extends Sprite implements IZFManager
{
   public var play : Play;
   public var endOfHP : Signal;
 
   private var _invalidComponents : Array;
   private var _playBkgd : Image;
   private var _canvas : Sprite;
   private var _p : Point = new Point(-1,-1);
   private var _tf : TextFormat;
   private var _goldTF : ZFTextField;
   private var _hpTF : ZFTextField;
   private var _waveTF : ZFTextField;
   private var _waveTileBar : WaveTileBar;
   private var _tower1 : TowerSelectIcon;
   private var _tower2 : TowerSelectIcon;
   private var _infoText : ScrollText;
   private var _activeTower : Tower;
   private var _gameOptions : GameOptionsPanel;
   private var _optsBtn : Button;
   private var _sellTowerBtn : Button;
   private var _upgradeTowerBtn : Button;
   private var _pauseBtn : Button;
   private var _optionsVisible : Boolean;
   private var _nextWaveBtns : Array;
 
   public function HudManager(playState:Play) {
      play = playState;
      _canvas = play.hudLayer;
 
      _invalidComponents = [];
      _nextWaveBtns = [];
 
      endOfHP = new Signal();
 
      _optionsVisible = false;
 
      _waveTileBar = new WaveTileBar(Config.currentMapData.enemyWaveData);
      _waveTileBar.x = 36;
      _waveTileBar.y = 567;
      // when a tile is touched, let EnemyManager handle spawning a new wave
      _waveTileBar.waveTileTouched.add(play.enemyMgr.spawnWave);
      _canvas.addChild(_waveTileBar);
 
      _playBkgd = new Image(Assets.playBkgdT);
      _playBkgd.x = 0;
      _playBkgd.y = 0;
      _playBkgd.width = 800;
      _playBkgd.height = 600;
      _playBkgd.touchable = false;
      _canvas.addChild(_playBkgd);
 
      _tower1 = new TowerSelectIcon(Assets.towerData.towers[0]);
      _tower1.x = 590;
      _tower1.y = 90;
      _canvas.addChild(_tower1);
      _tower1.onHover.add(_onHoverTowerSelectIcon);
      _tower1.onClick.add(_onClickTowerSelectIcon);
 
      _tower2 = new TowerSelectIcon(Assets.towerData.towers[1]);
      _tower2.x = 630;
      _tower2.y = 90;
      _canvas.addChild(_tower2);
      _tower2.onHover.add(_onHoverTowerSelectIcon);
      _tower2.onClick.add(_onClickTowerSelectIcon);
 
      _goldTF = new ZFTextField('goldCoin', 'CalistoMT', Config.currentGold.toString());
      _goldTF.x = 100;
      _goldTF.y = 0;
      _goldTF.componentIsInvalid.add(handleInvalidTFComponent);
      _goldTF.touchable = false;
      _canvas.addChild(_goldTF);
      Config.currentGoldChanged.add(updateGoldTF);
 
      _hpTF = new ZFTextField('heartIcon', 'CalistoMT', Config.currentHP.toString());
      _hpTF.x = 200;
      _hpTF.y = 0;
      _hpTF.componentIsInvalid.add(handleInvalidTFComponent);
      _hpTF.touchable = false;
      _canvas.addChild(_hpTF);
      Config.currentHPChanged.add(updateHPTF);
 
      var waveTxt:String = Config.currentWave.toString() + ' / ' + Config.maxWave.toString();
      _waveTF = new ZFTextField('waveIcon', 'CalistoMT', waveTxt);
      _waveTF.x = 300;
      _waveTF.y = 0;
      _waveTF.componentIsInvalid.add(handleInvalidTFComponent);
      _waveTF.touchable = false;
      _canvas.addChild(_waveTF);
      Config.currentWaveChanged.add(updateWaveTF);
 
      _infoText = new ScrollText();
      _infoText.x = 588;
      _infoText.y = 250;
      _infoText.isHTML = true;
      _infoText.textFormat = new TextFormat('CalistoMT', 16, 0xFFFFFF);
      _infoText.text = "";
      _canvas.addChild(_infoText);
 
      _upgradeTowerBtn = new Button(Assets.ta.getTexture('upgradeTowerBtn'));
      _upgradeTowerBtn.x = 580;
      _upgradeTowerBtn.y = 520;
      _canvas.addChild(_upgradeTowerBtn);
 
      _sellTowerBtn = new Button(Assets.ta.getTexture('sellTowerBtn'));
      _sellTowerBtn.x = 660;
      _sellTowerBtn.y = 520;
      _canvas.addChild(_sellTowerBtn);
 
      _hideTowerButtons();
 
      // Set up options stuff but do not add to stage yet
      _gameOptions = new GameOptionsPanel(new Point(150,-350), new Point(150,150));
      _gameOptions.onQuitGame.add(play.onQuitGameFromOptions);
 
      _optsBtn = new Button(Assets.optsBtnT);
      _optsBtn.x = 667;
      _optsBtn.y = 0;
      _optsBtn.addEventListener(Event.TRIGGERED, _onOptionsClicked);
      _canvas.addChild(_optsBtn);
 
      _resetPauseBtn(Assets.playBtnT);
      play.enemyMgr.onSpawnWave.add(onSpawnWave);
   }

And more from com.zf.managers.HudManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
   public function onSpawnWave(waveID:String):void {
      // remove after the first time since we dont show the nextWaveButtons
      // except for the first round
      play.enemyMgr.onSpawnWave.remove(onSpawnWave);
 
      // remove the next wave buttons but dont trigger a pause event
      removeNextWaveButtons(false);
   }
 
   public function update():void {
      updateUI();
      // always update the _waveTileBar, it will handle it's own optimization
      _waveTileBar.update();
   }
 
   public function updateUI():void {
      if(_invalidComponents.length > 0) {
         for(var i:int = 0; i < _invalidComponents.length; i++) {
            _invalidComponents[i].update();
            _invalidComponents.splice(i, 1);
         }
      }
   }
 
   public function updateGoldTF(amt:int):void {
      _goldTF.text = amt.toString();
   }
 
   public function updateHPTF(amt:int):void {
      _hpTF.text = amt.toString();
 
      if(amt <= 0) {
         Config.log('HudManager', 'updateHPTF', "HudManager at Zero HP");
         _removeListeners();
         Config.log('HudManager', 'updateHPTF', "HudManager dispatching endOfHP");
         endOfHP.dispatch();
      }
   }
 
   public function updateWaveTF(amt:int):void {
      _waveTF.text = amt.toString() + ' / ' + Config.maxWave.toString();
   }
 
   public function handleInvalidTFComponent(tf:ZFTextField):void {
      _invalidComponents.push(tf);
   }
 
   public function showNextWaveButtons():void {
      var p:Array = play.wpMgr.groupStartPositions,
          len:int = p.length;
      for(var i:int = 0; i < len; i++) {
         var nextWaveBtn:NextWaveButton = new NextWaveButton(Assets.ta.getTexture('nextWaveBtn_' + p[i].dir));
         nextWaveBtn.x = p[i].pos.x;
         nextWaveBtn.y = p[i].pos.y;
         nextWaveBtn.onClicked.add(removeNextWaveButtons);
         _canvas.addChild(nextWaveBtn);
         _nextWaveBtns.push(nextWaveBtn);
      }
   }
 
   public function removeNextWaveButtons(triggerPause:Boolean = true):void {
      var len:int = _nextWaveBtns.length;
      for(var i:int = 0; i < len; i++) {
         // tell all buttons to fade out and destroy
         _nextWaveBtns[i].fadeOut();
      }
 
      if(triggerPause) {
         play.onPauseEvent();
      }
   }
 
   private function _onTowerUpgradeClicked(evt:Event):void {
      if(_activeTower.canUpgrade()) {
         _activeTower.upgrade();
         showTowerData(_activeTower);
         if(_activeTower.level == _activeTower.maxLevel) {
            _hideTowerButtons();
            _showTowerButtons(false);
         }
      } else {
         trace("Cant create tower" );
      }
   }
 
   private function _hideTowerButtons():void {
      _upgradeTowerBtn.visible = false;
      _sellTowerBtn.visible = false;
      _upgradeTowerBtn.removeEventListener(Event.TRIGGERED, _onTowerUpgradeClicked);
      _sellTowerBtn.removeEventListener(Event.TRIGGERED, _onSellTowerClicked);
   }
 
   private function _showTowerButtons(showUpgrade:Boolean = true):void {
      _sellTowerBtn.visible = true;
      _sellTowerBtn.addEventListener(Event.TRIGGERED, _onSellTowerClicked);
 
      if(showUpgrade) {
         _upgradeTowerBtn.visible = true;
         _upgradeTowerBtn.addEventListener(Event.TRIGGERED, _onTowerUpgradeClicked);
      }
   }
 
   private function _onSellTowerClicked(evt:Event):void {
      play.towerMgr.sellTower(_activeTower);
   }
 
   private function _onPauseClicked(evt:Event):void {
      play.onPauseEvent();
   }
 
   public function onGamePaused():void {
      _resetPauseBtn(Assets.playBtnT);
      if(_optionsVisible) {
         _removeListeners();
      }
   }
 
   public function onGameResumed():void {
      _resetPauseBtn(Assets.pauseBtnT);
      if(!_optionsVisible) {
         _addListeners();
      }
   }
 
   private function _resetPauseBtn(t:Texture):void {
      if(_canvas.contains(_pauseBtn)) {
         _pauseBtn.removeFromParent(true);
         _pauseBtn.removeEventListener(Event.TRIGGERED, _onPauseClicked);
      }
      _pauseBtn = new Button(t);
      _pauseBtn.x = 520;
      _pauseBtn.y = 0;
      _pauseBtn.addEventListener(Event.TRIGGERED, _onPauseClicked);
      _canvas.addChild(_pauseBtn);
   }

Continuing with com.zf.managers.HudManager.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
   private function _onOptionsClicked(evt:Event):void {
      _optionsVisible = true;
      // clear any infotext, it will display OVER the options box
      showInfo('');
 
      // if gameOptions isn't already added to play.topLayer
      if(!play.topLayer.contains(_gameOptions)) {
         play.topLayer.addChild(_gameOptions);
      }
 
      play.onPauseEvent(true);
      _gameOptions.onDeactivated.add(_onOptionsDeactivated);
      _gameOptions.activate();
      _pauseBtn.removeEventListener(Event.TRIGGERED, _onPauseClicked);
   }
 
   private function _onOptionsDeactivated():void {
      _optionsVisible = false;
 
      _gameOptions.onDeactivated.remove(_onOptionsDeactivated);
      play.onPauseEvent(true);
      _pauseBtn.addEventListener(Event.TRIGGERED, _onPauseClicked);
   }
 
   private function _onClickTowerSelectIcon(towerData:Object, p:Point):void {
      if(!_optionsVisible && play.canCreateTower(towerData.id)) {
         play.createTower(towerData.id, p);
      }
   }
 
   private function _onHoverTowerSelectIcon(towerData:Object):void {
      if(!_optionsVisible) {
         generateTowerInfoHTML(towerData);
      }
   }
 
   public function showTowerData(t:Tower):void {
      _activeTower = t;
      var showUpgrade:Boolean = false;
      if(_activeTower.level < _activeTower.maxLevel) {
         showUpgrade = true;
      }
      _showTowerButtons(showUpgrade);
      generateTowerInfoHTML(t);
   }
 
   public function hideTowerData(t:Tower):void {
      if(_activeTower && _activeTower == t) {
         _activeTower = null;
         _hideTowerButtons();
         showInfo('');
      }
   }
 
   public function generateTowerInfoHTML(towerData:Object):void {
      var fontOpenH1Tag:String = '',
          fontOpenLabelTag:String = '',
          fontCloseTag:String = '',
          tName:String = '',
          isTower:Boolean = towerData is Tower;
 
      if(isTower) {
         tName = towerData.towerName;
      } else {
         tName = towerData.name;
      }
 
      var txt:String = fontOpenH1Tag + tName + fontCloseTag + '<br><br>'
                     + fontOpenLabelTag + 'Level: ' + towerData.level + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'Cost: ' + towerData.levelData[towerData.level].cost + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'Damage: ' + towerData.levelData[towerData.level].damage + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'DPS: ' + towerData.levelData[towerData.level].dps + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'Speed: ' + towerData.levelData[towerData.level].speed + fontCloseTag + '<br>'
                     + fontOpenLabelTag + 'Range: ' + towerData.levelData[towerData.level].range + fontCloseTag + '<br>';
 
      if(isTower && towerData.level < towerData.maxLevel) {
         txt += fontOpenLabelTag + 'Upgrade Cost: ' + towerData.levelData[towerData.level + 1].cost + fontCloseTag + '<br>';
      }
 
      showInfo(txt);
   }
 
   public function showInfo(msg:String):void {
      _infoText.text = msg;
   }
 
   private function _removeListeners():void {
      Config.currentWaveChanged.remove(updateWaveTF);
      Config.currentHPChanged.remove(updateHPTF);
      Config.currentGoldChanged.remove(updateGoldTF);
      _tower1.onHover.remove(_onHoverTowerSelectIcon);
      _tower1.onClick.remove(_onClickTowerSelectIcon);
      _tower2.onHover.remove(_onHoverTowerSelectIcon);
      _tower2.onClick.remove(_onClickTowerSelectIcon);
 
      _upgradeTowerBtn.removeEventListener(Event.TRIGGERED, _onTowerUpgradeClicked);
      _sellTowerBtn.removeEventListener(Event.TRIGGERED, _onSellTowerClicked);
      _optsBtn.removeEventListener(Event.TRIGGERED, _onOptionsClicked);
      _pauseBtn.removeEventListener(Event.TRIGGERED, _onPauseClicked);
   }
 
   private function _addListeners():void {
      Config.currentWaveChanged.add(updateWaveTF);
      Config.currentHPChanged.add(updateHPTF);
      Config.currentGoldChanged.add(updateGoldTF);
      _tower1.onHover.add(_onHoverTowerSelectIcon);
      _tower1.onClick.add(_onClickTowerSelectIcon);
      _tower2.onHover.add(_onHoverTowerSelectIcon);
      _tower2.onClick.add(_onClickTowerSelectIcon);
 
      _upgradeTowerBtn.addEventListener(Event.TRIGGERED, _onTowerUpgradeClicked);
      _sellTowerBtn.addEventListener(Event.TRIGGERED, _onSellTowerClicked);
      _optsBtn.addEventListener(Event.TRIGGERED, _onOptionsClicked);
      _pauseBtn.addEventListener(Event.TRIGGERED, _onPauseClicked);
   }
 
   public function destroy():void {
      Config.log('HudManager', 'destroy', "HudManager Destroying");
 
      _activeTower = null;
 
      _tower1.destroy();
      _tower1.removeFromParent(true);
      _tower1 = null;
 
      _tower2.destroy();
      _tower2.removeFromParent(true);
      _tower2 = null;
 
      Config.currentGoldChanged.removeAll();
      Config.currentHPChanged.removeAll();
      Config.currentWaveChanged.removeAll();
 
      _goldTF.destroy();
      _goldTF.removeFromParent(true);
      _goldTF = null;
      _hpTF.destroy();
      _hpTF.removeFromParent(true);
      _hpTF = null;
 
      _waveTF.destroy();
      _waveTF.removeFromParent(true);
      _waveTF = null;
      _upgradeTowerBtn.removeFromParent(true);
      _upgradeTowerBtn = null;
 
      _sellTowerBtn.removeFromParent(true);
      _sellTowerBtn = null;
 
      _optsBtn.removeFromParent(true);
      _sellTowerBtn = null;
 
      Config.log('HudManager', 'destroy', "HudManager Destroyed");
   }
}
}

KeyboardManager.as

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.zf.managers
{
   import com.zf.core.Config;
   import com.zf.states.Play;
   import com.zf.utils.KeyCode;
 
   import flash.display.Stage;
   import flash.events.KeyboardEvent;
 
   import org.osflash.signals.Signal;
public class KeyboardManager
{
   public var onPause:Signal;
 
   private var _stage:Stage;
   private var _play:Play;
   private var _signals:Array = [];
 
   public function KeyboardManager(play:Play, stage:Stage) {
      _play = play;
      _stage = stage;
      _stage.addEventListener(KeyboardEvent.KEY_DOWN, _onKeyDown);
 
      onPause = new Signal();
      _signals.push(onPause);
   }
 
   protected function _onKeyDown(evt:KeyboardEvent):void {
      // Handle events the game uses
      switch(evt.keyCode) {
         case KeyCode.SPACE:
            onPause.dispatch();
            break;
 
         case KeyCode.PLUS:
         case KeyCode.ADD:
            Config.changeGameSpeed(Config.GAME_SPEED_UP);
            break;
 
         case KeyCode.MINUS:
         case KeyCode.SUBTRACT:
            Config.changeGameSpeed(Config.GAME_SPEED_DOWN);
            break;
      }
   }
 
   public function destroy():void {
      _stage.removeEventListener(KeyboardEvent.KEY_DOWN, _onKeyDown);
      for(var i:int = 0; i < _signals.length; i++) {
         _signals[i].removeAll();
      }
   }
}
}

WaypointManager.as

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package com.zf.managers
{
   import com.zf.objects.map.TileData;
   import com.zf.objects.map.WaypointData;
   import com.zf.objects.enemy.Enemy;
 
   import flash.geom.Point;
   import com.zf.states.Play;
 
public class WaypointManager
{
   public var groupStartPositions : Array;
 
   private var _waypoints : Object;
   private var _waypointDist : Object;
   private var _tempEndpoints : Object;
   private var _play : Play;
   private var _halfTileWidth : Number;
   private var _halfTileHeight : Number;
 
   public function WaypointManager(p:Play) {
      _play = p;
      _waypoints = {};
      _waypointDist = {};
      _tempEndpoints = {};
      groupStartPositions = [];
   }
 
   /**
    * Gets called by Map to add a Waypoint when it comes across the right 
    * criteria for a specific piece of Tile data
    **/
   public function addWaypoint(tileData:TileData, pos:Point, halfTileWidth:Number, halfTileHeight:Number):void {
      // save values so we can use later
      _halfTileWidth = halfTileWidth;
      _halfTileHeight = halfTileHeight;
 
      // create a new WaypointData Object and pass in the tileData
      var wpData:WaypointData = new WaypointData(tileData);
 
      // set the waypoint point
      wpData.setPoint(pos, halfTileWidth, halfTileHeight);
 
      // loop through our waypoint groups and create a new array if we dont have the current group
      // in our _waypoints Object
      for each(var group:String in wpData.groups) {
         if(!_waypoints[group]) {
            _waypoints[group] = [];
         }
      }
 
      // If this is a spawnpoint, add another point offscreen for the "real" spawnpoint
      if(wpData.isSpawnpoint) {
         // set index to 0, this will be the first point
         var tmpObj:Object = {
                              "src": wpData.srcImageName,
                              "isWalkable": true,
                              "wp": true,
                              "wpGroup": wpData.wpGroup,
                              "wpIndex": 0,
                              "sp": true,
                              "spDirection": wpData.spDirection
         };
         // create a new 'real' spawnpoint with data
         var spWaypoint:WaypointData = new WaypointData(new TileData(tmpObj));
 
         // add the newly created WP
         _waypoints[group].push(spWaypoint);
 
         // unset the former spawnpoint wp
         wpData.isSpawnpoint = false;
         wpData.spDirection = '';
      }
 
      // handle if this is an endpoint, saving the data for later
      if(wpData.isEndpoint) {
         for each(var groupName:String in wpData.groups) {
            if(!_tempEndpoints[groupName]) {
               _tempEndpoints[groupName] = [];
            }
 
            // Keep a copy to the last wp "so far", when we sort, 
            // we'll take this "last" wp and add another one with the
            // right values to be the real off-screen endpoint
            _tempEndpoints[groupName].push(wpData);
         }
      }
 
      _waypoints[group].push(wpData);
   }
 
   /**
    * Gets called by Map to add a Group Start Position when it comes across
    * the right criteria for a specific piece of Tile data
    **/
   public function addGroupStartPosition(dir:String, pos:Point, halfTileWidth:Number, halfTileHeight:Number):void {
      groupStartPositions.push({dir: dir, pos: new Point(pos.x + halfTileWidth, pos.y + halfTileHeight)});
   }
 
   /**
    * Handles the temporary endpoints array and creates a new point that is
    * the actual endpoint for the Enemy, then sorts the array so it appears
    * in order
    **/
   public function handleEndpointAndSort():void {
      var groupName:String;
 
      // Before sorting, handle the _tempEndpoints by adding a new WP that's the real
      // off-screen final endpoint wp
      for(groupName in _tempEndpoints) {
         // should only be ONE endpoint per group, so grab the 0th element
         var tempWP:WaypointData = _tempEndpoints[groupName][0];
 
         // Add one to the index so this point comes after the other endpoint
         var lastIndex:int = tempWP.wpIndex + 1;
 
         var tmpObj:Object = {
                              "src": tempWP.srcImageName,
                              "isWalkable": true,
                              "wp": true,
                              "wpGroup": tempWP.wpGroup,
                              "wpIndex": lastIndex,
                              "ep": true,
                              "epDirection": tempWP.epDirection
         };
 
         // create a new 'real' spawnpoint with XML
         var newWP:WaypointData = new WaypointData(new TileData(tmpObj));
 
         // add the newly created WP
         _waypoints[groupName].push(newWP);
 
         // set the former endpoint wp to be a regular waypoint now that we've added the new endpoint
         _waypoints[groupName][tempWP.wpIndex].isEndpoint = false;
         _waypoints[groupName][tempWP.wpIndex].epDirection = '';
 
         // empty vector of endpoints for the group
         _tempEndpoints[groupName] = null;
         delete _tempEndpoints[groupName];
      }
 
      // Loop through groups and sort them
      for(groupName in _waypoints) {
         // sort each group
         _waypoints[groupName].sort(_waypointSort);
      }
 
      _calcRouteDistance();
   }
 
   /**
    * Returns an Array of endpoints based on a groupName
    **/
   public function getWaypointsByGroup(groupName:String):Array {
      return _waypoints[groupName];
   }
 
   /**
    * Returns the distance (Number) of a particular waypoint 
    * group's route when given the groupName
    **/
   public function getRouteDistance(groupName:String):Number {
      return _waypointDist[groupName];
   }
 
   /**
    * This is the sort function to use so the Array knows how to sort
    * the array. We're sorting on wpIndex
    **/
   private function _waypointSort(a:WaypointData, b:WaypointData):int {
      if(a.wpIndex < b.wpIndex) {
         return -1;
      } else if(a.wpIndex > b.wpIndex) {
         return 1;
      } else {
         return 0;
      }
   }
 
   /**
    * Actually traces the route of a waypoint group and calculates 
    * the total distance in Pixels for the waypoint group
    **/
   private function _calcRouteDistance():void {
      // handle the xy values of spawn and endpoints before getting distances
      _handleSpawnEndpointXYValues();
 
      for(var groupName:String in _waypoints) {
         // Get distance of the route
         var dist:Number = 0,
             prevWP:WaypointData,
             newX:Number,
             newY:Number,
             dx:Number,
             dy:Number,
             addedDist:Number = 0;
 
         for each(var wp:WaypointData in _waypoints[groupName]) {
            // handle the first wp
            if(!prevWP) {
               prevWP = wp;
               // skip the rest of the loop this round and get the next wp
               continue;
            }
 
            // figure out which direction we're heading next, get dx,dy
            dx = wp.point.x - prevWP.point.x;
            dy = wp.point.y - prevWP.point.y;
 
            // Set the previousWP's next direction
            // So "from prevWP, which way should I face to the next WP"
            if(dx > 0 && dy == 0) {
               prevWP.nextDir = Enemy.ENEMY_DIR_RIGHT;
            } else if(dx < 0 && dy == 0) {
               prevWP.nextDir = Enemy.ENEMY_DIR_LEFT;
            } else if(dx == 0 && dy > 0) {
               prevWP.nextDir = Enemy.ENEMY_DIR_DOWN;
            } else if(dx == 0 && dy < 0) {
               prevWP.nextDir = Enemy.ENEMY_DIR_UP;
            }
 
            // find the distance
            // regular distance formula: Math.sqrt(dx * dx + dy * dy);
            // since we're only moving up, down, left, or right, never any diagonals
            // we can simplify because dx OR dy will always be 0 making squaring, then squarerooting useless
            // but we do want the Absolute value so distance is a positive number
            addedDist = Math.abs(dx) + Math.abs(dy);
 
            // sum the distance for later group totals
            dist += addedDist;
 
            // When unit begins heading towards this wp, now it knows the distance
            prevWP.distToNext = addedDist;
 
            // set current waypoint to previous
            prevWP = wp;
         }
 
         // add total distance to the group route
         _waypointDist[groupName] = dist;
      }
   }
 
   /**
    * Loops through all groups in _waypoints and handles the 
    * x & y values of the spawnpoints and endpoints for the group
    **/
   public function _handleSpawnEndpointXYValues():void {
      // quadrupling halfTileWidth and halfTileHeight so enemies 
      // start 2 full tile widths/heights off screen
      var tileWidth:int = _halfTileWidth << 2;
          tileHeight:int = _halfTileHeight << 2;
 
      for(var groupName:String in _waypoints) {
         // get the length of this group
         var groupLen:int = _waypoints[groupName].length;
 
         // temp spawnpoint
         var sp:WaypointData = _waypoints[groupName][0];
         // temp first wp
         var fwp:WaypointData = _waypoints[groupName][1];
 
         // temp endpoint
         var ep:WaypointData = _waypoints[groupName][groupLen - 1];
         // temp next-to-last waypoint
         var ntlwp:WaypointData = _waypoints[groupName][groupLen - 2];
 
         // use fwp.regPoint to get the top-left corner coordinate for the tile
         var newX:Number = fwp.regPoint.x,
             newY:Number = fwp.regPoint.y,
             halfWidth:int = 0,
             halfHeight:int = 0;
 
         switch(sp.spDirection) {
            case 'left':
               newX -= tileWidth;
               halfHeight = _halfTileHeight;
               sp.nextDir = Enemy.ENEMY_DIR_RIGHT;
               break;
 
            case 'up':
               newY -= tileHeight;
               halfWidth = _halfTileWidth;
               sp.nextDir = Enemy.ENEMY_DIR_DOWN;
               break;
 
            case 'right':
               newX += tileWidth;
               halfHeight = _halfTileHeight;
               sp.nextDir = Enemy.ENEMY_DIR_LEFT;
               break;
 
            case 'down':
               newY += tileHeight;
               halfWidth = _halfTileWidth;
               sp.nextDir = Enemy.ENEMY_DIR_UP;
               break;
         }
 
         // set the new point for the spawnpoint
         sp.setPoint(new Point(newX, newY), halfWidth, halfHeight);
 
         // reuse vars
         newX = ntlwp.regPoint.x;
         newY = ntlwp.regPoint.y;
         halfWidth = halfHeight = 0;
 
         switch(ep.epDirection) {
            case 'left':
               newX -= tileWidth;
               halfHeight = _halfTileHeight;
               break;
 
            case 'up':
               newY -= tileHeight;
               halfWidth = _halfTileWidth;
               break;
 
            case 'right':
               newX += tileWidth;
               halfHeight = _halfTileHeight;
               break;
 
            case 'down':
               newY += tileHeight;
               halfWidth = _halfTileWidth;
               break;
         }
 
         // set the new point for the endpoint
         ep.setPoint(new Point(newX, newY), halfWidth, halfHeight);
      }
   }
 
   public function destroy():void {
      _waypoints = null;
   }
}
}

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

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

Thanks for reading all about the Managers classes!

-Travis

Share

AS3 Starling Tower Defense Tutorial – Part 9 – UI Game Components

In my previous tutorial on UI Menu Components I went over several UI components that mostly were found outside of the main Play State. The GameOptionsPanel is used in the Play State, but by “UI Game Components” I mean more the components that make up the HUD, the gold and HP counters, enemy healthbars that change when the enemy gets hit, those sorts of things. Last post was awful wordy, so this time we’re just going to get straight to more UI components!

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

NextWaveButton.as

Problem

As a player, in the Play State, at the beginning of a new Map I need to know where the badguys are going to be coming from (where they enter the map).

Solution

I need icons to represent where the enemies will enter the map and which direction they’re headed. It needs to stand out a bit from the square-ish nature of the tile map, so maybe some circles. And it should probably be animated or have some way of jumping out at the player so they know they can click the icons to start the game.

Execution

I’ve added these little green circles to my tilesheet.

Next Wave Buttons

They show up on the road next to where an Enemy group will be entering from.

Next Wave Buttons InGame

We need to make sure that the we grab the right texture so the arrow points in the correct direction, but other than that, this button is pretty simple. Oh, it also uses TweenLite to scale bigger and smaller so it grabs the player’s attention better on the screen.

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
package com.zf.ui.buttons.nextWaveButton
{
   import com.zf.core.Config;
   import com.zf.core.Game;
 
   import com.greensock.TweenLite;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.textures.Texture;
 
public class NextWaveButton extends Sprite
{
   public var onClicked:Signal;
 
   private var _btn:Button;
 
   public function NextWaveButton(btnT:Texture) {
      onClicked = new Signal();
 
      _btn = new Button(btnT);
      addChild(_btn);
 
      pivotX = _btn.width >> 1;
      pivotY = _btn.height >> 1;
 
      _btn.addEventListener(Event.TRIGGERED, _onBtnTriggered);
      expand();
   }
 
   public function fadeOut():void {
      TweenLite.to(this, .5, {alpha: 0, scaleX: 0, scaleY: 0, onComplete: destroy});
   }
 
   private function _onBtnTriggered(evt:Event):void {
      _btn.removeEventListener(Event.TRIGGERED, _onBtnTriggered);
      onClicked.dispatch();
   }
 
   private function expand():void {
      TweenLite.to(this, 0.5, {scaleX: 1.2, scaleY: 1.2, onComplete: contract});
   }
 
   private function contract():void {
      TweenLite.to(this, 0.5, {scaleX: 0.8, scaleY: 0.8, onComplete: expand});
   }
 
   public function destroy():void {
      _btn.removeFromParent(true);
      _btn = null;
 
      onClicked.removeAll();
 
      removeFromParent(true);
   }
}
}

HealthBar.as

Problem

As a player, in the Play State, I need to know how much health an Enemy has.

Solution

Let’s draw a health bar on the Enemy class. The Enemy class knows when it gets hit, it can manage updating the HealthBar.

Execution

This is a completely programmatically drawn health bar above the Enemy so there are no pretty pictures other than the HealthBar in action…

HealthBar

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
package com.zf.ui.healthBar
{
   import com.zf.objects.enemy.Enemy;
 
   import flash.display.Bitmap;
   import flash.display.BitmapData;
   import flash.display.Shape;
 
   import starling.display.Image;
   import starling.display.Sprite;
 
public class HealthBar extends Sprite
{
   private var _healthBar:Image;
   private var _healthCurrent:int;
   private var _healthMax:int;
   private var _drawWidth:int;
   private var _enemy:Enemy;
   private var _healthWidth;
   private var _percentDmg:Number;
   private var _s:Shape;
   private var _bmd:BitmapData;
 
   public function HealthBar(parentEnemy:Enemy, currentHP:int, maxHP:int, drawWidth:int = 20) {
      _enemy = parentEnemy;
      _healthCurrent = currentHP;
      _healthMax = maxHP;
      _drawWidth = drawWidth;
 
      _percentDmg = 0;
      _healthWidth = _drawWidth;
 
      _s = new Shape();
      update();
   }
 
   public function update():void {
      if(contains(_healthBar)) {
         _healthBar.removeFromParent(true);
      }
 
      _s.graphics.clear();
 
      // draw container
      _s.graphics.lineStyle(1, 0);
      _s.graphics.beginFill(0, 0);
      _s.graphics.drawRect(0, 0, _drawWidth, 5);
      _s.graphics.endFill();
 
      var fillCol:uint = 0x009999;
 
      if(_percentDmg > .35 && _percentDmg <= .69) {
         fillCol = 0xE3EF24;
      } else if(_percentDmg > 0 && _percentDmg <= .34) {
         fillCol = 0xFF0000;
      }
 
      // draw current health
      _s.graphics.lineStyle(0, fillCol, 1, true);
      _s.graphics.beginFill(fillCol);
      _s.graphics.drawRect(1, 1, _healthWidth - 2, 3);
      _s.graphics.endFill();
 
      _bmd = new BitmapData(_s.width, _s.height, true, 0);
      _bmd.draw(_s);
 
      _healthBar = Image.fromBitmap(new Bitmap(_bmd, "auto", true));
      _healthBar.touchable = false;
      _healthBar.x = _s.width >> 1;
      addChild(_healthBar);
   }
 
   public function takeDamage(dmg:Number):void {
      _healthWidth -= _drawWidth * (dmg / _healthMax);
      _percentDmg = _healthWidth / _drawWidth;
      update();
   }
}
}

ZFTextField.as

Problem

As a player, in the Play State, I need to know how many HitPoints I have left, how much Gold I have, and the current wave / total waves that are on this map.

Solution

So, as a Developer, these all sound like the same thing when abstracted out, “I need to display some value to the user, it will be similar to a few other values, # of HP or # of gold pieces or # of current waves / # total waves are all just integers.” So let’s make a reusable class that combines a TextField as the way to display something, but let’s at least try to make it slightly visual. So I want an icon next to each TextField letting the player know what this specific number represents. Also, this TextField isn’t really going to need to be updated often. If you think on the scale of 60FPS, 60 updates a second, the player will be quite alright waiting at least 20 of those 60 updates to know they got one more gold coin. So I want to create a way to let whatever is Managing this class know that this class does not need to be updated right now.

Execution

Here are 3 ZFTextFields in action. As we’ll see in the code, there are 3 visual components to each of these: an icon, a background shape, and a text field (the white numbers in the images). So I found some probably totally free artwork online to use as my icons. I found a coin to represent my Gold, a heart to represent health, and yeah, that’s a wave… of water… to represent Enemy waves. I know… I know… “don’t get too artsy-fartsy on us now dude… that almost looks half-decent and like you put some thought into it!”

ZFTextField

Let’s look at the code from com.zf.ui.text.ZFTextField.as now:

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.ui.text
{
   import com.zf.core.Assets;
 
   import flash.display.Bitmap;
   import flash.display.BitmapData;
   import flash.display.Shape;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.text.TextField;
 
public class ZFTextField extends Sprite
{
   public var componentIsInvalid:Signal;
 
   private var _tf:TextField;
   private var _icon:Image;
   private var _textVal:String;
   private var _bkgdShape:Image;
 
   public function ZFTextField(iconName:String, fontName:String, startValue:String = '')  
   {
      var _s:Shape = new Shape();
      _s.graphics.lineStyle(2);
      _s.graphics.beginFill(0xFFFFFF);
      _s.graphics.drawRoundRect(0, 0, 80, 26, 8, 8)
      _s.graphics.endFill();
 
       var _bmd:BitmapData = new BitmapData(_s.width, _s.height, true, 0);
      _bmd.draw(_s);
 
      _bkgdShape = Image.fromBitmap(new Bitmap(_bmd, "auto", true));
      _bkgdShape.alpha = .4;
      _bkgdShape.x = 20;
      _bkgdShape.y = 5;
      _bkgdShape.touchable = false;
      addChild(_bkgdShape);
 
      _icon = new Image(Assets.ta.getTexture(iconName));
      _icon.x = 0;
      _icon.y = 2;
      _icon.touchable = false;
      addChild(_icon);
 
      _tf = new TextField(80, 32, startValue);
      _tf.fontName = fontName
      _tf.x = 30;
      _tf.y = 2;
      _tf.color = 0xFFFFFF;
      _tf.fontSize = 20;
      _tf.touchable = false;
      addChild(_tf);
 
      componentIsInvalid = new Signal(ZFTextField);
 
      touchable = false;
   }
 
   public function update():void {
      _tf.text = _textVal;
   }
 
   public function set text(val:String):void {
      _textVal = val;
      componentIsInvalid.dispatch(this);
   }
 
   public function get text():String {
      return _textVal;
   }
 
   public function destroy():void {
      componentIsInvalid.removeAll();
      componentIsInvalid = null;
 
      _tf.removeFromParent(true);
      _tf = null;
 
      _icon.removeFromParent(true);
      _icon = null;
 
      _bkgdShape.removeFromParent(true);
      _bkgdShape = null;
 
      _textVal = null;
   }
}
}

TowerSelectIcon.as

Problem

As a player, in the Play State, I need some visual representation of the Towers I will be using, and I want to be able to click on them to “create a tower”.

Solution

Since my Towers are just static, boring, box images, so are the TowerSelectIcon icons. In fact, it’s the exact same image as the tower! What economy of assets! …it’s sort of like… “economy of language”… but with art… assets. Oh well…

Execution

Very boring. There they are.
Tower 1 Tower 2

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
package com.zf.ui.towerSelect
{
   import com.zf.core.Assets;
 
   import flash.geom.Point;
 
   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;
 
public class TowerSelectIcon extends Sprite
{
   public var onHover:Signal;
   public var onClick:Signal;
 
   private var _icon:Image;
   private var _data:Object;
 
   public function TowerSelectIcon(data:Object) {
      _data = data;
      _data.level = 0;
 
      // currently just a static image
      _icon = new Image(Assets.ta.getTexture(_data.imageName));
      addChild(_icon);
 
      onHover = new Signal(Object);
      onClick = new Signal(Object, Point);
 
      addEventListener(TouchEvent.TOUCH, _onTouch);
   }
 
   private function _onTouch(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(this);
      if(touch)
      {
         switch(touch.phase) {
            case TouchPhase.BEGAN:
               // using Sprite's localToGlobal function to take the x/y clicked on locally
               // and convert it to stage x/y values
               onClick.dispatch(_data, localToGlobal(touch.getLocation(this)));
               break;
 
            case TouchPhase.HOVER:
               onHover.dispatch(_data);
               break;
         }
      }
   }
 
   public function destroy():void {
      removeEventListener(TouchEvent.TOUCH, _onTouch);
 
      onHover.removeAll();
      onClick.removeAll();
 
      _icon.removeFromParent(true);
      _icon = null;
 
      removeFromParent(true);
   }
}
}

WaveTileBar.as

Problem

As a player, in the Play State, I want to be able to see visually how many waves I have remaining. I want to see how long I have until the next wave, and I want to be able to click on a wave to speed it up so it spawns faster.

Solution

Taking a page from classic TD genre styles, I’ll create an image for each wave. They’ll be aligned on the bottom of the screen, slowly moving towards the left. When they reach the left of the screen, the wave tile will “break” and the enemies will come charging onto the Map.

Execution

This is the wave tile background bar image. Onto this background image I’ll place all of my individual WaveTiles, evenly spaced apart, and then I can move them each tick. We’ll look at the individual WaveTile class next.
WaveTileBkgd

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
package com.zf.ui.waveTile
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
 
   import flash.geom.Point;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Image;
   import starling.display.Sprite;
public class WaveTileBar extends Sprite
{
   public var waveTileTouched:Signal;
 
   private const UPDATE_DELAY:int = 15;
   private const TILE_X_MOVE_PER_TICK:Number = 0.5;
   private const UPDATE_DELAY_HURRIED:int = 4;
   private const TILE_X_MOVE_PER_TICK_HURRIED:Number = 5;
 
   private var _tiles:Array;
   private var _data:Object;
   private var _waveXCt:int;
   private var _waveXBuffer:int = 10;
   private var _updateCt:int;
   private var _waveTileBarBkgd:Image;
   private var _barSpeed:int;
   private var _lastTouchedTileId:String;
   private var _updateDelay:int;
   private var _tileXMovePerTick:Number;
 
   public function WaveTileBar(data:Object) {
      _data = data;
      _tiles = [];
 
      _waveXCt = 0;
      _updateCt = 0;
      _barSpeed = 0;
 
      waveTileTouched = new Signal(String);
 
      // add background
      _waveTileBarBkgd = new Image(Assets.waveTileBkgdT);
      _waveTileBarBkgd.touchable = false;
      addChild(_waveTileBarBkgd);
 
      _setDelayToDefault();
 
      _createWaveTiles();
   }
 
   private function _setDelayToDefault():void {
      _updateDelay = UPDATE_DELAY;
      _tileXMovePerTick = TILE_X_MOVE_PER_TICK;
   }
 
   private function _setDelayToHurried():void {
      _updateDelay = UPDATE_DELAY_HURRIED;
      _tileXMovePerTick = TILE_X_MOVE_PER_TICK_HURRIED;
   }
 
   public function update():void {
      if(_tiles.length > 0 && _updateCt % _updateDelay == 0) {
         var len:int = _tiles.length;
         for(var i:int = 0; i < len; i++) {
            _tiles[i].x -= _tileXMovePerTick;
         }
 
         if(_tiles[0].x <= 0) {
            // If user touched this tile to speed all tiles up
            if(_tiles[0].id == _lastTouchedTileId) {
               // reset vars so bar doesnt go faster anymore
               _lastTouchedTileId = '';
               _setDelayToDefault();
            }
 
            waveTileTouched.dispatch(_tiles[0].id);
            _removeFirstWaveTile();
         }
      }
 
      _updateCt++;
   }
 
   private function _removeFirstWaveTile():void {
      _tiles[0].removeFromParent(true);
      // have the tile run it's destroy function
      _tiles[0].destroy();
      // remove it from the array
      _tiles.shift();
   }
 
   private function _createWaveTiles():void {
      var len:int = _data.numWaves;
      for(var i:int = 0; i < len; i++) {
         var tile:WaveTile = new WaveTile(_data.waveTiles[i]);
         tile.onClick.add(onTileTouched);
         tile.onHover.add(onTileHovered);
         tile.x = _waveXCt;
         addChild(tile);
 
         _tiles.push(tile);
 
         _waveXCt += tile.width + _waveXBuffer;
      }
   }
 
   public function onTileTouched(tileId:String):void {
      _lastTouchedTileId = tileId;
      _setDelayToHurried();
   }
 
   public function onTileHovered(tileId:String, tilePt:Point):void {
      //Config.log('WaveTileBar', 'onTileHovered', tileId + " hovered");
   }
 
   public function destroy():void {
      var len:int = _tiles.length;
      for(var i:int = 0; i < len; i++) {
         _tiles[i].onTouch.remove();
         _tiles[i].onHover.remove();
         _tiles[i].removeFromParent(true);
         _tiles[i].destroy();
      }
      _tiles = null;
   }
}
}

WaveTile.as

Now we’ll look at the individual Wave Tiles.

Wave Tiles

I basically just made a rounded rectangle in Photoshop, copied the layer 5 times, changed the color of each, and added an ElderScrolls rune sideways on the image. Oh and all the beveling and stuff as well. Let’s wrap this up by checking out the Wave Tile 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
package com.zf.ui.waveTile
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
 
   import flash.geom.Point;
 
   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;
 
public class WaveTile extends Sprite
{
   public var onClick:Signal;
   public var onHover:Signal;
 
   private var _tileImg:Image;
   private var _data:Object;
 
   public function WaveTile(waveTileData:Object) {
      _data = waveTileData;
 
      _tileImg = new Image(Assets.ta.getTexture(_data.image));
      addChild(_tileImg);
 
      onHover = new Signal(String, Point);
      onClick = new Signal(String);
 
      addEventListener(TouchEvent.TOUCH, _onTileImageClick);
   }
 
   private function _onTileImageClick(evt:TouchEvent):void {
      var touch:Touch = evt.getTouch(this);
      if(touch)
      {
         switch(touch.phase) {
            case TouchPhase.BEGAN:
               onClick.dispatch(id);
               break;
 
            case TouchPhase.HOVER:
               onHover.dispatch(id, touch.getLocation(this));
               break;
         }
      }
   }
 
   public function get id():String {
      return _data.id;
   }
 
   public function get image():String {
      return _data.image;
   }
 
   public function get title():String {
      return _data.title;
   }
 
   public function get desc():String {
      return _data.desc;
   }
 
   public function destroy():void {
      removeEventListener(TouchEvent.TOUCH, _onTileImageClick);
      removeChild(_tileImg);
      onHover.removeAll();
      onClick.removeAll();
   }
}
}

That’s it for now! 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’ve got a great start on the next tutorial post on Bullets, Collisions, and Managers. Make sure you check back next Tuesday for another post!
Thanks!
-Travis

Share

AS3 Starling Tower Defense Tutorial – Part 8 – UI Menu Components

Bad UI will tarnish every good game design decision you’ve ever made about your game.

So, you’ve finally finished your game. It has well-thought-out gameplay mechanics that you perfected by talking it out (to yourself in the shower). You didn’t even know you were so clever until you started writing dialogue for your characters, now maybe you can leave coding behind and be a game writer? Maybe write for Conan? Yeah, totally. Now, you did spend way too much time running around with a sound recorder near your parents swimming pool to get that raw water sound you needed for that one level, but the sound design is superb you must say. You finally figured out Photoshop well enough to know that those filters are meant to be used by people that know what those settings mean, and who intentionally change them to make something interesting, not just willy nilly changing settings til something doesn’t look awful. And as best you can tell, your code was 100% bug-free when you uploaded it. You’re a gaming success story and tomorrow you’re sure all your indie game heroes will be emailing you. “They’re not just stupid games now are they, dad!” you’ll get to yell on the phone, self-righteous, vindicated by your success. But… you overlooked one tiny, massive thing. The UI.

The reviews start rolling in.

  • “…had to wait through a whole minute of intros and story before the main menu. A minute! For a flash game!” Well, there were a few logo animations I really had to have for the sites I posted it on, and I wanted the user to hear the clever story intro I wrote right up front at the beginning.
  • “…no info/feedback on what specific upgrades actually do for the player, you can level up “Gold” but what does that mean…” It’s goooold you idiot! More points means more gold!
  • “…takes 5 seconds for the Options screen to animate in so I can update a setting…every single time” But guys, that was a badass animation, I mean, I went to bed around 4:40am after finishing that…
  • “…no mute button… have to leave the game and go back to the main menu to adjust the sound…” You could just turn it on or off at the beginning of the game before you started, why interrupt the audio while you’re playing. It provides AMBIANCE!
  • “…had to click on every level on the map select screen to see which one to play next…” But.. but.. well, maybe I should’ve done a better job of guiding the player as to which level he/she just unlocked and can play next.

You get the point. If you had bad game mechanics, terrible sound design, etc, but had a great UI, it’d be a bad game. If you had great game mechanics, great sound design, but had bad UI, it still might be a great game, but you’re punishing the player for playing and most likely people will see it as a bad game. You already got the point, I’m preaching to the choir. And why am I preaching anyways? Get back to swabbin’ th’ deck, ye codemonkey!

A quick note on this post, I’ve broken this post into 2 (more) parts. UI Menu Components (sub-part 1) and UI Game Components (sub-part 2). So there’s a whole new post about the UI components I use in the game itself coming in the next few days! If you happen to read this before the other post is done, I should have it finished by friday. These UI posts take a bit more writing and images and stuff, but I’ll have all the component code shared with you by the end of the week, thanks!

Now, some of those example ‘reviews’ above deal more with UX (User Experience) than UI (User Interface), but that’s a whole different blog post. Or a whole different thesis. Being a coder, I’m going to approach UI from a very methodical standpoint of “I have a problem, I need to fix the problem, here’s your solution and it may or may not have an adequately tied bow on it, but it is a solution.” This whole tutorial series has been more or less my approach to creating a TD game, so we’ll continue that trend with my approach to UI. Remember, this is not a UI expert writing this, nor an artist.

I am a terrible, terrible designer. I Photoshop at a 3nd grade level. My stick figures are misshapen and anyone watching my choice in colors will wonder if I did more of the eating of the crayons (non-toxic, they said…) than the using of the crayons. But the fact remains… I want to make games. I wanted to make this demo. You can’t go running to a design friend every 5 minutes when you need a new placeholder graphic for this or that. So if you’ve checked out the finished demo then you already know that the nicest thing one could say about my UI is that it “is functional.” As this is a demo, I wanted to show how I made the very basic of buttons, menus, HUD, etc. Ignore the artwork before your eyes, and see deep, deep, deep down inside where the code beauty exists…

LoadGameButton.as

Problem

As a player, on the main menu screen, I want to be able to create a new game, load a saved game and delete a saved game.

Solution

This is a wide open problem with a large number of solutions. I won’t go into all the ones I didn’t choose, but here’s the way I went. The Solution: I want a single class to handle the logic for this. I need a “New Game” image, a “Load Game” image, a “Delete Game” image. I arbitrarily narrowed the requirements down to the player having 3 “game slots” so they could have 3 different saved games at a time. Each “game slot” (I’m going to drop the quotes now, we’re all adults here)… each game slot will have it’s own instance of this class. For each game slot, if there is no data saved in the slot, display the “New Game” Texture on the Button. If there is saved data in the slot, show the Load Game Texture and also enable a Delete button so the user can delete their saved game. If the user clicks the delete button, I also need a way to warn the player that deleting game data is forever and they may cry if they say yes (<--- Not done in the actual demo but mandatory in your game!!).

Execution

Here are my two button images (hell yeah, beveling and outer glow! I’m gonna take these skills and FrontPage and have the most awesome geocities account in all the WWW!).
new game button load game button

And here, you can see the two different states the button could be in. The delete button is below the Load Game button and isn’t visible or clickable on the New Game button slot. Above the buttons you can see the game slot name.
new and load game buttons

Now we’ll look at the code and go through programmatically what’s going on here.

From src/com/zf/ui/buttons/loadGameButton/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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package com.zf.ui.buttons.loadGameButton
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.core.Game;
   import com.zf.utils.ZFGameData;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.text.TextField;
   import starling.textures.Texture;
 
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();
      }
   }
 
   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 _addDeleteGameBtn():void {
      _deleteGameBtn = new Button(Assets.deleteGameBtnT);
      _deleteGameBtn.x = 130;
      _deleteGameBtn.y = 105;
      addChild(_deleteGameBtn);
      _deleteGameBtn.addEventListener(Event.TRIGGERED, _onDeleteGameBtnClicked);
   }
 
   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 _onClick(evt:Event):void {
      setCurrentGameData();
      onGameSelected.dispatch();
   }
 
   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;
   }
}
}

Now, right off the bat, there is some dumb stuff going on here. Can you spot it? When the Button needs to change it’s Texture, I have to destroy it, create a whole new Button passing in the new texture, and handle removing and readding event listeners. That’s nuts. I should just be able to update change the Button Texture and be done with it. But we can’t. However, I found this helpful post on the Starling Forums on a Multi-State Button that you might find helpful. So, having to remove listeners, destroy the button, create a new Button and add new listeners is a pain, but lets weigh this against how often a player will be deleting their saved game data, and the fact that this is not part of the update-heavy Play State where every cycle and every tick and every saved render really counts. So this works and I’ll leave it like that for now.

MapSelectIcon.as

Problem

As a player, on the Map Select screen, I want to be able to see available maps, get some map info about them when I hover, and click to select the map I want to play next

Solution

Continuing the trend of purely functional, I use a single shrunk-down screenshot of each Map the user can play as the clickable button. When they hover over the image, they get a useless description of the map. And they can click the image to select the map and begin playing. Remember again that, as a hypocrite, I am showing you really terrible UI while telling you how you should have good UI. Most likely you would have a game map or something in the screen’s background, and these individual icons would be a lot smaller. Maybe they’d represent towns on the map, or countries, or just Level 1, Level 2… Either way, displaying good information about each map is a real boon to the player. You may keep track of and display things like: previous high score, summary of enemy waves, presence of epic boss at the end of the map, modifiers existing on that map (‘raining so -10%range/damage to archer towers’), number of lives the player gets on the map, etc. I always love popovers/tooltips as a way of receiving valuable, contextual information about something I am hovering over. But that’s just me.

Execution

So the following two tiny images are loaded into the game and represent the Map 1 and Map 2 icons.
Map Select 1Map Select 2

Lets look at those in proper glorious context though…
Map Select Screen

Below when I refer to the “map data” that gets passed in, I am referring to a new file that I don’t think I’ve gone over yet. We’ll look at this new JSON file now. This is from src/assets/json/maps/mapSelectData.json.

{
   "maps": [
      {
         "id": "map1",
         "file": "assets/maps/map1.json",
         "title": "First Map!",
         "desc": "This first map is easy!"
      },
      {
         "id": "map2",
         "file": "assets/maps/map2.json",
         "title": "Second Map!",
         "desc": "This second map is also easy!"
      }
   ]
}

It contains the list of maps and their map data for the MapSelect State. The “title” and “desc” attributes can be seen above. For the majority of cases, you would have the same types of icons for this map select screen used over and over. I.e. Town icon disabled, town icon enabled, town icon completed, etc. In my demo, I happen to use screenshots of the maps which, upon looking at this file, would go great in here. An attribute for which “iconImage” to use or something. However, I didn’t do that, and instead I’m passing in the texture I want for the button from the MapSelect State class when I initialize the MapSelectIcon’s class.

Alrighty… now, on to the code!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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;
         }
      }
   }
}
}

GameOptionsPanel.as

Problem

As a player, in the Play State, I need to be able to access game options to change sound settings.

Solution

In the game, I’ve got a very dinky “Options” Button in the top right corner. When the player clicks it, the GameOptionsPanel slides into view and lets the player change Music and SFX volume. These get saved to their main SharedObject data so once the player sets the sound volume, if they load any of the 3 game slots, or creates a new game in any of those slots, the sound settings persist. I don’t think a user would want different sound volumes for different games. That’s an assumption I made that may not be entirely user-friendly. If multiple people are playing on the same computer and they play different games, they may want different sound settings. It’s a chance I’m taking that really isn’t all that costly to have individual sound volumes for each game slot, but, this is the way I set it up.

Execution

Here is the in-game options panel:
Game Options Panel

As you can see, there is a separate volume control for the music volume and the SFX volume. Currently in the demo, since I don’t have any music playing, I simply run everything on the SFX volume, but were I to add music, I would just choose for the volume param to come from the Music Volume setting and not the SFX Volume. There is also a Quit Button here that lets the player quit out to the map select state. If the player clicks Cancel, no options are updated, and if the player clicks Save then we take the values of each of those volume settings and save them.

In this options panel, I use Feathers controls, specifically the Slider control. And I also use one of their themes, AzureMobileTheme. This theme requires that I include some assets to use it, and those can be found in the project level ./assets/ folder. It contains a fonts and images folder that it uses to skin controls. Alright, let’s look at the code found in com.zf.ui.gameOptions.GameOptionsPanel.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
package com.zf.ui.gameOptions
{
   import com.greensock.TweenLite;
   import com.zf.core.Assets;
   import com.zf.core.Config;
 
   import flash.geom.Point;
 
   import feathers.controls.Slider;
   import feathers.themes.AzureMobileTheme;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.text.TextField;
 
public class GameOptionsPanel extends Sprite
{
   public var onActivated:Signal;
   public var onDeactivated:Signal;
   public var onQuitGame:Signal;
 
   private var _bkgd:Image;
   private var _musicSlider:Slider;
   private var _sfxSlider:Slider;
   private var _musicTF:TextField;
   private var _sfxTF:TextField;
   private var _fontName:String = 'Wizzta';
   private var _saveBtn:Button;
   private var _cancelBtn:Button;
   private var _quitBtn:Button;
 
   private var _startPos:Point;
   private var _endPos:Point;
   private var _activateTween:TweenLite;
   private var _deactivateTween:TweenLite;
   private var _sfxVolume:Number;
   private var _musicVolume:Number;
 
   public function GameOptionsPanel(startPos:Point, endPos:Point) {
      _startPos = startPos;
      _endPos = endPos;
 
      onActivated = new Signal();
      onDeactivated = new Signal();
      onQuitGame = new Signal();
 
      addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }
 
   private function onAddedToStage(evt:Event):void {
      removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
      _bkgd = new Image(Assets.gameOptionsBkgdT);
      addChild(_bkgd);
 
      new AzureMobileTheme(this);
      _musicSlider = new Slider();
      _musicSlider.name = 'musicVolume';
      _musicSlider.minimum = 0;
      _musicSlider.maximum = 100;
      _musicSlider.value = Config.musicVolume * 100;
      _musicSlider.step = 1;
      _musicSlider.page = 10;
      _musicSlider.x = 160;
      _musicSlider.y = 75;
      _musicSlider.width = 200;
      addChild(_musicSlider);
 
      _sfxSlider = new Slider();
      _sfxSlider.name = 'sfxVolume';
      _sfxSlider.minimum = 0;
      _sfxSlider.maximum = 100;
      _sfxSlider.value = Config.sfxVolume * 100;
      _sfxSlider.step = 1;
      _sfxSlider.page = 10;
      _sfxSlider.x = 160;
      _sfxSlider.y = 105;
      _sfxSlider.width = 200;
      addChild(_sfxSlider);
 
      _musicTF = new TextField(50, 32, _getPercentText(Config.musicVolume));
      _musicTF.fontName = _fontName
      _musicTF.x = 365;
      _musicTF.y = 60;
      _musicTF.color = 0xFFFFFF;
      _musicTF.fontSize = 24;
      addChild(_musicTF);
 
      _sfxTF = new TextField(50, 32, _getPercentText(Config.sfxVolume));
      _sfxTF.fontName = _fontName
      _sfxTF.x = 365;
      _sfxTF.y = 90;
      _sfxTF.color = 0xFFFFFF;
      _sfxTF.fontSize = 24;
      addChild(_sfxTF);
 
      _saveBtn = new Button(Assets.optsSaveBtnT);
      _saveBtn.x = 380;
      _saveBtn.y = 235;
      addChild(_saveBtn);
 
      _cancelBtn = new Button(Assets.optsCancelBtnT);
      _cancelBtn.x = 200;
      _cancelBtn.y = 235;
      addChild(_cancelBtn);
 
      _quitBtn = new Button(Assets.optsQuitBtnT);
      _quitBtn.x = 20;
      _quitBtn.y = 235;
      addChild(_quitBtn);
 
      x = _startPos.x;
      y = _startPos.y;
      visible = false;
      touchable = false;
   }

And continuing on in com.zf.ui.gameOptions.GameOptionsPanel.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
   public function init():void {
      _musicVolume = Config.musicVolume;
      _sfxVolume = Config.sfxVolume;
      _musicSlider.value = _musicVolume * 100;
      _sfxSlider.value = _sfxVolume * 100;
   }
 
   public function activate():void {
      init();
      visible = true;
      touchable = true;
      _addListeners();
      TweenLite.to(this, 1, {x: _endPos.x, y: _endPos.y, onComplete: _activateTweenComplete});
   }
 
   public function deactivate():void {
      _removeListeners();
      TweenLite.to(this, 1, {x: _startPos.x, y: _startPos.y, onComplete: _deactivateTweenComplete});
   }
 
   public function saveOptions():void {
      Config.musicVolume = _musicVolume;
      Config.sfxVolume = _sfxVolume;
      Config.saveGameOptions();
   }
 
   private function _onSaveTriggered(evt:Event):void {
      saveOptions();
      deactivate();
   }
 
   private function _onCancelTriggered(evt:Event):void {
      deactivate();
   }
 
   private function _onQuitTriggered(evt:Event):void {
      deactivate();
      onQuitGame.dispatch();
   }
 
   private function _sliderChangeHandler(evt:Event):void {
      var slider:Slider = Slider(evt.currentTarget),
          val:Number = slider.value / 100;
 
      switch(slider.name) {
         case 'musicVolume':
            _musicVolume = val;
            _musicTF.text = _getPercentText(_musicVolume)
            break;
         case 'sfxVolume':
            _sfxVolume = val;
            _sfxTF.text = _getPercentText(_sfxVolume)
            break;
      }
   }
 
   private function _getPercentText(percent:Number):String {
      return (int(percent * 100)).toString() + '%';
   }
 
   private function _activateTweenComplete():void {
      onActivated.dispatch();
   }
 
   private function _deactivateTweenComplete():void {
      visible = false;
      touchable = false;
      onDeactivated.dispatch();
   }
 
   private function _addListeners():void {
      _musicSlider.addEventListener(Event.CHANGE, _sliderChangeHandler);
      _sfxSlider.addEventListener(Event.CHANGE, _sliderChangeHandler);
      _saveBtn.addEventListener(Event.TRIGGERED, _onSaveTriggered);
      _cancelBtn.addEventListener(Event.TRIGGERED, _onCancelTriggered);
      _quitBtn.addEventListener(Event.TRIGGERED, _onQuitTriggered);
   }
 
   private function _removeListeners():void {
      _musicSlider.removeEventListener(Event.CHANGE, _sliderChangeHandler);
      _sfxSlider.removeEventListener(Event.CHANGE, _sliderChangeHandler);
      _saveBtn.removeEventListener(Event.TRIGGERED, _onSaveTriggered);
      _cancelBtn.removeEventListener(Event.TRIGGERED, _onCancelTriggered);
      _quitBtn.removeEventListener(Event.TRIGGERED, _onQuitTriggered);
   }
 
}
}

UpgradeOption.as

Problem

As a player, on the Upgrade screen, I want to see how many upgrade points I have, the different options that I can upgrade, and I want to be able to add or remove to those points at any time. I also want a “Reset” button so if I’ve added a lot of points, I can ‘clear the board’ and start allocating points from 0.

Solution

So, I wanted to hit the core points of the problem. Currently the Feathers NumericStepper is in beta, so it’s not actually available for me to use here. I was bummed about that. So I’ll make my own in a way. So I needed art assets: a + and – button with enabled and disabled states, and I wanted something that showed off that I was “putting points into it” so I went with the following that I drew all by myself!!

Execution

In the project folder, these get drawn onto my atlas.png tilesheet, but you can find the individual images in src/assets/images/ui
Upgrade Options
In order from Left to Right: upgrade_add_disabled.png, upgrade_add_enabled.png, upgrade_rank_notselected.png, upgrade_rank_selected.png, upgrade_sub_disabled.png, upgrade_sub_enabled.png.

And here we can see them in the game screen.
Upgrade Options In Game
Available Points are displayed in the upper right with the reset button, and to the left we’ve got our actual UpgradeOptions.as component. It’s made up of a text field with the name of the upgrade, a minus button, a plus button, and 5 ‘ranks’ that can be filled up.

Tragically missing from this design, which you should implement in your game, is what each point in each rank actually does for you. This is not documented anywhere, but right now, each point you put into Tower Attack Speed decreases time by 10%, Tower Range gets you a 15% range boost for the tower, and Tower Damage gives you 20% per rank. These should be built into the UI in a helpful way so the player always knows the cost of each decision.

Now let’s look at the code from com.zf.ui.upgradeOption.UpgradeOption.as. It basically handles creating a reusable UI component where we can pass in a few params and create those three “Tower Attack Speed”, “Tower Range” and “Tower Damage components.

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
package com.zf.ui.upgradeOption
{
   import com.zf.core.Assets;
 
   import org.osflash.signals.Signal;
 
   import starling.display.Button;
   import starling.display.Image;
   import starling.display.Sprite;
   import starling.events.Event;
   import starling.text.TextField;
public class UpgradeOption extends Sprite
{
   public var optionChanged:Signal;
 
   private var _id:String;
   private var _label:String;
   private var _labelTF:TextField;
   private var _enabled:Boolean;
   private var _totalRanks:int;
   private var _currentRanks:int;
   private var _bonusPerRank:Number;
   private var _plusBtn:Button;
   private var _minusBtn:Button;
   private var _ranks:Array;
   private var _onStage:Boolean = false;
 
   public function UpgradeOption(id:String, label:String, numRanks:int, bonus:Number, enabled:Boolean = false) {
      _id = id;
      _label = label;
      _totalRanks = numRanks;
      _bonusPerRank = bonus;
      _enabled = enabled;
      optionChanged = new Signal(Boolean, Object);
 
      addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
   }
 
   public function onAddedToStage(evt:Event):void {
      removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
      _onStage = true;
      _ranks = [];
 
      _labelTF = new TextField(200, 35, _label, 'Wizzta', 30, 0xFFFFFF);
      _labelTF.y = -40;
      addChild(_labelTF);
      update();
   }
 
   public function update():void {
      _updateButtons();
      _updateRanks();
   }
 
   private function _updateButtons():void {
      _addPlusButton();
      _addMinusButton();
   }
 
   private function _updateRanks():void {
      var rank:Image,
          texture:String = '';
      for(var i:int = 0; i < _totalRanks; i++) {
         if(i < _currentRanks) {
            texture = 'upgrade_rank_selected'
         } else {
            texture = 'upgrade_rank_notselected';
         }
 
         rank = new Image(Assets.ta.getTexture(texture));
         rank.x = 70 + (30 * i);
         rank.y = 0;
         addChild(rank);
 
         _ranks.push(rank);
      }
   }
 
   public function set currentRanks(i:int):void {
      _currentRanks = i;
      if(_onStage) {
         update();
      }
   }
 
   public function getOptionValue():Object {
      return {
         'id': _id,
         'label': _label,
         'totalRanks': _totalRanks,
         'currentRanks': _currentRanks,
         'bonusPerRank': _bonusPerRank
      }
   }
 
   public function disable():void {
      _enabled = false;
      update();
   }
 
   public function enable():void {
      _enabled = true;
      update();
   }

And continuing with UpgradeOption.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
   private function _addPlusButton():void {
      // check if the _minusBtn already exists on stage
      if(contains(_plusBtn)) {
         _plusBtn.removeFromParent(true);
      }
 
      var texture:String = 'upgrade_add_disabled',
          addListener:Boolean = false;
      if(_enabled && _currentRanks < _totalRanks) {
         texture = 'upgrade_add_enabled';
         addListener = true;
      }
 
      _plusBtn = new Button(Assets.ta.getTexture(texture));
      _plusBtn.x = 40;
      _plusBtn.y = 0;
      _plusBtn.name = 'add';
      addChild(_plusBtn);
 
      if(addListener) {
         _plusBtn.enabled = true;
         _plusBtn.addEventListener(Event.TRIGGERED, _onButtonClicked);
      } else {
         _plusBtn.enabled = false;
         _plusBtn.removeEventListener(Event.TRIGGERED, _onButtonClicked);
      }
   }
 
   private function _addMinusButton():void {
      // check if the _minusBtn already exists on stage
      if(contains(_minusBtn)) {
         _minusBtn.removeFromParent(true);
      }
      var texture:String = 'upgrade_sub_disabled',
          addListener:Boolean = false;
      if(_enabled && _currentRanks > 0) {
         texture = 'upgrade_sub_enabled';
         addListener = true;
      }
 
      _minusBtn = new Button(Assets.ta.getTexture(texture));
      _minusBtn.x = 10;
      _minusBtn.y = 0;
      _minusBtn.name = 'sub';
      addChild(_minusBtn);
 
      if(addListener) {
         _minusBtn.enabled = true;
         _minusBtn.addEventListener(Event.TRIGGERED, _onButtonClicked);
      } else {
         _minusBtn.enabled = false;
         _minusBtn.removeEventListener(Event.TRIGGERED, _onButtonClicked);
      }
   }
 
   private function _addListeners():void {
      _plusBtn.addEventListener(Event.TRIGGERED, _onButtonClicked);
      _minusBtn.addEventListener(Event.TRIGGERED, _onButtonClicked);
   }
 
   private function _removeListeners():void {
      _plusBtn.removeEventListener(Event.TRIGGERED, _onButtonClicked);
      _minusBtn.removeEventListener(Event.TRIGGERED, _onButtonClicked);
   }
 
   private function _onButtonClicked(e:Event):void {
      var addedRank:Boolean = false;
      if(Button(e.currentTarget).name == 'add') {
         _currentRanks++;
         addedRank = true;
      } else {
         _currentRanks--;
      }
 
      update();
      optionChanged.dispatch(addedRank, getOptionValue());
   }
 
   public function destroy():void {
      _labelTF.removeFromParent(true);
      _labelTF = null;
 
      for(var i:int = 0; i < _ranks.length; i++) {
         _ranks[i].removeFromParent(true);
         _ranks[i] = null;
         _ranks.splice(i, 1);
      }
 
      _plusBtn.removeFromParent(true);
      _plusBtn = null;
 
      _minusBtn.removeFromParent(true);
      _minusBtn = null;
   }
}
}
  • Line 1-54 – these next two functions _addPlusButton() and _addMinusButton() are perfect candidates for some easy refactoring at a later date. I don’t know why I didn’t do this to start with, but here we are. These are essentially the exact same functions but they just have a few differences in the texture names to use, the name of the button, and the x value. I’ll go through the first function and you can apply those to the second. Oh, and some logic on how it enables/disables itself.
  • Line 3-5 – As the comment said, check to see if _plusBtn is already on this Sprite, if so, remove it
  • Line 9-12 – if this component is enabled and the _currentRanks is less than _totalRanks (meaning: I’ve got 3 points into this component that has a total of 5 points) then I can add more points to the component so set the texture to the add_enabled image and addListener is true (because I’ll want to add the event listeners)
  • Line 14-18 – creates a new Button with the proper texture and adds it to this Sprite
  • Line 20-26 – if addListener is true, add the event listeners to the plusBtn and enable it. If not, disable it (grays out the button) and remove the event listener
  • Line 56 – wrapper function to add event listeners to both buttons
  • Line 61 – wrapper function to remove event listeners to both buttons
  • Line 66 – event handler for when both the plus and minus buttons are clicked
  • Line 68-73 – check the name of the Button, if it’s ‘add’, then increment _currentRanks and set addedRank to true, otherwise decrement _currentRanks and leave addedRank to false.
  • Line 75 – update() is called to update the UI
  • Line 76 – optionChanged Signal is dispatched with if we added a rank or not, and we get the significant private variables, wrap them in an Object and send them off with this Signal as well
  • Line 79-94 – boring standard destroy() function except for 83-87 where we loop through the _ranks images, remove them from this Sprite, remove them from the _ranks array and null them out.

All of the other work is handled by the Upgrades State which we’ll look at another time.

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

Share

AS3 Starling Tower Defense Tutorial – Part 7 – Sounds, Stats, and Saves

We’ve knocked out the Big 2 posts already. Enemies and Towers have just been covered. Now we’re going to lump together Sounds and sound management, specifically with TreeFortressSoundAS. Then we’re going to take a look back at stats or “Totals” as the code refers to them (sorry for the misleading title, but ‘stats’ started with an ‘s’ and fit the pattern, but it’s really “Totals”). And finally we’re going to tie Totals up with a bow and make it persist wiiiith… our SharedObject Manager. I wrote this sucker myself, so you can blame me* when it decides to wipe three weeks of your hard-won stats.

*Note, please don’t blame me if this code does something unexpected!

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

Onward!

Sounds

Now that I look at it, I really don’t do a whole lot at all with sounds in this demo. It’s all very basic but I’ll walk through it nonetheless because a Flash game (or any kind of game) without sound is a very bad idea. Let’s revisit the whole process, starting with my initial sounds.json file.

Really quick though, in case my point hasn’t been made enough times in previous posts regarding loading files manually and not embedding them in your project, let’s take this very short list of sound files below and do some thought experiments. Imagine you’ve got a game completed and it’s posted up at Kongregate or ArmorGames. You chose to go the route of having all of your sound files embedded in your project via [Embed(source=”path/to/my.mp3″)], but now you want to change the path for an old var and point it to embed a new sound file. What happens when you want to do that? Well, Mr. CodeMonkey, you fire up your IDE and go change it, recompile, export your release build, push the new SWF up to wherever your game is hosted, and when the cache expires in the clients’ browsers, each individual player starts getting the new sounds whenever the new SWF loads.

If you had gone the route of loading sound files externally from a json file like the one below and you wanted to change paths, you or anyone on your team could open a text editor, change the path, upload the json file and the new mp3 file, and every cached swf is already loading that json file every time. It doesn’t care what those paths look like. So since its cached, it loads up super fast for the player, reads in that file and loads in the new sound with the player never knowing anything changed.

src/assets/json/sounds.json

{
   "files": [
      {
         "id": "ding1",
         "file": "assets/sounds/ding1.mp3"
      },
      {
         "id": "escape1",
         "file": "assets/sounds/escape1.mp3"
      },
      {
         "id": "shot1",
         "file": "assets/sounds/shot1.mp3"
      },
      {
         "id": "shot2",
         "file": "assets/sounds/shot2.mp3"
      },
      {
         "id": "banish1",
         "file": "assets/sounds/banish1.mp3"
      }
   ]
}

So let’s say players keep complaining about the shot1 sound. So we want to swap that out with a shot3.mp3 file that we just recorded or found. All we have to do is change that path
“file”: “assets/sounds/shot3.mp3” and whatever was supposed to play sound “shot1” will still play a sound, it’ll just be coming from shot3.mp3. No need to go into any class files and change any IDs anywhere or update any code!

As an interesting rabbit to chase that I thought about, I have not played around with this yet, but this seems like a really smart idea. So if you added some sort of “chance” or “frequency” param in there…

{
   "id": "shot1a",
   "file": "assets/sounds/shot1a.mp3",
   "freq": 0.25
},
{
   "id": "shot1b",
   "file": "assets/sounds/shot1b.mp3",
   "freq": 0.25
},
{
   "id": "shot1c",
   "file": "assets/sounds/shot1c.mp3",
   "freq": 0.50
},

Remember back in Tower.as the _playIfStateExists() function could generate a random number between 0 and 1. If you did some math when you read this sound metadata in, you could convert those frequencies to fall between 0 and 1. So, shot1c would have a 50% shot to get played as the sound this time, and shot1a and shot1b would each have a 25% chance. So each time your Tower fires, there’s a chance you’d play a slightly different sound. That’d help keep your players from getting bored with your sounds? Just a thought as I’m writing this.

So the sounds.json file gets read in and Assets.as calls each of the “file” params, loading each path/file. When the file loads, Assets calls

   Game.soundMgr.addSound(cbo.name, cbo.data);

cbo.name is the “id” param from sounds.json, and cbo.data is the actual MP3 Sound. SoundAS stores the sound object in a Dictionary and also saves it by id in an Object, so, you can call the sound by it’s String ID. I’m not going to post any code of theirs here because I’d hate to let that get stale, if they updated it and I’m sitting here with a code example from 2 versions ago. Go check it out for yourself at their git repo or check out the files that come with the project in src/treefortress/sound/.

So, we call addSound(), *magic* happens, and later on when we actually want to play the sound we call:

   Game.soundMgr.playFx(_soundData[state], Config.sfxVolume);

And remember, _soundData[state] keeps the ID for the sound I want to play, and Config.sfxVolume holds the current volume for sound effects. Currently, SoundAS has a few different ways to play your sounds. play() is the main function that actually plays a sound file. play() has a lot of params you can pass in. playFx() is one of their “convenience functions” which handles passing in a few params for you. It seemed much quicker and exactly what I wanted for this sound effect. SoundAS allows for the same sound to “overlap” or not. If you had a character in your game that when you clicked on them, they said “What do you want?” (probably in a funny voice I bet) and you keep clicking on him and one instance of the sound starts and tries to play, then you click again and it cuts off the sound (or worse, layers the same file on top of your first sound clip) and you keep doing this, you’ll never hear with the character says. But you could set those sounds to play and not allow multiple instances of the same sound to be played at the same time. Pretty smart.

playFx(), however, does allow for playing the same sound clip multiple times, as you would expect of a sound effect. Every time a tower fires I want that shot sound to fire no matter how many other towers are firing. And that’s why I’m using it.

So, sadly, that’s really all I’m using sounds for in this demo. I don’t have any music playing in the demo or really anything interesting happening sound-wise. But check out SoundAS. I’ve been working with it in my personal game that I’m working on and it’s been a really great experience. Definitely up there with Greensock’s Tween/Loading libs on how fast I was able to get started with someone else’s code library and make it do exactly what I wanted it to do and not cause me problems. Mo’ libs, Mo’ problems? Not with SoundAS, man!

Anyways… let’s move on to part two. Totals aka ‘stats’ aka ‘totals’ or whatever.

Totals.as

com.zf.core.Totals.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
package com.zf.core
{
   import com.zf.utils.Utils;
 
public class Totals
{
   public var totalDamage:Number;
   public var towersPlaced:int;
   public var enemiesSpawned:int;
   public var enemiesBanished:int;
   public var enemiesEscaped:int;
   public var bulletsFired:int;
   public var mapsCompleted:int;
   public var mapsWon:int;
 
   private var _str:String;
   private var _props:Array = ['totalDamage', 'towersPlaced', 'enemiesSpawned', 'enemiesBanished', 'enemiesEscaped', 'bulletsFired'];
 
   public function Totals() {
      reset();
   }
 
   public function reset():void {
      totalDamage 	= 0;
      towersPlaced 	= 0;
      enemiesSpawned 	= 0;
      enemiesBanished   = 0;
      enemiesEscaped 	= 0;
      bulletsFired 	= 0;
      mapsCompleted	= 0;
 
      // if player wins, update this to 1
      mapsWon           = 0;
 
      _str = '';
   }
 
   public function toString():String {
      return 'Game Stats:\n\n' + Utils.objectToString(this, _props, '\n');
   }
 
   public function toHtml():String {
      var s:String = '<font color="#FFFFFF" size="20">Game Stats: <br><br>';
      if(mapsWon) {
         s += 'MAP WON!';
      } else {
         s += 'Map Lost :(';
      }
      s += '</font><br><br>';
 
      s += Utils.objectToString(this, _props, '<br>', true);
 
      return s;
   }
}
}

That’s it there really. Most everywhere that updates those totals just does so explicitly since all the totals are public vars. Since this was such a short section let’s go ahead and throw in Utils since I just called it twice.

Utils.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
package com.zf.utils
{
   import com.adobe.serialization.json.JSON;
 
public class Utils
{
   public static function JSONDecode(s:String):Object {
      return com.adobe.serialization.json.JSON.decode(s);
   }
 
   public static function objectToString(obj:*, props:Array, lineBr:String = '\n', 
          toHTML:Boolean = false, fontSize:int = 20, fontColor:String = '#FFFFFF'):String 
   {
      var str:String = '',
          len:int = props.length;
 
      for(var i:int = 0; i < len; i++) {
         if(toHTML) {
            str += '<font color="' + fontColor + '" size="' + fontSize + '">';
         }
 
         str += props[i] + ' = ' + int(obj[props[i]]);
 
         if(toHTML) {
            str += '</font>';
         }
 
         str += lineBr;
      }
 
      return str;
   }
}
}

Ok, so we’ve trekked through sounds and stats, now it’s time for saves!

SharedObjectManager.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
package com.zf.managers
{
   import com.zf.core.Config;
   import com.zf.utils.ZFGameData;
 
   import flash.net.SharedObject;
 
public class SharedObjectManager
{
   public var so:SharedObject;
 
   private static var instance	         : SharedObjectManager;
   private static var allowInstantiation : Boolean;
 
   private const LOCAL_OBJECT_NAME	 : String = 'zombieflambe_demo';
 
   /***
    * Gets the singleton instance of SharedObjectManager or creates a new one
    */
   public static function getInstance():SharedObjectManager {
      if (instance == null) {
         allowInstantiation = true;
         instance = new SharedObjectManager();
         allowInstantiation = false;
      }
      return instance;
   }
 
   public function SharedObjectManager() {
      if (!allowInstantiation) {
         throw new Error("Error: Instantiation failed: Use SharedObjectManager.getInstance() instead of new.");
      } else {
         init();
      }
   }
 
   /**
    * Initializes the class, if gameOptions hasn't been set before, create gameOptions
    * Otherwise it initializes Config gameOptions from the shared object
    */
   public function init():void {
      so = SharedObject.getLocal(LOCAL_OBJECT_NAME);
      if(!so.data.hasOwnProperty('gameOptions')) {
         // set up the global game options
         so.data.gameOptions = {
            musicVolume: Config.DEFAULT_SOUND_VOLUME,
            sfxVolume: Config.DEFAULT_SOUND_VOLUME
         };
         Config.musicVolume = Config.DEFAULT_SOUND_VOLUME
         Config.sfxVolume = Config.DEFAULT_SOUND_VOLUME
      } else {
         Config.musicVolume = so.data.gameOptions.musicVolume;
         Config.sfxVolume = so.data.gameOptions.sfxVolume;
      }
   }
 
   /**
    * Create a new ZFdata object in name's place in data
    *
    * @param {String} name the current game name
    * @param {Boolean} updateThenSave if true, the function will call save, otherwise the user can save manually
    */
   public function createGameData(name:String, updateThenSave:Boolean = false):void {
      so.data[name] = new ZFGameData();
      so.data.gameOptions = {
         musicVolume: Config.DEFAULT_SOUND_VOLUME,
         sfxVolume: Config.DEFAULT_SOUND_VOLUME
      };
 
      // Reset config values
      Config.musicVolume = Config.DEFAULT_SOUND_VOLUME;
      Config.sfxVolume = Config.DEFAULT_SOUND_VOLUME;
      if(updateThenSave) {
         save();
      }
   }
 
   /**
    * Gets a whole block of game data
    *
    * @param {String} name the current game name
    * @returns {Object} the game data Object requested by game name
    */
   public function getGameData(name:String):Object {
      return (so.data[name]) ? so.data[name] : {};
   }
 
   /**
    * Sets a whole block of game data
    *
    * @param {Object} data the data we want to add
    * @param {String} name the current game name or blank to use Config.currentGameSOID
    * @param {Boolean} updateThenSave if true, the function will call save, otherwise the user can save manually
    */
   public function setGameData(data:Object, name:String = '', updateThenSave:Boolean = false):void {
      if(name == '') {
         name = Config.currentGameSOID;
      }
 
      so.data[name] = Object(data);
 
      if(updateThenSave) {
         save();
      }
   }
 
   /**
    * Sets a single property from game data
    *
    * @param {*} data the data we want to add
    * @param {String} prop the name of the property to update
    * @param {String} name the current game name or blank to use Config.currentGameSOID
    * @param {Boolean} updateThenSave if true, the function will call save, otherwise the user can save manually
    */
   public function setGameDataProperty(data:*, prop:String, name:String = '', updateThenSave:Boolean = false):void {
      if(name == '') {
         name = Config.currentGameSOID;
      }
 
      // check for nested property
      if(prop.indexOf('.') != -1) {
         // happens when you pass in 'upgrades.ptsTotal' will split by the . and
         // pass data in to so.data[name]['upgrades']['ptsTotal']
         var props:Array = prop.split('.');
         so.data[name][props[0]][props[1]] = data;
      } else {
         so.data[name][prop] = data;
      }
 
      if(updateThenSave) {
         save();
      }
   }
 
   /**
    * Gets a single property from game data
    *
    * @param {String} prop the name of the property to update
    * @param {String} name the current game name or blank to use Config.currentGameSOID
    * @returns {*} the game data property requested
    */
   public function getGameDataProperty(prop:String, name:String = ''):* {
      if(name == '') {
         name = Config.currentGameSOID;
      }
      return so.data[name][prop];
   }
 
   /**
    * Sets the global gameOptions Object on the SO
    *
    * @param {Object} data the gameOptions data we want to add
    * @param {Boolean} updateThenSave if true, the function will call save, otherwise the user can save manually
    */
   public function setGameOptions(data:Object, updateThenSave:Boolean = false):void {
      so.data.gameOptions.musicVolume = (!isNaN(data.musicVolume)) ? data.musicVolume : 0;
      so.data.gameOptions.sfxVolume = (!isNaN(data.sfxVolume)) ? data.sfxVolume : 0;
 
      if(updateThenSave) {
         save();
      }
   }
 
   public function dev_WipeAllMem():void {
      createGameData('game1', true);
      createGameData('game2', true);
      createGameData('game3', true);
   }
 
   /**
    * Gets the global gameOptions Object from the SO
    *
    * @returns {Object} the saved gameOptions data
    */
   public function getGameOptions():Object {
      return so.data.gameOptions;
   }
 
   /**
    * Checks to see if a game name exists on the SO
    *
    * @param {String} name the game name we want to check for to see if it exists
    * @returns {Boolean} if the game exists or not
    */
   public function gameExists(name:String):Boolean {
      return (so.data[name])
   }
 
   /**
    * Saves the SO to the user's HD
    */
   public function save():void {
      so.flush();
   }
}
}

I left the comments in this file for some reason, so I’ll just add some commentary around those.

That’s the SharedObjectManager. We’ll be looking at a few places that we use the SharedObjectManager in the next post on UI.

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!

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.objects.enemy.Enemy;
 
public class TowerStrategyNearest implements ITowerStrategy
{
   public function TowerStrategyNearest() {
   }
 
   public function findEnemy(enemies:Array):Enemy {
      var targetEnemy:Enemy,
          len:int = enemies.length,
          tmpEnemy:Enemy,
          closestDist:Number = -1;
 
      for(var i:int =0