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.

I’m going to run through the code mostly. I think this post is more implementation and less theory. I have my base ZFModel/ZFController/ZFView classes in my src/zf/mvc/ folder, so those base classes are there and then groups of related class files are under src/zf/classes/. In this tutorial’s example, my enemy MVC classes are in src/zf/classes/enemy/.

I had originally started with a structure more like src/zf/models, src/zf/controllers, and src/zf/views because I wanted all my models to be able to take advantage of the “internal” access modifier, you know, the whole “other classes inside my package can take advantage of my variables” one? But that didn’t work because as you’ll see, my parent class is trying to set values on my child class, and Flash didn’t much care for that. However, if I made the child fields public, the parent could see them just fine. Oh well, so since I couldn’t take advantage of “internal” I just made any var on the child class public that I would need in the parent class. I’ll show you what I mean in a minute.

ZFModel

Model is the honey badger of the group. Model could not possibly care less who Controller is or what a View does.

package com.zf.mvc
{
    import flash.events.EventDispatcher;
    import flash.events.Event;
    
    public class ZFModel extends EventDispatcher
    {
        private var _attributes:Object = new Object();
        
        // IF IS_EXPANDABLE is true, any properties not currently defined on the ZFModel will be stored in _attributes
        // IF IS_EXPANDABLE is false, any properties not currently defined on the ZFModel will throw errors
        private const IS_EXPANDABLE:Boolean = true;
        
        private const THROW_ERROR_ON_EXPAND:Boolean = false;
        private const THROW_ERROR_ON_INVALID_GET:Boolean = false;
        
        public function ZFModel()
        {
        }
        
        public function update():void {
        }
        
        /**
         * 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 the value to set on the model
         */
        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 key the key name to get from the model
         */
        public function get(key:String):* 
        {
            var retVal:*;
            
            // if the key exists on the ZFModel
            if(this.hasOwnProperty(key))
            {
                retVal = _getValOnModel(this, key);
            }
            else 
            {
                // key doesnt exist on model, if IS_EXPANDABLE, check _attributes
                if(IS_EXPANDABLE && _attributes.hasOwnProperty(key)) 
                {
                    retVal = _getValOnModel(_attributes, key);
                } 
                else 
                {
                    // Setup error message
                    var msg:String = "Cannot get property " + key + " from this ZFModel -- " + key + "doesn't exist on the ZFModel";
                    if(IS_EXPANDABLE) {
                        msg += " and was never set."
                    } 
                    
                    if(THROW_ERROR_ON_INVALID_GET) 
                    {
                        throw new Error(msg);
                    }
                    else
                    {
                        trace(msg);
                    }
                }
            }
            
            return retVal;
        }
        
        /**
         * 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 
        {
            // check to see if the property exists first
            if(this.hasOwnProperty(key)) 
            {
                // check to make sure this property is not already set to this value
                if(this[key] != value) 
                {
                    // set the value using "this" as the model 
                    _setValOnModel(this, key, value);
                }
            } 
            else 
            {
                if(IS_EXPANDABLE) 
                {
                    // set the value using "_attributes" as the overflow model
                    _setValOnModel(_attributes, key, value);
                } 
                else 
                {
                    var msg:String = "This ZFModel does not have property: " + key + " -- Please add " + key + " to your class definition.";
                    if(THROW_ERROR_ON_EXPAND) 
                    {
                        throw new Error(msg);
                    }
                    else
                    {
                        trace(msg);
                    }
                }
            }
        }

        /**
         * Sets a value on the model
         * 
         * @private
         * @param model the object used to store the item being set
         * @param key the key name of the value being set
         * @param value the value being set on the model
         */
        private function _setValOnModel(model:Object, key:String, value:*, silent:Boolean = false):void {
            // set the new value
            model[key] = value;
            
            if(!silent) {
                dispatchEvent(new Event(Event.CHANGE));
            }
        }
        
        /**
         * Gets a value on the model
         * 
         * @private
         * @param model the object used to store the item being set
         * @param key the key name of the value being set
         */
        private function _getValOnModel(model:Object, key:String):* {
            return model[key];
        }
    }
}
  • Line 6 – Right up top, ZFModel extends EventDispatcher because the view needs to know when the model changes, so ZFModel has to be kind enough to just sqwuak when something changes… he doesn’t care who is listening.
  • Line 12 – IS_EXPANDABLE… if you want to lock the model down to JUST variables that have been defined on child Models, then set that to false and it’ll throw errors or trace out the errors for you
  • Line 21 – update can be overridden in child Models and should be able to be called from my main game loop. Update the models and the views handle themselves.
  • Line 30 – set() is the way you set data on the model. The key param can either be a String or an Object. If it is an Object, val should be null. The silent param is there if you wanted to set the model and not dispatch a change event for some reason.
  • Line 33 – if the function received an Object for the key param, iterate over key’s properties and call _setValue
  • Line 37 – otherwise, no need to iterate if it’s just a string, go ahead and call _setValue
  • Line 47 – get() is the way you get data from the model
  • Line 52 – if the child Model has the property passed, then call _getValOnModel with the model being the child
  • Line 59 – if the child Model did not have the property as a class variable, and we have IS_EXPANDABLE set to true, check and see if we’ve got the property in our overflow “_attributes” variable. Otherwise, the rest of the code just handles “do you want me to throw an error or trace out a message if the property doesn’t exist anywhere”
  • Line 92 – _setValue() has much the same structure as we just saw in get(), “Check the child model’s class variables for the key, if not, check _attributes, if not then trace/throw error a message”
  • Line 134 – _setValOnModel() is where we actually set the value on the model/Object passed in to set the value on and if silent isn’t true, then we dispatch our change event
  • Line 150 – _getValOnModel() is where we actually get the value from the model/Object passed in

ZFController

package com.zf.mvc
{

    public class ZFController
    {
        protected var _model:ZFModel;

        public function ZFController(model:ZFModel)
        {
            _model = model;
        }
    }
}

… so that’s all I’ve got for the controller. In this tutorial there are no key events to listen for or do anything with, so ZFController is pretty light. It takes a reference to the model as a param. That’s it.

ZFView

package com.zf.mvc
{
    
    import flash.display.Bitmap;
    import flash.display.BitmapData;
    import flash.display.Sprite;
    import flash.events.Event;

    public class ZFView extends Sprite
    {
        protected var _model:ZFModel;
        protected var _controller:ZFController;
        
        public function ZFView(model:ZFModel, controller:ZFController)
        {
            _model = model;
            _controller = controller;
            
            _model.addEventListener(Event.CHANGE, onModelChangeHandler);
            
            addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
        }
        
        protected function onModelChangeHandler(evt:Event):void {
            // model values have changed, call update
            update();    
        }
        
        protected function onAddedToStage(evt:Event):void {
            // check model
            update();
            
            // render view
            render();
            removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
        }
        
        /**
         * Should check if the model hasChanged to know if it needs to render or not
         */
        public function update():void {
        }
        
        /**
         * Handles rendering this view
         */
        protected function render():void {
        }
    }
}
  • Line 9 – my ZFView extends Sprite just for ease of use. I’ve had enough Blitting for this week!
  • Line 14 – pass in the ZFModel and ZFController
  • Line 19 – add a listener on _model so if _model changes, view knows about it and child classes can handle how they want. By default, we’re just calling update() in the onModelChangeHandler() function when the model changes
  • Line 21 – listen for when this view gets added to the stage so it knows when it needs to start doing stuff
  • Line 29 – when ZFView is added to the stage, it will call update() to get any values that it may need from the ZFModel, and then it will call render() to display itself
  • Line 41 – 48 – update() and render() should be handled in subclasses

Alright, so there are the base classes, now let’s look at my Enemy classes to see what I’ve added.

EnemyModel

package com.zf.classes.enemy
{
    import com.zf.mvc.ZFModel;

    public class EnemyModel extends ZFModel
    {
        // setting public so ZFModel can have access to these
        public var armor:int;
        public var speed:int;
        public var x:int;
        public var y:int;
        public var direction:String;
        
        private var _distance:int = 0;
        private const DIST:int = 100;
        private var facing:Array = ['right', 'down', 'left', 'up'];
        private var facingIndex:int = 0;
        public function EnemyModel()
        {
            super();
            updateDirection();
        }
        
        override public function update():void {
            var newX:int = x,
                newY:int = y;
            
            _distance += speed;

            if(_distance >= DIST) {
                _distance = 0;
                updateDirection();
            }
            // Handle how this enemy moves
            // I want this guy to move around in a box pattern on the screen
            switch(direction) {
                case 'up':
                    newY -= speed;
                    break;
                case 'down':
                    newY += speed;
                    break;
                case 'right':
                    newX += speed;
                    break;
                case 'left':
                    newX -= speed;
                    break;
            }
            
            // Set the new x and y values so the view can use them
            set({x: newX, y: newY});
        }
        
        private function updateDirection():void {
            // set our first direction... moving right
            direction = facing[facingIndex];
            
            facingIndex++;
            if(facingIndex == facing.length) {
                facingIndex = 0;
            }
        }
    }
}
  • Line 8 – 17 – declare Enemy’s public class variables. These can/will be changed by it’s parent, ZFModel. And declare some private variables that we just want EnemyModel to handle without ZFModel getting to involved.
  • Line 20 – call super() so ZFModel gets to construct itself
  • Line 24 – EnemyModel has overridden update() so it can handle whatever Enemy needs to do. In this case, we’re going to handle updating our x and y property properly so the view will know where to draw itself
  • Line 55 – updateDirection() simply iterates through an array of directions so we know which value (x or y) to increment or decrement during update()

EnemyController

package com.zf.classes.enemy
{
    import com.zf.mvc.ZFModel;
    import com.zf.mvc.ZFController;
    
    public class EnemyController extends ZFController
    {
        public function EnemyController(model:ZFModel)
        {
            super(model);
        }
    }
}

Again, nothing really here at all.

EnemyView

package com.zf.classes.enemy
{
    import com.zf.mvc.ZFController;
    import com.zf.mvc.ZFModel;
    import com.zf.mvc.ZFView;
    
    public class EnemyView extends ZFView
    {
        public function EnemyView(model:ZFModel, controller:ZFController)
        {
            super(model, controller);
            this.graphics.drawRect(0, 0, 25, 25);
            width = 25;
            height = 25;
        }
        
        /**
         * Should check if the model hasChanged to know if it needs to render or not
         */
        override public function update():void {
            // update the view's x and y from the model
            x = _model.get('x');
            y = _model.get('y');
            // then rerender
            render();
        }
        
        /**
         * Handles rendering this view
         */
        override protected function render():void {
            this.graphics.clear(); 
            this.graphics.beginFill(0x0000FF);
            this.graphics.drawRect(x, y, 25, 25);
            this.graphics.endFill();
        }
    }
}
  • Line 12 – not too sure if that helps or I need that… it’s getting late and I don’t care to check, but the idea was I needed to draw something on “this” so that I could set a width and height. If you have an empty Sprite and even think about setting a width or height, Flash laughs at you.
  • Line 20 – I’ve overridden the update() function here to get the x and y values from our model, then call render to do something with those new values
  • Line 31 – I’ve overridden the render() function here so this EnemyView draws a 25×25 blue rectangle on itself

And last but not least, our Main class…

Main.as

public function Main() {
    enemyModel = new EnemyModel();
    enemyController = new EnemyController(enemyModel);
    enemyView = new EnemyView(enemyModel, enemyController);

    startEnemy();
    //benchmark(m,v,c);
}

private function startEnemy():void {
    var startingSettings:Object = {
        x: 20,
        y: 20,
        speed: 2
    };
    
    // set the model
    enemyModel.set(startingSettings);

    // add the enemyView to stage
    addChild(enemyView);
    
    addEventListener(Event.ENTER_FRAME, onTick);
}

private function onTick(evt:Event):void {
    // update Models
    enemyModel.update();
}

These are the relevant parts of Main.as

  • Line 2 – 4 – Create the EnemyModel, EnemyController, and EnemyView classes passing in the appropriate params, then call startEnemy()
  • Line 10 – startEnemy sets some initial settings for the model then adds the enemyView to stage (kicking off EnemyView’s update/render cycle
  • Line 23 – adds a listener to start our game loop
  • Line 28 – if we set up our MVC properly, I should only have to loop through all of my relevant ZFModel classes and subclasses and call update(), and their corresponding ZFViews should draw themselves accordingly.

That’s about it.

DEMO
VIEW THE DEMO

And Project Files
PROJECT FILES

Hopefully that was helpful! Thanks!

Categories:

0 Comments

Leave a Reply

Avatar placeholder