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”

FacebookTwitterGoogle+Share

Leave a Comment

Your email address will not be published.

1 Trackback

  1. AS3 BackboneJS-style Model Experiment Pt. 2 – Target Value Model Events | Zombie Flambe (Pingback)