AS3 BackboneJS-style Model Experiment Pt. 2 – Target Value Model Events

As we saw in AS3 BackboneJS-style Model Experiment Pt. 1 (definitely read through that first!) I created a BackboneJS-style model for use in AS3. You could listen for any change on the model, or for a specific property on the model to change.

This got me thinking though, let’s go back to my previous example where you have a “mana” property on the PlayerModel changing, and you have 6 Tower Icons that are listening for ‘change:mana’ and they’re having to check if the changed mana value is greater than or equal to the cost of the individual tower to know if the Tower Icon should “activate” visually and become clickable so the player can place a specific tower.

This means that every single time that single property changes, the Tower Icon is having to do a check to see if it should activate or not. Now, as we saw last post, that’s a lot more efficient than just getting a “change” event any time anything on the model changes… but, why can’t we give a specific Target value we’re looking for when we add the event listener?

Enter TargetModelEvents!

We looked at most of this code already so I’ll try to streamline it… first, again, the pieces that make the model work:

TargetModelEvent

This is the actual Event that gets sent to your callback:

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
package com.zf.events
{
   public class TargetModelEvent
   {
      // The ID of the component the event was set on
      public var contextId:String;
 
      // the model Object the event happened on
      public var model:Object;
 
      // the field name change that triggered the event
      public var key:String;
 
      // the new value of the field that changed
      public var value:*;
 
      // if the new value matches to comparison
      public var matches:Boolean;
 
      // if this is the initial time the value has been set on the model
      public var initialSet:Boolean;
 
      /**
       * Constructor for the TargetModelEvent
       * 
       * @param {Object} eventModel The model the event is being triggered from
       * @param {String} eventKey The field that changed to trigger the event
       * @param {*} newVal The new value the field changed to
       * @param {Boolean} match Does the newVal match the TargetModelEventObject's comparison
       * @param {Boolean} isInitial Is this the initial event being triggered for this field
       */
      public function TargetModelEvent(context:String, eventModel:Object, eventKey:String, newVal:*, match:Boolean, isInitial:Boolean) {
         contextId = context;
         model = eventModel;
         key = eventKey;
         value = newVal;
         matches = match;
         initialSet = isInitial;
      }
   }
}

TargetModelEventObject

TargetModelEventObject is the internal object used by the ZFModel to keep track of certain data:

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
package com.zf.events
{
   import com.zf.events.ModelEventObject;
 
   public class TargetModelEventObject
   {
      public static const GT:String = '>';
      public static const GTE:String = '>=';
      public static const LT:String = '<';
      public static const LTE:String = '<=';
      public static const EQ:String = '=';
 
      // the "target" value we want to listen for
      public var target:Number;
 
      // the comparison we want made for changing values vs the target
      public var compareType:String;
 
      // if the value tested matches vs the target comparison
      public var matches:Boolean;
 
      // the last state of the matches var to see if it changed
      public var lastMatches:Boolean;
 
      // if this is the initial time the comparison happens
      public var initialSet:Boolean;
 
      // The ID of the component setting the event
      public var contextId:String;
 
      // an internal ModelEventObject which has similar properties to set
      protected var modelEventObj:ModelEventObject;
 
      // if this is the first time running shouldTrigger
      protected var firstRun:Boolean;
 
      /**
       * Constructor for the TargetModelEventObject
       * 
       * @param {String} eventName The full event name string
       * @param {Function} cb The callback to use when triggering
       * @param {Number} targetValue The target value we want to check future values against
       * @param {String} comparison The inequality to compare future values vs the target
       * @param {String} context The ID of the component setting the event
       */
      public function TargetModelEventObject(eventName:String, cb:Function, targetValue:Number, comparison:String, context:String) {
         modelEventObj = new ModelEventObject(eventName, cb);
         target = targetValue;
         compareType = comparison;
         contextId = context;
 
         firstRun = true;
         initialSet = true;
      }
 
      /**
       * Check if the value matches the target and return a Boolean if
       * the model should trigger an event or not
       * 
       * @param {Number} val The value to test against the target
       * @return {Boolean} doTrigger True if the model should trigger an event, false if not
       */
      public function shouldTrigger(newVal:Number):Boolean {
         var doTrigger:Boolean = false,
            comparison:int = compare(newVal);
 
         if(firstRun) {
            firstRun = false;
            // always trigger on first change on the model
            doTrigger = true;
            // init matches and lastMatches to the value that comes back
            matches = lastMatches = doesValueMatch(newVal, comparison);
         } else {
            initialSet = false;
            lastMatches = matches;
            matches = doesValueMatch(newVal, comparison);
            if(matches != lastMatches) {
               doTrigger = true;
            }
         }
 
         return doTrigger;
      }
 
      /**
       * Converts the TargetModelEventObject into a TargetModelEvent for callbacks
       * 
       * @param {Object} model The model that changed
       * @param {Number} newValue The new value that changed
       * @return {TargetModelEvent} A new TargetEventModel instance
       */
      public function toCallbackParams(model:Object, newValue:Number):TargetModelEvent {
         return new TargetModelEvent(contextId, model, eventField, newValue, matches, initialSet);
      }
 
      public function toString():String {
         var str:String = '[TargetModelEventObject: '
            + 'target: ' + target.toString() + ', '
            + 'compareType: ' + compareType + ', '
            + 'eventName: ' + modelEventObj.eventName + ', '
            + 'eventType: ' + modelEventObj.eventType + ', '
            + 'eventField: ' + modelEventObj.eventField + ' ]';
 
         return str;
      }
 
      /**
       * Checks to see if the value matches the target comparison
       * 
       * @param {Number} val The value to test against the target
       * @param {int} comparison The int version of a comparison: 1 if >, -1 if <, 0 if ==
       * @return {Boolean} valMatches True if the value matches the target comparison, false if not
       */
      private function doesValueMatch(val:Number, comparison:int):Boolean {
         var valMatches:Boolean = false;
         if(comparison == 1 && (compareType == GT || compareType == GTE)) {
            valMatches = true;
         } else if(comparison == -1 && (compareType == LT || compareType == LTE)) {
            valMatches = true;
         } else if(comparison == 0 
               && (compareType == EQ || compareType == GTE || compareType == LTE)) {
	    valMatches = true;
         }
 
         return valMatches;
      }
 
      /**
       * Compares the value against the target value
       * 
       * @param {Number} val The value to test against the target
       * @return {int} test The int version of a comparison: 1 if >, -1 if <, 0 if ==
       */
      private function compare(val:Number):int {
         var test:int;
         if(val > target) {
            test = 1;
         } else if(val < target) {
            test = -1;
         } else if(val == target) {
            test = 0;
         }
 
         return test;
      }
 
      /**
       * Expose modelEventObj's properties thru this
       */
      public function get eventName():String { 
         return modelEventObj.eventName; 
      }		
      public function get eventField():String { 
         return modelEventObj.eventField; 
      }		
      public function get callback():Function { 
         return modelEventObj.callback; 
      }		
      public function get hasFieldEvent():Boolean { 
         return modelEventObj.hasFieldEvent; 
      }	
   }
}

TargetModelEventObject has some constants that could definitely be moved out at some point to make things easier to type. The biggest function here is the “shouldTrigger()” function. That’s what ZFModel uses to know if this specific TargetModelEvent should be triggered or not.

ZFModel

And finally! ZFModel with my TargetModelEvents code built in! For the sake of brevity, I’ve removed all the code that had to do with Part 1 and didn’t affect Part 2. I wouldn’t copy/paste this code and try to use it. Download the Project file from the link at the end of this post and see the full ZFModel there.

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
package com.zf.models
{
   import com.zf.events.ModelEventObject;
   import com.zf.events.TargetModelEventObject;
   import com.zf.utils.Utils;
 
   import flash.utils.Dictionary;
 
   public class ZFModel
   {
      private var _attributes:Object;
      private var _events:Dictionary;
 
      public function ZFModel(options:Object = null) {
         // setup events Dictionary
         _events = new Dictionary(true);
         clean(true);
 
         // if options were passed in, set them on the model
         if(options) {
            set(options);
         }
      }
 
      /**
       * Adds a new TargetModelEventObject for the event to the model
       * 
       * @param {String} eventName The full event name to listen for: 'change:myField'
       * @param {Function} callback The function to call back when triggering the event
       * @param {Number} targetValue The target number to check future changes against
       * @param {String} comparison The inequality to use when checking target vs new values
       */
      public function onTarget(eventName:String, callback:Function, targetValue:Number, comparison:String, contextId:String):void {
         var targetMEO:TargetModelEventObject = new TargetModelEventObject(eventName, callback, targetValue, comparison, contextId);
 
         if(!_events['target'][targetMEO.eventField]) {
            _events['target'][targetMEO.eventField] = [];
         }
 
         _events['target'][targetMEO.eventField].push(targetMEO);
      }
 
      /**
       * Removes a TargetModelEventObject from the model
       * 
       * @param {String} eventName The full event name to remove
       * @param {Function} callback The callback function to remove
       * @param {Number} targetValue The target number originally listened for
       * @param {String} comparison The inequality originally listened for
       */
      public function offTarget(eventName:String, callback:Function, targetValue:Number, comparison:String, contextId:String):void {
         var targetMEO:TargetModelEventObject = new TargetModelEventObject(eventName, callback, targetValue, comparison, contextId);
 
         // if the field doesnt exist, return
         if(!_events['target'][targetMEO.eventField]) {
            return;
         }
 
         var tmp:Array = _events['target'][targetMEO.eventField];
         for(var i:int = 0; i < tmp.length; i++) {
            if(tmp[i].callback == callback 
                  && tmp[i].target == targetValue
                  && tmp[i].compareType == comparison
                  && tmp[i].contextId == contextId) {
               tmp.splice(i, 1);
               break;
            }
         }
 
         if(_events['target'][targetMEO.eventField].length == 0) {
            delete _events['target'][targetMEO.eventField];
         }
      }
 
      /**
       * Removes and resets all event listeners on the model
       * 
       * @param {Boolean} [destroyAttribs=false] If true, will destroy all attributes on the model
       */
      public function clean(destroyAttribs:Boolean = false):void {
         if(destroyAttribs) {
            // Create a new object, destroying anything that previously existed
            _attributes = {};
         }			
 
         // TargetModelEvents
         _events['target'] = new Dictionary(true);
      }
 
      /**
       * Sets a single property or an object with properties on the model
       * 
       * @param {*} key The key name of the value to set on the model
       * @param {*} [val=null] The value to set on the model
       * @param {Boolean} [silent=false] If silent is true, will not dispatch a change event
       */
      public function set(key:*, val:* = null, silent:Boolean = false):void {
         // if key comes in as an object
         if(typeof(key) == 'object') {
            for (var prop:* in key) { 
               _setValue(prop, key[prop]);
            } 
         } else if(typeof(key) == 'string') {
            _setValue(key, val);
         }
      }
 
      /**
       * Gets an attribute from the model
       * 
       * @param {String} key The key name to get from the model
       * @return {*} Returns the value from the model
      */
      public function get(key:String):* {
         return _attributes[key];
      }
 
      /**
       * Checks to see if the key is on this ZFModel, or on _attributes if IS_EXPANDABLE
       * 
       * @private
       * @param key the key name of the value to set on the model
       * @param val the value to set on the model
       */
      private function _setValue(key:String, value:*, silent:Boolean = false):void  {
         if(_attributes[key] && _attributes[key] == value) {
            // the values didnt change so don't dispatch change event
            return;
         }
         // set the new value
         _attributes[key] = value;
 
         if(!silent) {
            _dispatchEvent('change', _attributes, key, value);
         }
      }
 
      /**
       * Iterates through all events and triggers callbacks for appropriate events
       * 
       * @param {String} eventType The type of event being dispatched: 'change' for now 
       * @param {Object} model The model the event is being triggered from
       * @param {String} key The field name that is changing on the model
       * @param {*} value The new value the field changed to
       */
      private function _dispatchEvent(eventType:String, model:Object, key:String, value:*):void {
         var i:int;
 
         // dispatch for TargetModelEventObjects
         if(_events['target'][key]) {
            var tmpTMEO:TargetModelEventObject;
            for(i = 0; i < _events['target'][key].length; i++) {
               tmpTMEO = _events['target'][key][i];
               if(tmpTMEO.shouldTrigger(value)) {
                  tmpTMEO.callback(tmpTMEO.toCallbackParams(model, value));
               }
            }
         }
      }
   }
}

NOW let’s look at our model callback counts… click the “Start/Restart” label to kick things off:

Get Adobe Flash player

As you can see from the demo, when a listened-for value is initially set, a TargetModelEvent is triggered with the initialSet property set to true. When the target condition is reached (in this case, the target value was 5 and the condition was greater-than-or-equal-to) the callback gets triggered and the TargetModelEvent’s “matches” property is set to true. This was done with the following code from the Project files:

targetModel.onTarget('change:mana', onTargetChanged, 5, TargetModelEventObject.GTE, 'targetModel');

So, now your Tower Icons just call playerModel.onTarget(‘change:mana’, callbackFn, targetValue, targetCondition, contextID) and now all the TowerIcon has to check is if(event.matches) { // do stuff! } The “contextId” property is basically just a unique ID you want to set so you can delete the callback/listener properly.

ZFModel Usage

With the ZFModel you can now do things like the following:

// listen for a property to reach a certain value -- will trigger when 'hitPoints' <= zero (your player is dead!) 
zfModel.onTarget('change:hitPoints', onHitpointsAtZero, , 0, TargetModelEventObject.LTE, 'targetModelHP_atZero');
 
// listen for player's XP to hit that next level value
zfModel.onTarget('change:playerXPPoints', onPlayerLeveledUp, nextXPLevelValue, TargetModelEventObject.GTE, 'xpPointsTilNextLevel');
 
// remove those event callbacks/listeners
zfModel.offTarget('change:hitPoints', onHitpointsAtZero, , 0, TargetModelEventObject.LTE, 'targetModelHP_atZero');
zfModel.offTarget('change:playerXPPoints', onPlayerLeveledUp, nextXPLevelValue, TargetModelEventObject.GTE, 'xpPointsTilNextLevel');

I found using this Target value type listener in very specific cases boosted my FPS by a good amount when things get crazy on the battle field. If you think about a “swarm” wave of badguys being shot to bits, that mana value (or gold or player XP) gets changed a LOT and any way you can delay those logic loops, or minimize how many times you have to check values, the better!

Project Files can be found in ZIP format here.

Share

AS3 BackboneJS-style Model Experiment Pt. 1

Most of my day-to-day work is done using BackboneJS and other JavaScript libs. As a basic experiment, I thought I’d re-create the BackboneJS-style Model in AS3. Why, you might ask? Sure, in AS3, in a lot of cases, it’s going to be infinitely more efficient to have a single public property in class that you change vs. having an actual setter/getter. But hear my case out, and let me know what you think.

To start, why a model? With any MVC framework you’ve got a “model” that… models… a set of data for a particular class; a PlayerModel, EnemyModel, etc. EnemyModel may have properties like “currentHP” and “maxHP” to model its current & max hitpoint values. On the PlayerModel, say, you may have a “currentMana” property that holds the current state of how much mana your player currently has. One of the benefits to any model holding your state, is that when you change the value of a property on the model, you can dispatch a Change event.

Quick example using (you guessed it…) a TowerDefense game as the context to our tutorial!

package 
{
   public class PlayerModel {
      public var mana:Number;
 
      public function PlayerModel(startMana:Number) {
         mana = startMana;
      }
   }
}

Using this basic Class to hold your mana property, when a timer ticks by saying that the Player has now regenerated X amount of mana, you may have something like:

var playerModel:PlayerModel = new PlayerModel(5);
 
// ... other stuff, then in your 'Tick' function
playerModel.mana = newManaValue;

Cool! Super Easy! You’ve got the prototype down, your game is coming along, and now you start adding your UI elements. You’ve got your ArrowTower icon on the screen and it takes 50 mana to build. How do you know when to activate that icon so the player knows he/she can click on it and build a new ArrowTower? Well, using the above system, you would have to check every frame of your game…

if(playerModel.mana > 50) {
   activateIcon();
}

Now you add 5 more Tower Icons to the mix, and you’ve got six UI elements querying that PlayerModel every single ENTER_FRAME event to see if they need to turn on or turn off. It’s not that taxing to check that value, but now we’re doing it 6 times, 60 times a second, when we could be using that same CPU power to handle other game logic… We’re now talking uselessly expensive CPU cycles.

With responsive game/UI development, there’s a much better way! Enter the model, triggering change events, and now our UI elements only get notified when “mana” changes.

Here’s a really dumbed-down version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package 
{
   import flash.events.EventDispatcher;
   import flash.utils.Dictionary;
 
   public class PlayerModel extends EventDispatcher {
      private var _model:Dictionary;
 
      public function PlayerModel() {
         _model = new Dictionary(true);
      }
 
      public function set(key:String, value:Number) {
         _model[key] = value;
         dispatchEvent(Event.CHANGE);
      }
   }
}

So now all we have to do is:

var playerModel:PlayerModel = new PlayerModel();
playerModel.addEventListener(Event.CHANGE, onChange);
 
// eventually we set the mana amount in the manaRegenerate function
playerModel.set('mana', newManaValue);

Now all 6 of those Tower Icons will get notified that some value on the PlayerModel has changed. Sweet! We’re efficient! We’re done!

Nope. We’re slightly more efficient. What if you also have properties like “currentHP”, “currentGold”, “x”, “y”, etc on that PlayerModel? Now all 6 of those Tower Icons are having their onChange functions fire off any time any property on that PlayerModel gets changed. The player moves to the left 5 pixels and your Tower Icons have to go check playerModel.mana to see if the mana changed… nope, the x value did but not mana. Still a lot of wasted cycles.

BackboneJS Models (and I’m sure many other frameworks) have a way to listen for changes to specific properties on the model, so you only get notified when the property you want to listen for changes. The syntax would look like the following:

playerModel.on('change:mana', onManaChanged);

This would really help those Tower Icons from wasting time wouldn’t it?

See the demo below and click the “Start/Restart” label in the top right to kick it off.

Get Adobe Flash player

What the demo doesn’t show is various other properties being set on that model, which would’ve triggered “change” events. This example is just showing what happens when “mana” changes. So now your Tower Icons know when JUST the ‘mana’ property changes.

Let’s look at the code behind my ZFModel, looking at the ModelEvent and ModelEventObject classes first.

ModelEvent

The ModelEvent Class is what gets sent to callback function or event listener handler functions. I store the Object version of the model from which the event gets dispatched from, the event “key” or name (“type” in other Flash Events) of the event that caused this ModelEvent to be triggered, and the changed value of the property on the model. This is the what you would expect when receiving a basic Event in Flash without the bubbling or capture since this is a Model event not an event from the display list.

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
package com.zf.events
{
   public class ModelEvent
   {
      // the model Object the event happened on
      public var model:Object;
 
      // the field name change that triggered the event
      public var key:String;
 
      // the new value of the field that changed
      public var value:*;
 
      /**
       * Constructor for the ModelEvent
       * 
       * @param {Object} eventModel The model the event is being triggered from
       * @param {String} eventKey The field that changed to trigger the event
       * @param {*} newVal The new value the field changed to
       */
      public function ModelEvent(mdl:Object, eventKey:String, newVal:*) {
         model = mdl;
         key = eventKey;
         value = newVal;
      }
   }
}

ModelEventObject

The ModelEventObject Class is used by ZFModel to store some additional data for itself about the nature of the event you’re listening for. It contains the callback function to use when the actual event happens.

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
package com.zf.events
{
   public class ModelEventObject
   {
      // the full event name being passed in,
      // may be: 'change' or 'change:myField'
      public var eventName:String;
 
      // the type of event: 'change', if the event was 'change:myField',
      // eventType has the 'change' part
      public var eventType:String;
 
      // the field name for the event, if the event was 'change:myField',
      // eventField has the 'myField' part
      public var eventField:String;
 
      // the callback function for when the model event occurs
      public var callback:Function;
 
      // if this ModelEventObject is listening for a specific field or not: 
      // true if 'change:myField', false if just 'change'
      public var hasFieldEvent:Boolean;
 
      /**
       * Constructor for ModelEventObject
       * 
       * @param {String} event The full event name to listen for
       * @param {Function} cb The Function to call back when event is triggered
       */
      public function ModelEventObject(event:String, cb:Function) {
         eventName = event;
         callback = cb;
 
         eventField = '';
         hasFieldEvent = false;
 
         // check to see if we're listening for a specific field
         if(event.indexOf(':') != -1) {
            // split the event by ':' delimeter
            var tmp:Array = eventName.split(':');
 
            eventType = tmp[0];
            eventField = tmp[1];
 
            hasFieldEvent = true;
         } else {
            // else we are not listening for a specific event
            eventType = eventName;
         }
      }
 
      /**
       * Converts the ModelEventObject into a ModelEvent for callbacks
       * 
       * @param {Object} model The model that changed
       * @param {*} newValue The new value that changed
       * @return {TargetModelEvent} A new EventModel instance
       */
      public function toCallbackParams(model:Object, newValue:*):ModelEvent {
         var evtName:String;
         if(hasFieldEvent) {
            evtName = eventField;
         } else {
            evtName = eventType;
         }
         return new ModelEvent(model, evtName, newValue);
      }
 
      public function toString():String {
         var str:String = '[ModelEventObject: '
            + 'eventName: ' + eventName + ', '
            + 'eventType: ' + eventType + ', '
            + 'eventField: ' + eventField + ' ]';
         return str;
      }
   }
}

ZFModel

Here’s where the magic happens…

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
package com.zf.models
{
   import com.zf.events.ModelEventObject;
   import com.zf.utils.Utils;
 
   import flash.utils.Dictionary;
 
   public class ZFModel
   {
      private var _attributes:Object;
      private var _events:Dictionary;
 
      public function ZFModel(options:Object = null) {
         // setup events Dictionary
         _events = new Dictionary(true);
         clean(true);
 
         // if options were passed in, set them on the model
         if(options) {
            set(options);
         }
      }
 
      /**
       * Removes and resets all event listeners on the model
       * 
       * @param {Boolean} [destroyAttribs=false] If true, will destroy all attributes on the model
       */
      public function clean(destroyAttribs:Boolean = false):void {
         if(destroyAttribs) {
            // Create a new object, destroying anything that previously existed
            _attributes = {};
         }			
 
         // any change event on the model
         _events['change'] = [];
         // a specific field thats changed on the model
         _events['changeField'] = new Dictionary(true);
      }
 
      /**
       * Adds an event callback to the model's events
       * 
       * @param {String} eventName The full name of the event you want to remove
       * @param {Function} callback The function to call back when the event is triggered
       */
      public function on(eventName:String, callback:Function):void {
         // check for multiple events in the eventName
         if(eventName.indexOf(' ') != -1) {
            var events:Array = eventName.split(' ');
            for(var i:int = 0; i < events.length; i++) {
               _addListener(events[i], callback);
            }
         } else {
            _addListener(eventName, callback);
         }
      }
 
      /**
       * Removes an event callback from the model's events
       * 
       * @param {String} eventName The full name of the event you want to remove
       * @param {Function} callback The function to call back when the event is triggered
       */
      public function off(eventName:String, callback:Function):void {
         // check for multiple events in the eventName
         if(eventName.indexOf(' ') != -1) {
            var events:Array = eventName.split(' ');
            for(var i:int = 0; i < events.length; i++) {
               _removeListener(events[i], callback);
            }
         } else {
            _removeListener(eventName, callback);
         }
      }
 
      /**
       * Sets a single property or an object with properties on the model
       * 
       * @param {*} key The key name of the value to set on the model
       * @param {*} [val=null] The value to set on the model
       * @param {Boolean} [silent=false] If silent is true, will not dispatch a change event
       */
      public function set(key:*, val:* = null, silent:Boolean = false):void {
         // if key comes in as an object
         if(typeof(key) == 'object') {
            for (var prop:* in key) { 
               _setValue(prop, key[prop]);
            } 
         } else if(typeof(key) == 'string') {
            _setValue(key, val);
         }
      }
 
      /**
       * Gets an attribute from the model
       * 
       * @param {String} key The key name to get from the model
       * @return {*} Returns the value from the model
       */
      public function get(key:String):* {
         return _attributes[key];
      }
 
      /**
       * Checks to see if the key is on this ZFModel, or on _attributes if IS_EXPANDABLE
       * 
       * @private
       * @param key the key name of the value to set on the model
       * @param val the value to set on the model
       */
      private function _setValue(key:String, value:*, silent:Boolean = false):void {
         if(_attributes[key] && _attributes[key] == value) {
            // the values didnt change so don't dispatch change event
            return;
         }
         // set the new value
         _attributes[key] = value;
 
         if(!silent) {
            _dispatchEvent('change', _attributes, key, value);
         }
      }
 
      /**
       * Iterates through all events and triggers callbacks for appropriate events
       * 
       * @param {String} eventType The type of event being dispatched: 'change' for now 
       * @param {Object} model The model the event is being triggered from
       * @param {String} key The field name that is changing on the model
       * @param {*} value The new value the field changed to
       */
      private function _dispatchEvent(eventType:String, model:Object, key:String, value:*):void {
         var i:int;
         // dispatch all generic change events
         if(_events['change'].length) {
            var changeMEO:ModelEventObject;
            for(i = 0; i < _events['change'].length; i++) {
               changeMEO = _events['change'][i];
               changeMEO.callback(changeMEO.toCallbackParams(model, value));
            }
         }
 
         // dispatch all field change events
         if(_events['changeField'][key]) {
            var changeFieldMEO:ModelEventObject;
            for(i = 0; i < _events['changeField'][key].length; i++) {
               changeFieldMEO = _events['changeField'][key][i];
               changeFieldMEO.callback(changeFieldMEO.toCallbackParams(model, value));
            }
         }
      }
 
      /**
       * Adds an event listener to the model
       * 
       * @param {String} eventName The full event name to add
       * @param {Function} callback The callback function to call when event is triggered
       */
      private function _addListener(eventName:String, callback:Function):void {
         var eventObj:ModelEventObject = new ModelEventObject(eventName, callback);
         if(eventObj.hasFieldEvent) {
            var field:String = eventObj.eventField;
            // check if field array already exists
            if(!_events['changeField'][field]) {
               _events['changeField'][field] = [];
            }
 
            // add the callback for when the field changes
            _events['changeField'][field].push(eventObj);
         } else {
            _events['change'].push(eventObj);
         }
      }
 
      /**
       * Removes an event listener from the model
       * 
       * @param {String} eventName The full event name to remove
       * @param {Function} callback The callback function to match against
       */
      private function _removeListener(eventName:String, callback:Function):void {
         var eventObj:ModelEventObject = new ModelEventObject(eventName, callback);
         if(eventObj.hasFieldEvent) {
            var field:String = eventObj.eventField;
            // check if field array already exists
            if(_events['changeField'][field]) {
               _removeListenerByCallback(_events['changeField'][field], callback);
            }
 
            if(_events['changeField'][field].length == 0) {
               delete _events['changeField'][field];
            }
         } else {
            _removeListenerByCallback(_events['change'], callback);
         }
      }
 
      /**
       * Removes a model's event listener by callback
       * 
       * @param {Array} events The events array to iterate through
       * @param {Function} callback The callback function to match against
       */
      private function _removeListenerByCallback(events:Array, callback:Function):void {
         for(var i:int = 0; i < events.length; i++) {
            if(events[i].callback == callback) {
               // remove all listeners for this field
               // using this callback
               events.splice(i, 1);
            }
         }
      }
 
      public function toString():String {
         var str:String = '- - - Attributes:\n';
         str += attributesToString();
         str += '\n- - - Events:\n';
         str += eventsToString();
         return str;
      }
 
      public function eventsToString():String {
         return Utils.dictToString(_events);
      }
 
      public function attributesToString():String {
         return Utils.objToString(_attributes);
      }
   }
}

ZFModel Usage

With the ZFModel you can now do things like the following:

// load an empty model
var zfModel:ZFModel = new ZFModel();
 
// load a model with default params
var zfModel:ZFModel = new ZFModel({
   mana: 50,
   x: 10,
   title: "My Model"
});
 
// listen for all model changes
zfModel.on('change', onModelChange);
 
// listen for a specific field to change
zfModel.on('change:mana', onManaChanged);
 
// remove the previous listeners/callbacks
zfModel.off('change', onModelChange);
zfModel.off('change:mana', onManaChanged);
 
// set a single value on the model
zfModel.set('mana', 55);
 
// set the model using an object
zfModel.set({
   newProperty: 'hello',
   mana: 2
});
 
// set a single value SILENTLY (does not trigger a change event)
zfModel.set('mana', 10, true);
 
// set several values SILENTLY (does not trigger a change event)
zfModel.set({
   newProperty: 'hello',
   mana: 2
}, null, true);
 
// get a value from the model
zfModel.get('mana');

Those familiar with BackboneJS Models will see a number of features lacking like previousAttributes. There are definitely a number of improvements I (or you) could to this… Event Pooling, validation, etc, it’s not a 100% exact port, but it works wonderfully for my purposes.

Project Files can be found in ZIP format here.

Stay tuned for PART 2 that I created on top of this… This is where it really gets fun… “TargetModelEvents”

Share

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

Demo Updates – BitmapFont Working!

I’ve updated my AS3 Starling TD Demo today. Thankfully, a very courteous person emailed me today letting me know that my Bitmap Fonts were not showing up if you didn’t actually have the fonts installed on your machine. Whoops!

Now that the BitmapFonts are actually working using the proper output from Glyph Designer, I think I need a few more tutorials on how to use that program. Let’s just chalk it up to my complete inexperience with Glyph Designer, but, I wanted to use the same font and have it be different colors in different TextFields. And the problem with this is that it seems if your font png that you export from Glyph Designer has a color (solid/gradient) that if you try to tell the TextField.color that you want this TextField to be white (0xFFFFFF) it tried to blend the color of the BitmapFont with white… never really giving me an actual crisp white that I wanted. Also, in some other case when I tried to assign a color to the TextField it seemed to think I was saying “I want no fill color at all” and you could see through the letters. I’m almost positive this is all my own fault.

I updated some of my posts to include the minor code changes I made to a few files. They’re mostly very small x/y/fontSize kinds of changes.

So what was the culprit? When I exported my fonts from Glyph Designer, I chose “.fnt (Plain Text)”. Well, I didn’t “choose” it, it’s the default setting. My eyes read “FNT” and my brain said “Yup! That’s the file type we want! Export it!” and really if I would’ve clicked the dropdown, there’s a “.fnt (XML)” that I should’ve changed it to. So now the *.fnt files are all actually XML. I’m really curious why Flash was totally fine with me taking straight plain text and typecasting it to XML without complaining. There was not even any semblance of or any structure whatsoever. I guess maybe in typecasting it to XML, it just returned null or an empty XML Object or something. Ooooh well… it’s all fixed now.

Remember, if you’re exporting from Glyph Designer for use in Starling’s BitmapFont, export from Glyph Designer using the “.fnt (XML)” Export Type option!

Thanks again Christophe for letting me know!
-Travis

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]