2D Game Engine Development in HTML5
Textbook readings
- Chapter 5 - Learning Some Helpful Libraries (JQuery, Underscore)
- Chapter 9 - Engine framework, Inheritance, Events, Components
Base Engine Framework - Code
Framework for making an Engine object. Check out "JavaScript Patterns" Chapter 5 and 6
Defines public, private, and static functions for an Engine 'class' that can be created.
Support for multiple instances of an Engine
- CODE - Basic Engine object
//========================================================================= var Engine = Engine || {} Engine = (function(configOptions) { //========================================================================= // static data - var _currentInstance = null; var _instanceIdentifier = 0; //========================================================================= // static function - GetCurrentInstance = function() { return _currentInstance; } //========================================================================= // static function - // turns a string of passed-in, comma-separated names and turns them // into an array of names with any whitespaces stripped out _normalizeArg = function(arg) { if(_.isString(arg)) { arg = arg.replace(/\s+/g,'').split(","); } if(!_.isArray(arg)) { arg = [ arg ]; } return arg; }; //========================================================================= // helper function - // Shortcut to extend Engine with new functionality // binding the methods to engine extend = function(obj) { _(this).extend(obj); return this; }; //========================================================================= // Engine constructor function var engineDefinition = function (configOptions) { if( !(this instanceof Engine) ) { return new Engine(); } // Some base options that can be extended with additional // passed in options through parameter configOptions this.options = { imagePath: "images/", audioPath: "audio/", dataPath: "data/", audioSupported: [ 'mp3','ogg' ], sound: true }; if(configOptions) { _(this.options).extend(configOptions); } // Member data this.components = {}; this.inputs = {}; this.joypad = {}; // Support for multiple inheritence _instanceIdentifier += 1; this._instanceID = _instanceIdentifier; _currentInstance = this; } //========================================================================= // Engine prototype engineDefinition.prototype = { constructor: Engine, version: "1.0", // public member functions extend: extend, }; //========================================================================= // public static functions engineDefinition.GetCurrentInstance = GetCurrentInstance; engineDefinition._normalizeArg = _normalizeArg; return engineDefinition; }());
Main Game Loop has the responsibility of updating objects/systems and rendering
- CODE - RequestAnimationFrame Polyfill
//========================================================================= // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating // requestAnimationFrame polyfill by Erik Möller // fixes from Paul Irish and Tino Zijdel (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }());
- CODE - Game Loop
// Pulled from Engine.js //========================================================================= // Takes a callback that expects a parameter telling it the elapsed // time since the previous frame Engine.prototype.setGameLoop = function(callback) { this.lastGameLoopFrame = new Date().getTime(); // funky - Use a local variable to bind original context // because it gets lost in the wrapper var that = this; that.gameLoopCallbackWrapper = function(now) { _currentInstance = that; that.loop = requestAnimationFrame(that.gameLoopCallbackWrapper); var dt = now - that.lastGameLoopFrame; if(dt > 100) { dt = 100; } callback.apply(that,[dt / 1000]); that.lastGameLoopFrame = now; _currentInstance = null; }; requestAnimationFrame(this.gameLoopCallbackWrapper); }; //========================================================================= // stop game timer Engine.prototype.pauseGame = function() { if(this.loop) { cancelAnimationFrame(this.loop); } this.loop = null; }; //========================================================================= // resume game timer Engine.prototype.unpauseGame = function() { if(!this.loop) { this.lastGameLoopFrame = new Date().getTime(); this.loop = requestAnimationFrame(this.gameLoopCallbackWrapper); } };
Game Object System - Code
Adding Classical Inheritence to JavaScript
Allow for new classes to be extended from existing ones using the extend method
Inherited objects can share the same instance methods as the parent objects and call parent methods using this._super() from the child method
The constructor automatically calls the init() method of the object
/* Simple JavaScript Inheritance * By John Resig http://ejohn.org/ * MIT Licensed. */ // Inspired by base2 and Prototype (function(){ var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; // The base Class implementation (does nothing) this.Class = function(){}; // Create a new Class that inherits from this class Class.extend = function(prop) { 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 prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[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]) : prop[name]; } // The dummy class constructor function Class() { // All construction is actually done in the init method if ( !initializing && this.init ) this.init.apply(this, arguments); } // Populate our constructed prototype object Class.prototype = prototype; // Enforce the constructor to be what we expect Class.prototype.constructor = Class; // And make this class extendable Class.extend = arguments.callee; return Class; }; })(); if(!(typeof exports === 'undefined')) { exports.Class = Class; }
CODE - Examples of Class Functionality
var Person = Class.extend( { init: function() { console.log('Created Person'); }, speak: function() { console.log('Person Speaking'); } }); var p = new Person(); p.speak(); var Guy = Person.extend({ init: function() { this._super(); console.log( 'Create Guy'); }, speak: function() { this.super(); console.log( "I'm a Guy!" ); } }); var bob = new Guy(); bob.speak(); var Girl = Person.extend( { init: function() { console.log( "Created Girl" ); }, speak: function() { console.log( "I'm a Girl!" ); } }); var jill = new Girl(); jill.speak();
JSFiddle - Test examples of Class Functionality
Event API & Event Class
Bind, Trigger, and unbind to events
Engine.Evented = Class.extend({ //========================================================================= // 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). bind: 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 = this.listeners || {}; this.listeners[event] = this.listeners[event] || []; this.listeners[event].push([ target || this, callback]); // Store the event info on the target, so we can unbind it later if(target) { if(!target.binds) { target.binds = []; } target.binds.push([this,event,callback]); } }, //========================================================================= // 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. trigger: function(event,data) { if(this.listeners && this.listeners[event]) { for(var i=0,len = this.listeners[event].length;i (less than) 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] listener[1].call(listener[0],data); } } }, //========================================================================= // Allow for all callbacks to be unbounded for a specific event unbindAll: function(event) { if(this.listeners[event]) { delete this.listeners[event]; } }, //========================================================================= // 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). unbind: function(event,target,callback) { var l = this.listeners && this.listeners[event]; if(l) { // 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); } } } } }, //========================================================================= // If the object has any event info stored on it // then we want to remove all of them. debind: function() { if(this.binds) { for(var i=0,len=this.binds.length;i < len;i++) { var boundEvent = this.binds[i], source = boundEvent[0], event = boundEvent[1]; source.unbind(event,this); } } } });
Component API
Derives from Evented, so any type of component can register for and recieve events
//========================================================================= // Pulled from Engine.js Engine.prototype.registerComponent = function(name,methods) { //@TODO - verify that we don't already have a component registered by the given name methods.name = name; // Create a new component with the methods passed in this.components[name] = Engine.Component.extend(methods); }; // Component.js // Base Component class // Handles being added and removed to an entity. // Extends the Evented class, so that it can be bind and be bounded to. Engine.Component = Engine.Evented.extend({ //========================================================================= init: function(entity) { // set a property so that the component can refer back to the entity this.entity = entity; // extend the entity with new properties from the component if(this.extend) { _.extend(entity,this.extend); } // add the component to the entity as a property under its name entity[this.name] = this; // add the component to the entity's list of active components entity.activeComponents.push(this.name); // if there's any post-initialization requirements (like binding listeners) // then we do that in the .added() function if(this.added) { this.added(); } }, //========================================================================= destroy: function() { // remove properties that match the keys of the extend object in the entity if(this.extend) { var extensions = _.keys(this.extend); for(var i=0,len=extensions.length;i < len;i++) { delete this.entity[extensions[i]]; } } // destroy the property from the entity delete this.entity[this.name]; // remove the component from the entity's list of active components var idx = this.entity.activeComponents.indexOf(this.name); if(idx != -1) { this.entity.activeComponents.splice(idx,1); } // remove any event handlers this component has bound this.debind(); // if there's any post-destruction requirements // then do that in the .destroyed() function if(this.destroyed) { this.destroyed(); } } });
Game Objects / Entities
Entity also derives from Evented, so it can register for and receive events.
Can be assigned any component by name. It is
Engine.Entity = Engine.Evented.extend({ //========================================================================= // Checks if this entity has a component type hasComponent: function(componentName) { return this[componentName] ? true : false; }, //========================================================================= // Add any number of components by name to this entity addComponent: function(componentNameList) { componentNameList = Engine._normalizeArg(componentNameList); if(!this.activeComponents) { this.activeComponents = []; } // loops over all the components to be added, looks them up in the // engine's component list, and creates the new component object for(var i=0,len=componentNameList.length;i < len;i++) { var name = componentNameList[i], compClass = Engine.GetCurrentInstance().components[name]; if(!this.hasComponent(name) && compClass) { var c = new compClass(this); // trigger an event signaling a new component was added this.trigger('addComponent',c); } } return this; }, //========================================================================= // Remove any number of components by name deleteComponent: function(componentNameList) { componentNameList = Engine._normalizeArg(componentNameList); // loops over all the components to be removed, looks them // up to see if this entity has it, and destroys it if so for(var i=0,len=componentNameList.length;i < len;i++) { var name = componentNameList[i]; if(name && this.hasComponent(name)) { // trigger an event signaling a component was destroyed // this.trigger('delComponent',this[name]); this[name].destroy(); } } return this; }, //========================================================================= destroyEntity: function() { // prevent duplicate destruction (if that's possible) if(this.destroyed) { return; } // Debind to remove any event handlers associated with this object this.debind(); // If this object has a parent and can be removed, then do it if(this.parent && this.parent.remove) { this.parent.remove(this); } // trigger an event on this object signaling that it has been destroyed this.trigger('removed'); // mark this object as destroyed this.destroyed = true; } });