Overview
Expect this Unit to take just under 1 hour. To demonstrate what you should have at the end of this Unit, check out the Demo for Unit 3. Don't forget to use the Chrome DevTools to look at the console and check out the source code for how I'm testing the engine.
We're going to create an object called Engine, which will have the functionality of managing a game loop as well as providing a namespace for sub-systems.
This object is a definition, and we will want to create an instance of. There's no big compelling reason to go this route with JavaScript, but it's a good warmup before diving into our Game Object System, which will be inheritence heavy. Plus it's a common approach in native game programming for the game engine to an instantiable object.
engine_core.js
Initial approach
API Source Code:
// The "Engine" variable is in global scope, and will be accessible in any JavaScript file.
// Use an anonymous function that runs immediately to keep things in scope.
var Engine = (function() {
var engineDefinition;
// @TODO - create the base functionality of managing a game loop
// @TODO - assign a bunch of functions to the engineDefinition
return engineDefinition;
}());
requestAnimationFrame() polyfill
Updating our game every frame is something that isn't well handled consistently across all browsers. We'll be using what's called a polyfill - a piece of code that provides the technology that we'd normally expect the browser to provide natively - to help manage that. This requestAniamtionFrame() polyfill lets the browser control our update frequency, which is optimized for performance and synchronized to it's repaint cycle, instead of us setting our own update interval using timers.
I typically put this in a separate file and put any other polyfill or utility code-snippets, which would get included before engine_core.js. For now it's fine just to have it in this file.
Implementation Source Code:
//=========================================================================
// 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);
};
}());
setGameLoop()
This function takes in a function as a parameter which will be called each time the browser fullfills a requestAnimationFrame() request.
Implementation Source Code:
//=========================================================================
// Main Game Loop - update and render
// 1. Step the game logic through a small chunk of time handling
// any user input, motion/physics, collisions and updating game objects.
//
// 2. How we process a rendering step depends on if we are using canvas, svg, css
// For canvas, we usually want to clear the entire canvas and redraw the necessary
// sprites. For css or svg games, provided we updated the properties of the objects
// on the page correctly, the browser takes car of moving and updating the objects.
setGameLoop = function(callback) {
this.lastGameLoopFrame = 0; //new Date().getTime();
// funky - Use a local variable to bind original context
// because it gets lost in the wrapper
var that = this;
this.gameLoopCallbackWrapper = function(now) {
that.loop = requestAnimationFrame(that.gameLoopCallbackWrapper);
if( that.lastGameLoopFrame === 0 )
that.lastGameLoopFrame = 0;
var dt = now - that.lastGameLoopFrame;
if(dt > 100) { dt = 100; }
callback.apply(that,[dt / 1000]);
that.lastGameLoopFrame = now;
};
requestAnimationFrame(this.gameLoopCallbackWrapper);
};
pauseGame()
Cancels any pending animation frame request
Implementation Source Code:
//=========================================================================
// stop game timer
pauseGame = function() {
console.log( "Engine Paused" );
if(this.loop) {
cancelAnimationFrame(this.loop);
}
this.loop = null;
};
unpauseGame()
Re-assigns the gameloop callback function to requestAnimationFrame
Implementation Source Code:
//=========================================================================
// resume game timer
unpauseGame = function() {
console.log( "Engine Unpaused" );
if(!this.loop && this.gameLoopCallbackWrapper) {
this.lastGameLoopFrame = 0; //new Date().getTime();
this.loop = requestAnimationFrame(this.gameLoopCallbackWrapper);
}
};
Creating the engine definition
There's a number of topics to discuss in this next section of code:
- Constructor function
- Singleton pattern
- Function objects
- Default properties
- Static properties/functions
Implementation Source Code:
//=========================================================================
// static data - Singleton Engine instance
//
var _currentInstance = null;
//=========================================================================
// static function - return singleton
//
GetSingleton = function() {
return _currentInstance;
};
//=========================================================================
// Default engine properties
//
var defaults =
{
maxWidth: 1028,
maxHeight: 768
};
//=========================================================================
// Engine constructor
//
var engineDefinition = function (configOptions) {
// Do not allow more than one instance of the engine
if( _currentInstance ) {
engineError( "Attempting to create multiple instances of the Engine" );
return _currentInstance;
}
// Make sure this function was called with new
if( !(this instanceof Engine) ) {
return new Engine(configOptions);
}
console.log( "New Engine instance: ", this);
// Third-party library usage: Underscore
// Check the documentation to see the format of these functions
// Assign default properties to the new instance
_.defaults(this, configOptions, defaults );
// Avoiding a JavaScript bad part:
// Use Underscore library to bind these functions so that the context
// passed in is *always* the engine instance, so that if the user
// registers them as a callback the 'this' pointer is correct
_.bindAll(this,"pauseGame","unpauseGame");
_currentInstance = this;
};
//=========================================================================
// Engine prototype
// engineDefinition is a function, but we can treat it as if it were an
// object. Properties assigned onto the prototype are available to any
// instances of this object as public member functions/properties.
//
engineDefinition.prototype = {
setGameLoop: setGameLoop,
pauseGame: pauseGame,
unpauseGame: unpauseGame
};
//=========================================================================
// public static properties/functions
// These will only be available through the Engine object, which we are
// treating as if it were a namespace
//
engineDefinition.GetSingleton = GetSingleton;
Test Code
Now, here's a small example of how we would create an instance of our Engine:
Source Code:
// Use jQuery to determine the maximum window width and height.
// Normally I'd use a fixed sized, but I wanted to demonstrate some a
// jQuery feature as well as put to use the the optional configuration
// system
var engineConfig = {
maxWidth: $(window).width(),
maxHeight: $(window).height()
};
// Create a new engine instance with our custom configuration parameters!
var myEngine = new Engine( engineConfig );
myEngine.setGameLoop( function(elapsedTime) {
console.error( "frame update, elapsed Time: ", elapsedTime );
})
Homework
Part 1: Adding the file to our project
- Create a subdirectory called js_engine
- Create engine_core.js and put it in this subdirectory.
- Add engine_core.js to our list of engine files to load through modernizr.
Part 2: Writing the engine core
- Add the requestAnimationFrame() polyfill code
- Create the Engine object
- Write a function that returns an object, and assign it to a global variable called "Engine".
- Inside this function, write the three functions we discussed: setGameLoop, pauseGame, unpauseGame.
- Write the constructor function as we discussed.
- Assign this constructor function the three functions through the prototype property.
Part 3: Create an instance of your engine and demonstrate it works
- In main.js, once the engine files have finished loading, create a new instance of the engine.
- Assign the instance a game loop function that prints to the console each frame