2017-04-27 16:03:19 +00:00
|
|
|
var NOOP = require('../utils/NOOP');
|
2017-05-03 16:07:15 +00:00
|
|
|
var GetValue = require('../utils/object/GetValue');
|
2017-04-27 16:03:19 +00:00
|
|
|
var RequestAnimationFrame = require('../dom/RequestAnimationFrame');
|
|
|
|
|
2017-05-03 16:07:15 +00:00
|
|
|
// Frame Rate config
|
|
|
|
// fps: {
|
|
|
|
// min: 10,
|
|
|
|
// target: 60,
|
|
|
|
// max: 120
|
|
|
|
// forceSetTimeOut: false,
|
|
|
|
// deltaHistory: 10
|
|
|
|
// }
|
|
|
|
|
2017-05-04 00:08:50 +00:00
|
|
|
var TimeStep = function (game, config)
|
2017-04-27 16:03:19 +00:00
|
|
|
{
|
|
|
|
this.game = game;
|
|
|
|
|
|
|
|
this.raf = new RequestAnimationFrame();
|
|
|
|
|
|
|
|
this.started = false;
|
|
|
|
this.running = false;
|
2017-05-03 16:07:15 +00:00
|
|
|
|
|
|
|
this.minFps = GetValue(config, 'min', 5);
|
|
|
|
this.maxFps = GetValue(config, 'max', 120);
|
|
|
|
this.targetFps = GetValue(config, 'target', 60);
|
|
|
|
|
|
|
|
this._min = 1000 / this.minFps; // 200ms between frames (i.e. super slow!)
|
|
|
|
this._max = 1000 / this.maxFps; // 8.333ms between frames (i.e. super fast, 120Hz displays)
|
|
|
|
this._target = 1000 / this.targetFps; // 16.666ms between frames (i.e. normal)
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-03 16:07:15 +00:00
|
|
|
// 200 / 1000 = 0.2 (5fps)
|
|
|
|
// 8.333 / 1000 = 0.008333 (120fps)
|
|
|
|
// 16.666 / 1000 = 0.01666 (60fps)
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-04 00:08:50 +00:00
|
|
|
/**
|
|
|
|
* @property {number} fps - An exponential moving average of the frames per second.
|
|
|
|
* @readOnly
|
|
|
|
*/
|
|
|
|
this.actualFps = this.targetFps;
|
|
|
|
|
|
|
|
this.nextFpsUpdate = 0;
|
|
|
|
this.framesThisSecond = 0;
|
|
|
|
|
2017-04-27 16:03:19 +00:00
|
|
|
this.callback = NOOP;
|
|
|
|
|
2017-05-03 16:07:15 +00:00
|
|
|
this.forceSetTimeOut = GetValue(config, 'forceSetTimeOut', false);
|
2017-04-27 16:03:19 +00:00
|
|
|
|
|
|
|
this.time = 0;
|
|
|
|
this.startTime = 0;
|
|
|
|
this.lastTime = 0;
|
2017-05-04 16:32:05 +00:00
|
|
|
this.frame = 0;
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-09 00:24:46 +00:00
|
|
|
this._pauseTime = 0;
|
2017-05-09 09:42:43 +00:00
|
|
|
this._coolDown = 0;
|
2017-05-09 00:24:46 +00:00
|
|
|
|
2017-04-27 16:03:19 +00:00
|
|
|
this.delta = 0;
|
|
|
|
this.deltaIndex = 0;
|
|
|
|
this.deltaHistory = [];
|
2017-05-03 16:07:15 +00:00
|
|
|
this.deltaSmoothingMax = GetValue(config, 'deltaHistory', 10);
|
2017-04-27 16:03:19 +00:00
|
|
|
};
|
|
|
|
|
2017-05-04 00:08:50 +00:00
|
|
|
TimeStep.prototype.constructor = TimeStep;
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-04 00:08:50 +00:00
|
|
|
TimeStep.prototype = {
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-09 00:24:46 +00:00
|
|
|
// Called when the visibility API says the game is 'hidden' (tab switch, etc)
|
|
|
|
pause: function ()
|
2017-04-27 16:03:19 +00:00
|
|
|
{
|
2017-05-09 09:42:43 +00:00
|
|
|
// console.log('TimeStep.pause');
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-09 00:24:46 +00:00
|
|
|
this._pauseTime = window.performance.now();
|
|
|
|
},
|
|
|
|
|
|
|
|
// Called when the visibility API says the game is 'visible' again (tab switch, etc)
|
|
|
|
resume: function ()
|
|
|
|
{
|
|
|
|
this.resetDelta();
|
|
|
|
|
|
|
|
this.startTime += this.time - this._pauseTime;
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-09 09:42:43 +00:00
|
|
|
// console.log('TimeStep.resume - paused for', (this.time - this._pauseTime));
|
2017-05-09 00:24:46 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
resetDelta: function ()
|
|
|
|
{
|
2017-04-28 02:14:30 +00:00
|
|
|
var now = window.performance.now();
|
|
|
|
|
|
|
|
this.time = now;
|
|
|
|
this.lastTime = now;
|
2017-05-04 00:08:50 +00:00
|
|
|
this.nextFpsUpdate = now + 1000;
|
|
|
|
this.framesThisSecond = 0;
|
2017-05-04 16:32:05 +00:00
|
|
|
this.frame = 0;
|
2017-04-27 16:03:19 +00:00
|
|
|
|
|
|
|
// Pre-populate smoothing array
|
|
|
|
|
|
|
|
for (var i = 0; i < this.deltaSmoothingMax; i++)
|
|
|
|
{
|
2017-05-09 00:24:46 +00:00
|
|
|
this.deltaHistory[i] = this._target;
|
2017-04-27 16:03:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.delta = 0;
|
|
|
|
this.deltaIndex = 0;
|
2017-05-09 09:42:43 +00:00
|
|
|
|
|
|
|
this._coolDown = this.deltaSmoothingMax;
|
2017-05-09 00:24:46 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
start: function (callback)
|
|
|
|
{
|
|
|
|
if (this.started)
|
|
|
|
{
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.started = true;
|
|
|
|
this.running = true;
|
|
|
|
|
|
|
|
this.deltaHistory = [];
|
|
|
|
|
|
|
|
this.resetDelta();
|
|
|
|
|
|
|
|
this.startTime = window.performance.now();
|
2017-04-27 16:03:19 +00:00
|
|
|
|
|
|
|
this.callback = callback;
|
|
|
|
|
2017-05-03 16:07:15 +00:00
|
|
|
this.raf.start(this.step.bind(this), this.forceSetTimeOut);
|
2017-04-27 16:03:19 +00:00
|
|
|
},
|
|
|
|
|
2017-05-03 16:07:15 +00:00
|
|
|
// time comes from requestAnimationFrame and is either a high res time value,
|
|
|
|
// or Date.now if using setTimeout
|
2017-04-27 16:03:19 +00:00
|
|
|
step: function (time)
|
|
|
|
{
|
2017-05-04 16:32:05 +00:00
|
|
|
// Debug only
|
2017-05-09 09:42:43 +00:00
|
|
|
var debug = 0;
|
2017-05-04 16:32:05 +00:00
|
|
|
var dump = [];
|
|
|
|
|
|
|
|
this.frame++;
|
|
|
|
|
2017-05-03 00:34:29 +00:00
|
|
|
var idx = this.deltaIndex;
|
|
|
|
var history = this.deltaHistory;
|
|
|
|
var max = this.deltaSmoothingMax;
|
|
|
|
|
2017-05-03 16:07:15 +00:00
|
|
|
// delta time (time is in ms)
|
2017-05-09 09:42:43 +00:00
|
|
|
var dt;
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-04 16:32:05 +00:00
|
|
|
// When a browser switches tab, then comes back again, it takes around 10 frames before
|
2017-05-09 09:42:43 +00:00
|
|
|
// the delta time settles down so we employ a 'cooling down' period before we start
|
|
|
|
// trusting the delta values again, to avoid spikes flooding through our delta average
|
|
|
|
|
|
|
|
if (this._coolDown > 0)
|
|
|
|
{
|
|
|
|
this._coolDown--;
|
|
|
|
|
|
|
|
dt = this._target;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
dt = (time - this.lastTime);
|
|
|
|
}
|
2017-05-04 16:32:05 +00:00
|
|
|
|
2017-05-03 16:07:15 +00:00
|
|
|
// min / max range (yes, the < and > should be this way around)
|
|
|
|
if (dt > this._min || dt < this._max)
|
2017-04-27 16:03:19 +00:00
|
|
|
{
|
2017-05-03 16:07:15 +00:00
|
|
|
// Probably super bad start time or browser tab context loss,
|
2017-05-03 00:34:29 +00:00
|
|
|
// so use the last 'sane' dt value
|
|
|
|
|
2017-05-04 16:32:05 +00:00
|
|
|
debug = dt;
|
2017-05-03 01:21:32 +00:00
|
|
|
|
2017-05-03 00:34:29 +00:00
|
|
|
dt = history[idx];
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-03 16:07:15 +00:00
|
|
|
// Clamp delta to min max range (in case history has become corrupted somehow)
|
|
|
|
dt = Math.max(Math.min(dt, this._max), this._min);
|
2017-05-03 01:21:32 +00:00
|
|
|
}
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-02 23:54:09 +00:00
|
|
|
// Smooth out the delta over the previous X frames
|
2017-04-27 16:03:19 +00:00
|
|
|
|
|
|
|
// add the delta to the smoothing array
|
|
|
|
history[idx] = dt;
|
|
|
|
|
|
|
|
// adjusts the delta history array index based on the smoothing count
|
|
|
|
// this stops the array growing beyond the size of deltaSmoothingMax
|
2017-05-04 16:32:05 +00:00
|
|
|
this.deltaIndex++;
|
|
|
|
|
|
|
|
if (this.deltaIndex > max)
|
|
|
|
{
|
|
|
|
this.deltaIndex = 0;
|
|
|
|
}
|
2017-04-27 16:03:19 +00:00
|
|
|
|
2017-05-04 16:32:05 +00:00
|
|
|
// Delta Average
|
2017-04-27 16:03:19 +00:00
|
|
|
var avg = 0;
|
|
|
|
|
2017-04-28 02:14:30 +00:00
|
|
|
// Loop the history array, adding the delta values together
|
2017-05-04 16:32:05 +00:00
|
|
|
|
2017-04-27 16:03:19 +00:00
|
|
|
for (var i = 0; i < max; i++)
|
|
|
|
{
|
2017-05-04 16:32:05 +00:00
|
|
|
// Debug
|
|
|
|
if (history[i] < 16 || history[i] > 17)
|
|
|
|
{
|
|
|
|
dump.push({ i: i, dt: history[i] });
|
|
|
|
}
|
|
|
|
|
2017-04-27 16:03:19 +00:00
|
|
|
avg += history[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then divide by the array length to get the average delta
|
|
|
|
avg /= max;
|
|
|
|
|
|
|
|
// Set as the world delta value
|
|
|
|
this.delta = avg;
|
|
|
|
|
|
|
|
// Real-world timer advance
|
|
|
|
this.time += avg;
|
|
|
|
|
2017-05-04 00:08:50 +00:00
|
|
|
// Update the estimate of the frame rate, `fps`. Every second, the number
|
|
|
|
// of frames that occurred in that second are included in an exponential
|
|
|
|
// moving average of all frames per second, with an alpha of 0.25. This
|
|
|
|
// means that more recent seconds affect the estimated frame rate more than
|
|
|
|
// older seconds.
|
|
|
|
if (time > this.nextFpsUpdate)
|
|
|
|
{
|
|
|
|
// Compute the new exponential moving average with an alpha of 0.25.
|
|
|
|
// Using constants inline is okay here.
|
|
|
|
this.actualFps = 0.25 * this.framesThisSecond + 0.75 * this.actualFps;
|
|
|
|
this.nextFpsUpdate = time + 1000;
|
|
|
|
this.framesThisSecond = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.framesThisSecond++;
|
|
|
|
|
|
|
|
// Interpolation - how far between what is expected and where we are?
|
|
|
|
var interpolation = avg / this._target;
|
|
|
|
|
|
|
|
this.callback(this.time, avg, interpolation);
|
2017-04-27 16:03:19 +00:00
|
|
|
|
|
|
|
// Shift time value over
|
|
|
|
this.lastTime = time;
|
2017-05-04 16:32:05 +00:00
|
|
|
|
2017-05-09 09:42:43 +00:00
|
|
|
/*
|
|
|
|
if (debug !== 0 || dump.length)
|
2017-05-04 16:32:05 +00:00
|
|
|
{
|
|
|
|
console.group('Frame ' + this.frame);
|
|
|
|
console.log('Interpolation', interpolation, '%');
|
|
|
|
|
|
|
|
if (debug)
|
|
|
|
{
|
|
|
|
console.log('Elapsed', debug, 'ms');
|
|
|
|
}
|
|
|
|
|
2017-05-09 09:42:43 +00:00
|
|
|
// console.log('Frame', this.frame, 'Delta', avg, '(average)', debug, '(now)');
|
|
|
|
|
2017-05-04 16:32:05 +00:00
|
|
|
console.log('Delta', avg, '(average)');
|
|
|
|
|
|
|
|
if (dump.length)
|
|
|
|
{
|
|
|
|
console.table(dump);
|
|
|
|
}
|
|
|
|
|
|
|
|
console.groupEnd();
|
|
|
|
}
|
2017-05-09 09:42:43 +00:00
|
|
|
*/
|
2017-04-27 16:03:19 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
tick: function ()
|
|
|
|
{
|
2017-04-28 02:14:30 +00:00
|
|
|
this.step(window.performance.now());
|
2017-04-27 16:03:19 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
sleep: function ()
|
|
|
|
{
|
|
|
|
if (this.running)
|
|
|
|
{
|
|
|
|
this.raf.stop();
|
|
|
|
|
|
|
|
this.running = false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
wake: function (seamless)
|
|
|
|
{
|
|
|
|
if (this.running)
|
|
|
|
{
|
|
|
|
this.sleep();
|
|
|
|
}
|
|
|
|
else if (seamless)
|
|
|
|
{
|
2017-04-28 02:14:30 +00:00
|
|
|
this.startTime += -this.lastTime + (this.lastTime = window.performance.now());
|
2017-04-27 16:03:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.raf.start(this.step.bind(this), this.useRAF);
|
|
|
|
|
|
|
|
this.running = true;
|
|
|
|
|
2017-04-28 02:14:30 +00:00
|
|
|
this.step(window.performance.now());
|
2017-04-27 16:03:19 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
setFps: function (value)
|
|
|
|
{
|
|
|
|
this.fps = value;
|
|
|
|
|
|
|
|
this.wake();
|
|
|
|
},
|
|
|
|
|
|
|
|
getFps: function ()
|
|
|
|
{
|
|
|
|
return this.fps;
|
|
|
|
},
|
|
|
|
|
|
|
|
stop: function ()
|
|
|
|
{
|
|
|
|
this.running = false;
|
|
|
|
this.started = false;
|
|
|
|
|
|
|
|
this.raf.stop();
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-05-04 00:08:50 +00:00
|
|
|
module.exports = TimeStep;
|