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 Experiments – MVC in AS3 Tutorial

I’ve been using Backbone.js and jQuery quite extensively for the past year or so and I’ve only recently gotten back into AS3 dev. And I’ve never actually ever used any MVC framework in AS3/Flex, so I wanted to take a quick break away from my AS3 game engine series to have fun and mess around with MVC in AS3. As I didn’t want to stray too far from my game engine, I decided I wanted to whip up some base Model/View/Controller classes, and then extend those to an EnemyModel/EnemyView/EnemyController class.

The reason this is in “AS3 Experiments” is because this isn’t just straightforward MVC. I’ve added an expandable Model. Imagine you’re loading in JSON from a file or endpoint and you pass the data straight in to the Model’s constructor or some “setPropertiesFromJSON()” type function. I wanted to play around with the idea of, “what happens if that JSON file has properties inside that you don’t have hardcoded class variables for?” In JavaScript, this wouldn’t even be a conversation. But in AS3, while you do have Dynamic classes, I didn’t want to go that route. So, as we’ll see, if the model doesn’t find the specific class property you’re trying to set, it saves the data anyways in an _attributes Object so you don’t lose it. More on that later.

So, I’ve been reading through Rex van der Spuy’s book, AdvancED Game Design with Flash and right from the start, the book just jumps right into MVC… fantastic! I’ve found it’s tough to find “advanced” books and especially “advanced” tutorials online, so this book was very refreshing. There are no sections titled “Data Types in AS3”, or “What is a Function?” I’m really enjoying this book.

In the online tutorial front, I came across a couple good sites worth mentioning on MVC in AS3 whilst googling, so I’ll list those here as well.

  • Advance AS3 Game Dev 02: Rectangles and MVC – this is video #2 in a series on advanced game dev. Don’t waste your time looking for Video 1… I spent probably 10 minutes googling before reading the youtube comments (because who reads those?) but most of them were people complaining about video 1 not being public. oh well… I’ll take what I can get. Pretty good series.
  • The Model-View-Controller Design Pattern in Actionscript 3 on the Swinburne University of Tech site had a great tutorial that was quite helpful as well.

Ok, enough love, let’s code.

Read More

Share