Skip to content

Instantly share code, notes, and snippets.

@kirbysayshi
Created March 21, 2011 17:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kirbysayshi/879814 to your computer and use it in GitHub Desktop.
Save kirbysayshi/879814 to your computer and use it in GitHub Desktop.
My attempt at two methods of a constant game-loop in JavaScript
// Example CGLM usage:
ZAP.CGLM
.setMode('custom')
.mode
.setFPS(30)
.setTimeStep(10)
.setSpeedX(1);
function priorityOne(dt, stats){
// do physics step
step(dt*0.001); // dt comes in ms, engine needs seconds
}
function priorityTwo(stats){
// render everything
}
ZAP.CGLM.register(priorityOne, priorityTwo);
ZAP.CGLM.start();
(function(root){
//---------------------------------------------------------------------
// Constant Game Loop Machine
//---------------------------------------------------------------------
ZAP.CGLM = (function(root){
var CGLM = {
modes: {}
,mode: {}
,onTimeStep: function(){} // happens a lot
,onFrameUpdate: function(){} // happens not as much
,start: function(){
this.mode.onStart();
root.addEventListener('message', main, false);
root.postMessage('start timestep dispatch', '*');
console.log('CGLM START');
return this;
}
,stop: function(){
root.removeEventListener('message', main, false);
this.mode.onStop();
console.log('CGLM HALTED');
return this;
}
,register: function(onTimeStep, onFrameUpdate){
this.onTimeStep = onTimeStep;
this.onFrameUpdate = onFrameUpdate;
return this;
}
,setMode: function(mode){
if( typeof this.modes[mode] !== 'undefined' ){
this.mode = this.modes[mode];
} else {
throw [
'The mode "'
,mode
,'" is not defined for the Constant Game Loop Machine'
].join('');
}
return this;
}
};
function main(){
CGLM.mode.run( CGLM.onTimeStep, CGLM.onFrameUpdate );
root.postMessage('timestep dispatch', '*');
}
return CGLM;
})(this);
/**
* Custom mode allows control over all three control points:
* speed multiplier (iterations of the timeStep loop), timeStep
* (the value passed into the onTimeStep callback), and framerate,
* or how often onFrameUpdate is called. If the time between
* render frames is greater than 1.5 seconds, execution is
* automatically halted to prevent browser lockup.
*
*/
ZAP.CGLM.modes.custom = (function(){
var startTime
,totalFrames
,totalIterations = 0
,lastActualDT = 0
,lastTime = 0
,totalDTSinceLastFrame = 0
,totalDTSinceLastTimeStep = 0
,iterations = 1
,targetFrameInterval = 16
,timeStep = 5
,targetFPS = 60
,frameUpdateStats = {
delta: 0
,fps: 0
,avgFps: 0
}
,timeStepStats = {
delta: 0
,ips: 0
,avgips: 0
,iterations: 0
};
return {
run: function(tCommands, nCommands){
var d = +new Date()
,delta = d - lastTime
,i;
totalDTSinceLastFrame += delta;
totalDTSinceLastTimeStep += delta;
if(totalDTSinceLastTimeStep >= timeStep){
timeStepStats.delta = timeStep;
timeStepStats.ips = (iterations / totalDTSinceLastTimeStep * 1000).toFixed(2);
timeStepStats.avgips = (totalIterations / (d - startTime) * 1000).toFixed(2);
timeStepStats.iterations = iterations;
for(i = 0; i < iterations; i++){ tCommands(timeStep, timeStepStats); }
totalDTSinceLastTimeStep = 0;
totalIterations += iterations;
}
if(totalDTSinceLastFrame >= targetFrameInterval){
frameUpdateStats.delta = totalDTSinceLastFrame;
frameUpdateStats.fps = (1 / totalDTSinceLastFrame * 1000).toFixed(2);
frameUpdateStats.avgfps = (totalFrames / (d - startTime) * 1000).toFixed(2);
nCommands(frameUpdateStats);
if( totalDTSinceLastFrame > 1500 ){
console.log([
'AUTO-HALT.'
,' DELTA WAS GREATER THAN 1500 MS.'
,' TIME: ' + totalDTSinceLastFrame].join(''));
ZAP.CGLM.stop();
}
totalDTSinceLastFrame = 0;
totalFrames += 1;
}
lastTime = d;
}
,setFPS: function(fps){
targetFPS = fps;
targetFrameInterval = 1000 / targetFPS;
return this;
}
,setTimeStep: function(dt){
timeStep = dt;
return this;
}
,setSpeedX: function(x){
iterations = x;
return this;
}
,onStart: function(){
lastTime = +new Date();
totalDTSinceLastFrame = 0;
totalFrames = 0;
totalIterations = 0;
startTime = lastTime;
return this;
}
,onStop: function(){
return this;
}
}
})();
/**
* Realtime mode attempts to call onTimeStep as often as necessary
* to keep the passed game time equal to the passed world time, based
* on the target timeStep. If simTimeScale is less than 1, more time
* passes in the real world than in the game world. If the time between
* render frames is greater than 1.5 seconds, execution is
* automatically halted to prevent browser lockup.
*
*/
ZAP.CGLM.modes.realtime = (function(){
var startTime
,totalFrames
,totalIterations
,lastRunDT = 0 // how long the previous run() took
,thisRunDT = 0 // how long the current run is taking
,lastTime = 0 // the previous time run() was called
,totalDTSinceLastFrame = 0
,simTimeScale = 1 // 0.5 means 1 real second = 0.5 sim seconds
,iterations = 0
,remainingIT = 0
,targetFrameInterval = 16
,timeStep = 5
,targetFPS = 60
,frameUpdateStats = {
delta: 0
,fps: 0
,avgFps: 0
,lag: 0
}
,timeStepStats = {
delta: 0
,ips: 0
,avgips: 0
,iterations: 0
};
return {
run: function(tCommands, nCommands){
var iStartTime = +new Date()
,delta = iStartTime - lastTime
,i
,iEndTime;
totalDTSinceLastFrame += delta;
totalDTSinceLastTimeStep += (delta * simTimeScale);
remainingIT = iterations = (totalDTSinceLastTimeStep/timeStep) + remainingIT;
iterations = ~~iterations; // faster than Math.floor
remainingIT = remainingIT - iterations;
if(iterations > 0){
timeStepStats.delta = timeStep;
timeStepStats.ips = (iterations / totalDTSinceLastTimeStep * 1000).toFixed(2);
timeStepStats.avgips = (totalIterations / (iStartTime - startTime) * 1000 * 100).toFixed(2);
timeStepStats.iterations = iterations;
for(i = 0; i < iterations; i++){ tCommands(timeStep, timeStepStats); }
totalDTSinceLastTimeStep = 0;
totalIterations += iterations;
}
if(totalDTSinceLastFrame >= targetFrameInterval){
thisRunDT = +new Date() - iStartTime;
frameUpdateStats.delta = totalDTSinceLastFrame;
frameUpdateStats.fps = (1 / totalDTSinceLastFrame * 1000).toFixed(2);
frameUpdateStats.avgfps = (totalFrames / (iStartTime - startTime) * 1000).toFixed(2);
frameUpdateStats.lag = thisRunDT - lastRunDT;
nCommands(frameUpdateStats);
// TODO: if this happens, make it adaptive instead of halting?
if( totalDTSinceLastFrame > 1500 ){
console.log([
'AUTO-HALT.'
,' DELTA WAS GREATER THAN 1500 MS.'
,' TIME: ' + totalDTSinceLastFrame].join(''));
ZAP.CGLM.stop();
}
totalDTSinceLastFrame = 0;
totalFrames += 1;
}
lastRunDT = thisRunDT;
lastTime = iStartTime;
}
,setFPS: function(fps){
targetFPS = fps;
targetFrameInterval = 1000 / targetFPS;
return this;
}
,setTimeStep: function(dt){
timeStep = dt;
return this;
}
,setSimTimeScale: function(scale){
simTimeScale = scale;
return this;
}
,onStart: function(){
totalDTSinceLastFrame = 0;
totalDTSinceLastTimeStep = 0;
totalIterations = 0;
totalFrames = 0;
lastTime = +new Date();
startTime = lastTime;
return this;
}
,onStop: function(){
return this;
}
}
})();
root.ZAP = ZAP;
})(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment