Overview
Expect this Unit to take about 2-5 hours, depending on your comfortability with Object-Orientated Programming and with JavaScript. Further reading might be required to help solidify the concepts. To demonstrate what you should have at the end of this Unit, check out the Demo for Unit 4. Don't forget to use the Chrome DevTools to look at the console and check out the source code for my tests!
This Unit might be one of the heaviest in terms of architecturing our game engine. The decisions we make for our game object system shape how we'll write game systems and gameplay code. We have lots of choices, but I'm here to tell you that there is generally no right way, just a best way given our circumstances. What I chose to do here works for our project and helps teach common concepts used in many other games.
JavaScript's prototypical inheritance model enables us to use classical inheritance through some work. Why am I choosing to use classical inheritance? It's a lot easier to get a few key features through classical inheritance: inheriting constructors, calling a parent's function from a derived function, and I want it to be super easy to create derived object types. Following a classical approach isn't very natural in JavaScript, but it's a valuable pattern to be comfortable with.
We are going to talk about 3 files here:
- class.js - sets the foundation for using classical inheritance for out object model.
- baseObject.js - gives us the support for events
- drawNode.js - provides a render heirarchy
class.js
The guy who created JQuery, John Resig, created another JavaScript library called Prototype.js. You can read about this open-source code from his blog: http://ejohn.org/blog/simple-javascript-inheritance/. His blog post does a fairly good job at explaining the code, but to give you a basic rundown the idea is that we want to allow new classes to be extended from existing ones through a methond called extend. Inherited objects can share the same instance methods as the parent objects and call parent methods using this._super() from the child method. There's a good bit of overhead in making this work: when creating the child class each method is checked against the parent to see if it is inherited and if so creates a wrapper function.
This code also supports an init() function to be used a constructor, which is chained through all inherited types.
You can grab the code from John's blog, but there are a few adjustments I made for this project: I also wanted member objects to extend any member object of the same from the parent's. This means a parent class type could have a member object called .defaults with a number of properties, and if a child class has a property of the same name the values from the child's .defaults will be combined with the values from the parent class. In order to help with debugging, I also generate a unique function name for the constuctor, so that when looking at the object in the watch window we get more useful type information. It is an unfortunately side effect of using this system that we have to remember to assign the .typename to a new class type, but the payoff is worth it.
Compare my version with the original, and see if you can better understand the changes!
class.js
Source Code:
//*****************************************************************************
// Class - Simple JavaScript Inheritance
// by John Resig http://ejohn.org/
// Inspired by base2 and Prototype
// MIT Licensed.
//
// Modifications by Thomas O'Connor
//
// Required Libraries:
// - Underscore
//
// Notes:
// - If a child class tries to call _super() and there was no inhereted
// function, then we get a hard error.
//
// Updates:
// - Along with overwriting existing functions, child classes can now extend
// existing objects (such as the .defaults in our sprite class).
// - The updated code regarding the dynamically generated constructor function
// helps with debugging, since the function will now have a name that is
// representative of the derived class type. However, the __proto__ will show
// up in the debugger with the parent's name even though it more closely
// represents the child class, since we copy child class members onto an
// instance of the parent's class each time we create a new child... ah whatever.
//*****************************************************************************
(function(){
var initializing = false,
fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function BaseClass(){};
// Create a new Class that inherits from this class
Class.extend = function(prop) {
// _super variable stores the parent class methods
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
if( typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ) {
prototype[name] = (function(name, fn){
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]);
}
// Tom addition:
// Check if we're overwriting an existing object
else if( typeof prop[name] == "object" && typeof _super[name] == "object" ) {
prototype[name] = _(_super[name]).clone();
_(prototype[name]).extend( prop[name] );
}
else {
prototype[name] = prop[name];
}
}
// Another Tom addition - this is solely to help with debugging:
// If the user supplied a name for the class we use that name for the constructor function
// Generate a dynamic function name that indicates what this is a derived class from
prop.typename = prop.typename || "ChildOf" + this.typename;
// When the constructor function is called, the context ("this") should be the parent class
// Using JS's Function (akin to eval()) the context is lost, unless we use .apply to
// propagate the context correctly.
function customAction() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
// This approach isn't ideal, but the benefit of getting better debug information is worth it.
var func = new Function("action", "return function " + prop.typename + "(){ action.apply(this, arguments) };")(customAction);
// Populate our constructed prototype object
func.prototype = prototype;
func.prototype.constructor = func;
// And make this class extendable
if( func.extend === undefined ) {
func.extend = arguments.callee;
}
func._super = arguments.callee;
return func;
};
})();
Class Test Code
Here is some test code directly from John's blog to demonstrate how class.js should work.
Source Code:
var Person = Class.extend({
init: function(isDancing){
this.dancing = isDancing;
},
dance: function(){
return this.dancing;
}
});
var Ninja = Person.extend({
init: function(){
this._super( false );
},
dance: function(){
// Call the inherited version of dance()
return this._super();
},
swingSword: function(){
return true;
}
});
var p = new Person(true);
p.dance(); // => true
var n = new Ninja();
n.dance(); // => false
n.swingSword(); // => true
// Should all be true
alert( p instanceof Person );
alert( p instanceof Class );
alert( n instanceof Ninja );
alert( n instanceof Person );
alert( n instanceof Class );
Supporting Events through baseObject.js
Let's create our game engine's first class!
The BaseObject class is going to provide some basic behavior that we want all objects in our game engine to have, and that is sending and receiving events.
Events are also known as messages, and they allow us to keep parts of the engine from becoming too tightly coupled. We wants objects to be able to communicate with each other without having to actually know anything specific about each other, or about each other at all! This is especially handy when we get to components
First we should think about our API. We want the following features:
- The ability to receive and react to events. We call this binding, as we will bind a callback function and object instance to a particular event on another or the same object,to be called when the event is triggered.
- The ability to trigger an event. It simply means notifying the object that an event has occured.
- Unbind to a particular event, or to all events.
- Debind any other objects that tried
Let me explain that more clearly: Events are triggered on a specific object. Any other object (including itself) can bind a callback function to an event to be notified when this event occurs for the specific object.
One more thing, we also want to have a constructor and default properties. This class is what other classes will be deriving from, so we might as well start here with those features. Check out the constructor function and see that we are adding a property called .listeners.
We are also doing something that I think is pretty neat with the .defaults property and taking in other properties through the constructor. Using the Underscore library, we can concatenate these objects and assign the properties back onto the object. Everything in .defaults will get assigned on to the object, then anything in the props argument in the init() function will get assigned over it - adding new values or overriding anything from defaults.
A destroy function is meant to be called when the object is no longer needed. It handles cleaning up anything that needs cleaning up. In this case we will want to call .debindEvents() which we will explain soon.
baseObject.js
API Source Code:
//*****************************************************************************
// Base Object
//
// Required Systems:
// * Engine
// * Class
//
// Required libraries:
// * Underscore
//
// Features:
// Initialization function that handles extending the object
// with the properties defined in defaults
//
// Used to make an object support getting notified of events.
// Events are just strings, can be triggered on any object that derives from
// this class.
//
// Triggering an event on an object will dispatch any callbacks registered
// for that event. If there is a target bound to it, then that object will
// have its callback dispatched.
//
//*****************************************************************************
Engine.BaseObject = Class.extend({
// Class name, helpful for debugging
typename: "BaseObject",
//=========================================================================
// Derived class types can extend/overwrite any members in the defaults
defaults: {
engine: undefined
},
//=========================================================================
// Initialize the object so that any properties passed in get assigned onto
// the object.
init: function(props) {
// Assign the defaults as well as given properties to the object
_.extend(this, props);
_.defaults(this, this.defaults );
this.engine = Engine.GetSingleton();
this.listeners = {};
},
//=========================================================================
// On destruction, make sure to debind any objects binded to this one
destroy: function() {
this.debindEvents();
},
//=========================================================================
// Bind a listener to a specific event and trigger the callback on the target.
// Target is an optional argument that provides a context for the callback and
// allows the callback to be removed with a call to debind (to prevent stale
// events from hanging around).
bindEvent: function(event,target,callback) {
// @TODO
},
//=========================================================================
// Checks to see if there are any listeners listening to the specified event
// If so, each of the listeners is looped over and the callback is called
// with the provided context.
triggerEvent: function(event,data) {
// @TODO
},
//=========================================================================
// Allow for all callbacks to be unbounded for a specific event
unbindAllEvents: function(event) {
// @TODO
},
//=========================================================================
// Allow for events to be unbinded when an object is destroyed or no longer
// needs to be triggered on specific events. Callback is optional, if not
// inclued the unbind all events for the given context (target).
unbindEvent: function(event,target,callback) {
// @TODO
},
//=========================================================================
// If the object has any event info stored on it
// then we want to remove all of them.
debindEvents: function() {
// @TODO
}
});
bindEvent()
Bind a target listener to a specific event on this object and trigger the callback on the target. If no target is provided then we assume that it means the target is 'this' object itself. We store listeners and the callback in an array called listeners, keyed by the event name. We also add the flexibility of the user passing in the function by name instead of by value.
Implementation Source Code:
bindEvent: function(event,target,callback) {
// Handle the case where there is no target provided
if(!callback) {
callback = target;
target = null;
}
// Handle case for callback that is a string
if(_.isString(callback)) {
callback = target[callback];
}
// Add a listener to the object, keyed by the name of the vent
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push([ target || this, callback]);
// Store the event info on the target, so we can debind it later if needed
if(target) {
if(!target.binds) { target.binds = []; }
target.binds.push([this,event,callback]);
}
}
triggerEvent()
When an event has occured, we'll call this function which will check to see if there are any listeners wanting to know about this specific event. There's a neat bit of JavaScript here in how we actually call the callback function - functions have a .call method that we can use to pass in the context, which is known as 'this' in the scope of the function.
Implementation Source Code:
triggerEvent: function(event,data) {
if(this.listeners && this.listeners[event]) {
for(var i=0,len = this.listeners[event].length;i<len;i++) {
var listener = this.listeners[event][i];
// listener is an array that contains the callback at [0] and then
// the context (target) at [1]
var result = listener[1].call(listener[0],data);
if( result === true ) {
// remove this item from the list
this.listeners[event].splice(i, 1);
}
}
}
}
unbindEvent() and unbindAllEvents()
To unbind a specific listener from a specific event, we also need to know the callback. Deleting a single element from an array means that we need to walk over the array backwards, and splice the disjointed elements back together.
Implementation Source Code:
unbindEvent: function(event,target,callback) {
var l = this.listeners && this.listeners[event];
if(l) {
// Handle case for callback that is a string
if(_.isString(callback)) {
callback = target[callback];
}
// loop over the array to find any listeners for the given context
// do it backwards because removing an entry changes the length
for(var i = l.length-1;i>=0;i--) {
if(l[i][0] == target) {
if(!callback || callback == l[i][1]) {
this.listeners[event].splice(i,1);
}
}
}
}
}
The second one is super easy. If there are any listeners registered for a specific event, then remove them all. It's as easy as using delete.
Source Code:
unbindAllEvents: function(event) {
if(this.listeners[event]) {
delete this.listeners[event];
}
}
debindEvents()
Say this object has a lot of other objects registered to listen to events on it, and then this object is destroyed. While unbind is used to remove listeners from this object, debind is used to remove all this object as a listener from any other object(s). Otherwise, the other object might still try to trigger a callback on this guy after it's destroyed.
Implementation Source Code:
debindEvents: function() {
if(this.binds) {
for(var i=0,len=this.binds.length;i<len;i++) {
var boundEventInfo = this.binds[i],
source = boundEventInfo[0],
event = boundEventInfo[1];
source.unbindEvent(event,this);
}
}
}
Event Test Code
Let us create a few new object types that derive from BaseObject.
One of them will trigger an event, and the other object will bind itself to that same event so it can respond to it.
Check out the code and see if you can follow along!
Source Code:
// Define new classes
var Person = Engine.BaseObject.extend({
typename: "Person",
startDancing: function() {
console.log( "Triggering the 'dance' event on 'this' Person:", this );
this.triggerEvent("dance");
}
});
var Dog = Engine.BaseObject.extend({
typename: "Dog",
bark: function() {
console.log( "woof! woof!" );
}
});
// Create instances of these new object types
var newPerson = new Person();
var newDog = new Dog();
// Bind the "bark" function to when a "dance" event occurs on the the Person instance
newPerson.bindEvent( "dance", newDog, "bark" );
console.log( "Testing the event system:" );
newPerson.startDancing(); // This will trigger an event, which the dog is binded to
Object Heirarchy through drawNode.js
Now, we create our first derived class type DrawNode.
This object will be responsible for our rendering and updating heirarchy. Why we are doing this will make more sense once we get to game objects and sprites, but it is important to setup now as this is a base feature of our engine.
First let me point out the situation with the default properties and the constructor again. We know this is a derived class type, so we make sure to call this._super(props) passing in the properties so they can be combined with the .defaults. If you remember the feature I added to John's class library, the .defaults defined here were already added/concatenated with the parent's class, so everything will work out as you would expect!
Next let's figure out how we'll approach the API for managing the heirarchy. We'll refer to this internally as node management. We'll want to be able to attach other objects as children, as well as remove objects, so we need functions like: attachChild, removeChild, removeAllChildren. When adding a child node, the list of nodes should remain sorted so that they are stored according to the value of their .zOrder property. Since this is a heirarchy, we'll also want to execute some funtion on this node and then execute the same function on the children nodes. This is called the Visitor Pattern, a common computer science pattern for separating an algorithm from the object structure on which it is operating.
drawNode.js
API Source Code:
//*****************************************************************************
// Draw Nodes
//
// Required Systems:
// * Engine
// * Engine.BaseObject
//
// Required libraries:
//
// Features:
//
//*****************************************************************************
var nodeSortFunc = function(a, b) { return a.zOrder - b.zOrder; };
Engine.DrawNode = Engine.BaseObject.extend({
typename: "DrawNode",
defaults: {
zOrder: 0
},
init: function(props) {
this._super(props);
this.nodes = [];
},
destroy: function() {
this._super();
this.removeAllChildren(true);
},
//=========================================================================
// Node management
attachChild: function( drawNode ) {
// @TODO
},
removeChild: function(drawNode) {
// @TODO
},
removeAllChildren: function() {
// @TODO
},
// Recursively visit all nodes in this tree, calling the passed in function
// name on each node
visit: function(funcName, param) {
// @TODO
},
});
attachChild()
When adding a child node, we'll store it on the .nodes array. We'll also want to resort the array using the nodeSortFunc() we have defined earlier.
Let's also take this oportunity to trigger an event on both this object and the passed in object to notify anyone interested that this attachment as occured.
Implementation Source Code:
attachChild: function( drawNode ) {
this.nodes.push(drawNode);
this.nodes.sort(nodeSortFunc);
// Trigger an event on both this stage object and the passed in entity
this.triggerEvent('nodeAttached', drawNode);
drawNode.triggerEvent('attachedToNode', this);
}
removeChild()
Removing a node from an array is simple, we just need to remember to splice the the disjointed elements back together.
Implementation Source Code:
removeChild: function(drawNode) {
var i, n;
for( i=0, n=this.nodes.length; i<n; i++ ) {
if( this.nodes[i] === drawNode ) {
this.nodes.splice(i,1);
return;
}
}
}
removeAllChildren()
To remove all children nodes, wWalk over the array destroying each one and then clear the array by setting its length to zero.
Implementation Source Code:
removeAllChildren: function() {
var i, n;
for( i=0, n=this.nodes.length; i<n; i++ ) {
this.nodes[i].destroy(true);
}
this.nodes.length = 0;
}
visit()
There are two parameters, the name of the function to call on each node, and the parameter to pass into it. To visit a node is to call this function on it passing in the parameters.
An example of the types of functions we'll be using to visit onto nodes would be a draw() call to render it, or a step() call to update any gameplay logic.
If you look at the heirarchy of nodes as if it were a tree (a common data-structure concept in computer science) then you might see that there's a interesting problem in how we might visit every child node of this node, and then all the childrens' children, etc. We are going to use recursion to solve this problem. Any time we visit a child node, it will recursively visit all of it's children nodes, etc.
- First, let's visit each of the nodes that have a .zOrder value that is less than this node.
- Then, we visit this node. This means calling the specified function if it exists.
- Finally, we visit all of the children nodes that have a great .zOrder value than this node.
Implementation Source Code:
visit: function(funcName, param) {
var node,
position,
i,
n = this.nodes.length;
// draw all nodes with a zOrder less than this
for (i = 0, n < this.nodes.length; i < n; i += 1) {
node = this.nodes[i];
if (node.zOrder < this.zOrder) {
node.visit(funcName, param);
} else {
break;
}
}
// visit this node
if( this[funcName] ) {
this[funcName](param);
}
// draw the rest of the nodes with a zOrder higher than this
for (i; i < n; i += 1) {
node = this.nodes[i];
node.visit(funcName, param);
}
}
Test Code
Here we will create a new object type that might behave as if it were a sprite. We will create our heirarchy by assigning each new instance a unique zOrder value and attaching them to each other as we might want them to be managed.
Implementation Source Code:
// A new derived object with a simple function to demonstrate recursive visiting
var TestSpriteClass = Engine.DrawNode.extend({
typename: "TestSpriteClass",
printName: function() {
console.log( "visiting sprite", this.name, "with zOrder", this.zOrder );
}
});
// Create a bunch of 'sprites', assigning them zOrder values to control their render order
var spriteScene = new TestSpriteClass( {name: "spriteScene" });
var player = new TestSpriteClass( {zOrder: 50, name: "player" });
var foreground = new TestSpriteClass( {zOrder: 1, name: "foreground" });
var background = new TestSpriteClass( {zOrder: 100, name: "background" });
var player_hat = new TestSpriteClass( {zOrder: 51, name: "playerhat" });
// We can attach them in any order, because they will remain sorted
spriteScene.attachChild(foreground);
spriteScene.attachChild(background);
spriteScene.attachChild(player);
player.attachChild(player_hat);
// Can you figure out what will show up in the console log?
console.log( "Testing DrawNodes heriarchy model:" );
spriteScene.visit( "printName" );
Homework
Part 1. Create and add the three files to your project: class.js, baseObject.js, drawNode.js
- Create the files and put them all in your /js_engine sub-directory.
- Add all three files to the list of engine files to load through modernizr in main.js
Part 2. Fill out class.js.
- Make sure to test the class system.
- Somewhere in main.js write a function that contains the test code written in the class lecture. Call this function after the engine files have all finished loading.
Part 3. Fill out baseObject.js
- Add in the source code from the class lecture.
- Fill out the empty functions for bindEvent, triggerEvent, unbindEvent, unbindAllEvents, debindEvents.
- Similar to what you did for class.js, write a test function and call it after the engine is loaded.
Part 4. Fill out drawNode.js
- Add in the source code from the class lecture.
- Fill out the empty functions for attachChild, removeChild, removeAllChildren, visit
- Similar to what you did for class.js, write a test function and call it after the engine is loaded.