var Class = require('../utils/Class'); var GetValue = require('../utils/object/GetValue'); var NOOP = require('../utils/NOOP'); var RequestAnimationFrame = require('../dom/RequestAnimationFrame'); // Frame Rate config // fps: { // min: 10, // target: 60, // forceSetTimeOut: false, // deltaHistory: 10, // panicMax: 120 // } // http://www.testufo.com/#test=animation-time-graph var TimeStep = new Class({ initialize: /** * [description] * * @class TimeStep * @memberOf Phaser.Boot * @constructor * @since 3.0.0 * * @param {Phaser.Game} game - A reference to the Phaser.Game instance that owns this Time Step. * @param {FPSConfig} config */ function TimeStep (game, config) { /** * A reference to the Phaser.Game instance. * * @property {Phaser.Game} game * @readOnly * @since 3.0.0 */ this.game = game; /** * [description] * * @property {Phaser.DOM.RequestAnimationFrame} raf * @readOnly * @since 3.0.0 */ this.raf = new RequestAnimationFrame(); /** * A flag that is set once the TimeStep has started running and toggled when it stops. * * @property {boolean} started * @readOnly * @default false * @since 3.0.0 */ this.started = false; /** * A flag that is set once the TimeStep has started running and toggled when it stops. * The difference between this value and `started` is that `running` is toggled when * the TimeStep is sent to sleep, where-as `started` remains `true`, only changing if * the TimeStep is actually stopped, not just paused. * * @property {boolean} running * @readOnly * @default false * @since 3.0.0 */ this.running = false; /** * The minimum fps rate you want the Time Step to run at. * * @property {integer} minFps * @default 5 * @since 3.0.0 */ this.minFps = GetValue(config, 'min', 5); /** * The target fps rate for the Time Step to run at. * * Setting this value will not actually change the speed at which the browser runs, that is beyond * the control of Phaser. Instead, it allows you to determine performance issues and if the Time Step * is spiraling out of control. * * @property {integer} targetFps * @default 60 * @since 3.0.0 */ this.targetFps = GetValue(config, 'target', 60); /** * The minFps value in ms. * Defaults to 200ms between frames (i.e. super slow!) * * @property {number} _min * @private * @since 3.0.0 */ this._min = 1000 / this.minFps; /** * The targetFps value in ms. * Defaults to 16.66ms between frames (i.e. normal) * * @property {number} _target * @private * @since 3.0.0 */ this._target = 1000 / this.targetFps; /** * An exponential moving average of the frames per second. * * @property {integer} actualFps * @readOnly * @default 60 * @since 3.0.0 */ this.actualFps = this.targetFps; /** * [description] * * @property {integer} nextFpsUpdate * @readOnly * @default 0 * @since 3.0.0 */ this.nextFpsUpdate = 0; /** * The number of frames processed this second. * * @property {integer} framesThisSecond * @readOnly * @default 0 * @since 3.0.0 */ this.framesThisSecond = 0; /** * A callback to be invoked each time the Time Step steps. * * @property {function} callback * @default NOOP * @since 3.0.0 */ this.callback = NOOP; /** * You can force the Time Step to use Set Timeout instead of Request Animation Frame by setting * the `forceSetTimeOut` property to `true` in the Game Configuration object. It cannot be changed at run-time. * * @property {boolean} forceSetTimeOut * @readOnly * @default false * @since 3.0.0 */ this.forceSetTimeOut = GetValue(config, 'forceSetTimeOut', false); /** * [description] * * @property {integer} time * @default 0 * @since 3.0.0 */ this.time = 0; /** * [description] * * @property {integer} startTime * @default 0 * @since 3.0.0 */ this.startTime = 0; /** * [description] * * @property {integer} lastTime * @default 0 * @since 3.0.0 */ this.lastTime = 0; /** * [description] * * @property {integer} frame * @readOnly * @default 0 * @since 3.0.0 */ this.frame = 0; /** * [description] * * @property {boolean} inFocus * @readOnly * @default true * @since 3.0.0 */ this.inFocus = true; /** * [description] * * @property {integer} _pauseTime * @private * @default 0 * @since 3.0.0 */ this._pauseTime = 0; /** * [description] * * @property {integer} _coolDown * @private * @default 0 * @since 3.0.0 */ this._coolDown = 0; /** * [description] * * @property {integer} delta * @default 0 * @since 3.0.0 */ this.delta = 0; /** * [description] * * @property {integer} deltaIndex * @default 0 * @since 3.0.0 */ this.deltaIndex = 0; /** * [description] * * @property {array} deltaHistory * @default 0 * @since 3.0.0 */ this.deltaHistory = []; /** * [description] * * @property {integer} deltaSmoothingMax * @default 10 * @since 3.0.0 */ this.deltaSmoothingMax = GetValue(config, 'deltaHistory', 10); /** * [description] * * @property {integer} panicMax * @default 120 * @since 3.0.0 */ this.panicMax = GetValue(config, 'panicMax', 120); /** * The actual elapsed time in ms between one update and the next. * Unlike with `delta` no smoothing, capping, or averaging is applied to this value. * So please be careful when using this value in calculations. * * @property {number} rawDelta * @default 0 * @since 3.0.0 */ this.rawDelta = 0; }, /** * Called when the DOM window.onBlur event triggers. * * @method Phaser.Boot.TimeStep#blur * @since 3.0.0 */ blur: function () { this.inFocus = false; }, /** * Called when the DOM window.onFocus event triggers. * * @method Phaser.Boot.TimeStep#focus * @since 3.0.0 */ focus: function () { this.inFocus = true; this.resetDelta(); }, /** * Called when the visibility API says the game is 'hidden' (tab switch out of view, etc) * * @method Phaser.Boot.TimeStep#pause * @since 3.0.0 */ pause: function () { this._pauseTime = window.performance.now(); }, /** * Called when the visibility API says the game is 'visible' again (tab switch back into view, etc) * * @method Phaser.Boot.TimeStep#resume * @since 3.0.0 */ resume: function () { this.resetDelta(); this.startTime += this.time - this._pauseTime; }, /** * [description] * * @method Phaser.Boot.TimeStep#resetDelta * @since 3.0.0 */ resetDelta: function () { var now = window.performance.now(); this.time = now; this.lastTime = now; this.nextFpsUpdate = now + 1000; this.framesThisSecond = 0; this.frame = 0; // Pre-populate smoothing array for (var i = 0; i < this.deltaSmoothingMax; i++) { this.deltaHistory[i] = Math.min(this._target, this.deltaHistory[i]); } this.delta = 0; this.deltaIndex = 0; this._coolDown = this.panicMax; }, /** * Starts the Time Step running, if it is not already doing so. * Called automatically by the Game Boot process. * * @method Phaser.Boot.TimeStep#start * @since 3.0.0 * * @param {function} callback - The callback to be invoked each time the Time Step steps. */ start: function (callback) { if (this.started) { return this; } this.started = true; this.running = true; for (var i = 0; i < this.deltaSmoothingMax; i++) { this.deltaHistory[i] = this._target; } this.resetDelta(); this.startTime = window.performance.now(); this.callback = callback; this.raf.start(this.step.bind(this), this.forceSetTimeOut); }, /** * The main step method. This is called each time the browser updates, either by Request Animation Frame, * or by Set Timeout. It is responsible for calculating the delta values, frame totals, cool down history and more. * You generally should never call this method directly. * * @method Phaser.Boot.TimeStep#step * @since 3.0.0 * * @param {integer} time - The current time. Either a High Resolution Timer value if it comes from Request Animation Frame, or Date.now if using SetTimeout. */ step: function (time) { this.frame++; this.rawDelta = time - this.lastTime; var idx = this.deltaIndex; var history = this.deltaHistory; var max = this.deltaSmoothingMax; // delta time (time is in ms) var dt = (time - this.lastTime); // When a browser switches tab, then comes back again, it takes around 10 frames before // 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.inFocus) { this._coolDown--; dt = Math.min(dt, this._target); } if (dt > this._min) { // Probably super bad start time or browser tab context loss, // so use the last 'sane' dt value dt = history[idx]; // Clamp delta to min (in case history has become corrupted somehow) dt = Math.min(dt, this._min); } // Smooth out the delta over the previous X frames // 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 this.deltaIndex++; if (this.deltaIndex > max) { this.deltaIndex = 0; } // Delta Average var avg = 0; // Loop the history array, adding the delta values together for (var i = 0; i < max; i++) { 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 += this.rawDelta; // 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. // // When a browser window is NOT minimized, but is covered up (i.e. you're using // another app which has spawned a window over the top of the browser), then it // will start to throttle the raf callback time. It waits for a while, and then // starts to drop the frame rate at 1 frame per second until it's down to just over 1fps. // So if the game was running at 60fps, and the player opens a new window, then // after 60 seconds (+ the 'buffer time') it'll be down to 1fps, so rafin'g at 1Hz. // // When they make the game visible again, the frame rate is increased at a rate of // approx. 8fps, back up to 60fps (or the max it can obtain) // // There is no easy way to determine if this drop in frame rate is because the // browser is throttling raf, or because the game is struggling with performance // because you're asking it to do too much on the device. if (time > this.nextFpsUpdate) { // Compute the new exponential moving average with an alpha of 0.25. 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(time, avg, interpolation); // Shift time value over this.lastTime = time; }, /** * Manually calls TimeStep.step, passing in the performance.now value to it. * * @method Phaser.Boot.TimeStep#tick * @since 3.0.0 */ tick: function () { this.step(window.performance.now()); }, /** * Sends the TimeStep to sleep, stopping Request Animation Frame (or SetTimeout) and toggling the `running` flag to false. * * @method Phaser.Boot.TimeStep#sleep * @since 3.0.0 */ sleep: function () { if (this.running) { this.raf.stop(); this.running = false; } }, /** * Wakes-up the TimeStep, restarting Request Animation Frame (or SetTimeout) and toggling the `running` flag to true. * The `seamless` argument controls if the wake-up should adjust the start time or not. * * @method Phaser.Boot.TimeStep#wake * @since 3.0.0 * * @param {boolean} [seamless=false] - Adjust the startTime based on the lastTime values. */ wake: function (seamless) { if (this.running) { this.sleep(); } else if (seamless) { this.startTime += -this.lastTime + (this.lastTime = window.performance.now()); } this.raf.start(this.step.bind(this), this.useRAF); this.running = true; this.step(window.performance.now()); }, /** * Stops the TimeStep running. * * @method Phaser.Boot.TimeStep#stop * @since 3.0.0 * * @return {Phaser.Boot.TimeStep} The TimeStep object. */ stop: function () { this.running = false; this.started = false; this.raf.stop(); return this; }, /** * Destroys the TimeStep. This will stop Request Animation Frame, stop the step, clear the callbacks and null * any objects. * * @method Phaser.Boot.TimeStep#destroy * @since 3.0.0 */ destroy: function () { this.stop(); this.callback = null; this.raf = null; this.game = null; } }); module.exports = TimeStep;