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.

FacebookTwitterGoogle+Share

Leave a Comment

Your email address will not be published.