diff --git a/src/core/Runner.js b/src/core/Runner.js index f827cfe6..15ceb223 100644 --- a/src/core/Runner.js +++ b/src/core/Runner.js @@ -1,9 +1,11 @@ /** -* The `Matter.Runner` module is an optional utility which provides a game loop, -* that handles continuously updating a `Matter.Engine` for you within a browser. -* It is intended for development and debugging purposes, but may also be suitable for simple games. -* If you are using your own game loop instead, then you do not need the `Matter.Runner` module. -* Instead just call `Engine.update(engine, delta)` in your own loop. +* Pre-release beta version. +* +* The `Matter.Runner` module is a lightweight, optional utility which provides a game loop. +* It is intended for development and debugging purposes inside a browser environment. +* It will continuously update a `Matter.Engine` with a given fixed timestep whilst synchronising updates with the browser frame rate. +* This runner favours a smoother user experience over perfect time keeping. +* To directly step the engine as part of your own alternative game loop implementation, see `Engine.update`. * * See the included usage [examples](https://github.com/liabru/matter-js/tree/master/examples). * @@ -20,73 +22,64 @@ var Common = require('./Common'); (function() { - var _requestAnimationFrame, - _cancelAnimationFrame; - - if (typeof window !== 'undefined') { - _requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame - || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; - - _cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame - || window.webkitCancelAnimationFrame || window.msCancelAnimationFrame; - } - - if (!_requestAnimationFrame) { - var _frameTimeout; - - _requestAnimationFrame = function(callback){ - _frameTimeout = setTimeout(function() { - callback(Common.now()); - }, 1000 / 60); - }; - - _cancelAnimationFrame = function() { - clearTimeout(_frameTimeout); - }; - } + Runner._timeBufferMargin = 1.5; + Runner._smoothingLowerBound = 0.1; + Runner._smoothingUpperBound = 0.9; /** - * Creates a new Runner. The options parameter is an object that specifies any properties you wish to override the defaults. + * Creates a new Runner. + * See the properties section below for detailed information on what you can pass via the `options` object. * @method create * @param {} options */ Runner.create = function(options) { var defaults = { - fps: 60, - deltaSampleSize: 60, - counterTimestamp: 0, - frameCounter: 0, - deltaHistory: [], - timePrev: null, + delta: 1000 / 60, + frameDelta: 0, + frameDeltaSmoothing: true, + frameDeltaSnapping: true, + frameDeltaHistory: [], + frameDeltaHistorySize: 100, frameRequestId: null, - isFixed: false, + timeBuffer: 0, + timeLastTick: null, + maxUpdates: null, + maxFrameTime: 1000 / 50, + maxFrameDelta: 1000 / 10, + lastUpdatesDeferred: 0, enabled: true }; var runner = Common.extend(defaults, options); - runner.delta = runner.delta || 1000 / runner.fps; - runner.deltaMin = runner.deltaMin || 1000 / runner.fps; - runner.deltaMax = runner.deltaMax || 1000 / (runner.fps * 0.5); - runner.fps = 1000 / runner.delta; + if (runner.maxUpdates === null) { + runner.maxUpdates = Math.ceil(runner.maxFrameTime / runner.delta); + } + + // for temporary back compatibility only + runner.fps = 0; return runner; }; /** - * Continuously ticks a `Matter.Engine` by calling `Runner.tick` on the `requestAnimationFrame` event. + * Continuously updates a `Matter.Engine` on every browser frame whilst synchronising updates with the browser frame rate. + * It is intended for development and debugging purposes inside a browser environment. + * This runner favours a smoother user experience over perfect time keeping. + * The number of updates per frame is kept within limits specified by `runner.maxFrameTime`, `runner.maxUpdates` and `runner.maxFrameDelta`. + * When device performance is too limited the simulation may appear to slow down compared to real time. + * As an alternative, to directly step the engine in your own game loop implementation, see `Engine.update`. * @method run - * @param {engine} engine + * @param {runner} runner + * @param {engine} [engine] + * @return {runner} runner */ Runner.run = function(runner, engine) { - // create runner if engine is first argument - if (typeof runner.positionIterations !== 'undefined') { - engine = runner; - runner = Runner.create(); - } + // initial time buffer for the first frame + runner.timeBuffer = runner.delta * Runner._timeBufferMargin; - (function run(time){ - runner.frameRequestId = _requestAnimationFrame(run); + (function onFrame(time){ + runner.frameRequestId = Runner._onNextFrame(runner, onFrame); if (time && runner.enabled) { Runner.tick(runner, engine, time); @@ -97,84 +90,190 @@ var Common = require('./Common'); }; /** - * A game loop utility that updates the engine and renderer by one step (a 'tick'). - * Features delta smoothing, time correction and fixed or dynamic timing. - * Consider just `Engine.update(engine, delta)` if you're using your own loop. + * Used by the game loop inside `Runner.run`. + * + * As an alternative to directly step the engine in your own game loop implementation, see `Engine.update`. * @method tick * @param {runner} runner * @param {engine} engine * @param {number} time */ Runner.tick = function(runner, engine, time) { - var timing = engine.timing, - delta; + var tickStartTime = Common.now(), + engineDelta = runner.delta, + updateCount = 0; - if (runner.isFixed) { - // fixed timestep - delta = runner.delta; - } else { - // dynamic timestep based on wall clock between calls - delta = (time - runner.timePrev) || runner.delta; - runner.timePrev = time; + // find frame delta time since last call + var frameDelta = time - runner.timeLastTick; - // optimistically filter delta over a few frames, to improve stability - runner.deltaHistory.push(delta); - runner.deltaHistory = runner.deltaHistory.slice(-runner.deltaSampleSize); - delta = Math.min.apply(null, runner.deltaHistory); + // fallback for unexpected frame delta values (e.g. 0, NaN or from long pauses) + if (!frameDelta || frameDelta > runner.maxFrameDelta) { + // reuse last accepted frame delta or fallback to one update + frameDelta = runner.frameDelta || engineDelta; + } + + if (runner.frameDeltaSmoothing) { + // record frame delta over a number of frames + runner.frameDeltaHistory.push(frameDelta); + runner.frameDeltaHistory = runner.frameDeltaHistory.slice(-runner.frameDeltaHistorySize); - // limit delta - delta = delta < runner.deltaMin ? runner.deltaMin : delta; - delta = delta > runner.deltaMax ? runner.deltaMax : delta; + // sort frame delta history + var deltaHistorySorted = runner.frameDeltaHistory.slice(0).sort(); - // update engine timing object - runner.delta = delta; + // sample a central window to limit outliers + var deltaHistoryWindow = runner.frameDeltaHistory.slice( + deltaHistorySorted.length * Runner._smoothingLowerBound, + deltaHistorySorted.length * Runner._smoothingUpperBound + ); + + // take the mean of the central window + var frameDeltaSmoothed = _mean(deltaHistoryWindow); + frameDelta = frameDeltaSmoothed || frameDelta; } - // create an event object + if (runner.frameDeltaSnapping) { + // snap frame delta to the nearest 1 Hz + frameDelta = 1000 / Math.round(1000 / frameDelta); + } + + // update runner values for next call + runner.frameDelta = frameDelta; + runner.timeLastTick = time; + + // accumulate elapsed time + runner.timeBuffer += runner.frameDelta; + + // limit time buffer size to a single frame of updates + runner.timeBuffer = Common.clamp( + runner.timeBuffer, 0, runner.frameDelta + engineDelta * Runner._timeBufferMargin + ); + + // reset count of over budget updates + runner.lastUpdatesDeferred = 0; + + // get max updates per second + var maxUpdates = runner.maxUpdates; + + // create event object var event = { - timestamp: timing.timestamp + timestamp: engine.timing.timestamp }; + // tick events before update Events.trigger(runner, 'beforeTick', event); + Events.trigger(runner, 'tick', event); - // fps counter - runner.frameCounter += 1; - if (time - runner.counterTimestamp >= 1000) { - runner.fps = runner.frameCounter * ((time - runner.counterTimestamp) / 1000); - runner.counterTimestamp = time; - runner.frameCounter = 0; - } + // simulate time elapsed between calls + while (engineDelta > 0 && runner.timeBuffer >= engineDelta * Runner._timeBufferMargin) { + var updateStartTime = Common.now(); - Events.trigger(runner, 'tick', event); + // update the engine + Events.trigger(runner, 'beforeUpdate', event); + Engine.update(engine, engineDelta); + Events.trigger(runner, 'afterUpdate', event); - // update - Events.trigger(runner, 'beforeUpdate', event); + // consume time simulated from buffer + runner.timeBuffer -= engineDelta; + updateCount += 1; - Engine.update(engine, delta); + // find elapsed time during this tick + var elapsedTimeTotal = Common.now() - tickStartTime, + elapsedTimeUpdate = Common.now() - updateStartTime; - Events.trigger(runner, 'afterUpdate', event); + // defer updates if over performance budgets for this frame + if (updateCount >= maxUpdates || elapsedTimeTotal + elapsedTimeUpdate > runner.maxFrameTime) { + runner.lastUpdatesDeferred = Math.round(Math.max(0, (runner.timeBuffer / engineDelta) - Runner._timeBufferMargin)); + break; + } + } + // track timing metrics + engine.timing.lastUpdatesPerFrame = updateCount; + + // tick events after update Events.trigger(runner, 'afterTick', event); + + // show useful warnings if needed + if (runner.frameDeltaHistory.length >= 100) { + if (runner.lastUpdatesDeferred && Math.round(runner.frameDelta / engineDelta) > maxUpdates) { + Common.warnOnce('Matter.Runner: runner reached runner.maxUpdates, see docs.'); + } else if (runner.lastUpdatesDeferred) { + Common.warnOnce('Matter.Runner: runner reached runner.maxFrameTime, see docs.'); + } + + if (typeof runner.isFixed !== 'undefined') { + Common.warnOnce('Matter.Runner: runner.isFixed is now redundant, see docs.'); + } + + if (runner.deltaMin || runner.deltaMax) { + Common.warnOnce('Matter.Runner: runner.deltaMin and runner.deltaMax were removed, see docs.'); + } + + if (runner.fps !== 0) { + Common.warnOnce('Matter.Runner: runner.fps was replaced by runner.delta, see docs.'); + } + } }; /** - * Ends execution of `Runner.run` on the given `runner`, by canceling the animation frame request event loop. - * If you wish to only temporarily pause the runner, see `runner.enabled` instead. + * Ends execution of `Runner.run` on the given `runner`, by canceling the frame loop. + * + * To temporarily pause the runner, see `runner.enabled`. * @method stop * @param {runner} runner */ Runner.stop = function(runner) { - _cancelAnimationFrame(runner.frameRequestId); + Runner._cancelNextFrame(runner); }; /** - * Alias for `Runner.run`. - * @method start + * Schedules a `callback` on this `runner` for the next animation frame. + * @private + * @method _onNextFrame * @param {runner} runner - * @param {engine} engine + * @param {function} callback + * @return {number} frameRequestId */ - Runner.start = function(runner, engine) { - Runner.run(runner, engine); + Runner._onNextFrame = function(runner, callback) { + if (typeof window !== 'undefined') { + runner.frameRequestId = window.requestAnimationFrame(callback); + } else { + Common.warnOnce('Matter.Runner: missing required global window.requestAnimationFrame.'); + } + + return runner.frameRequestId; + }; + + /** + * Cancels the last callback scheduled on this `runner` by `Runner._onNextFrame`. + * @private + * @method _cancelNextFrame + * @param {runner} runner + */ + Runner._cancelNextFrame = function(runner) { + if (typeof window !== 'undefined') { + window.cancelAnimationFrame(runner.frameRequestId); + } else { + Common.warnOnce('Matter.Runner: missing required global window.cancelAnimationFrame.'); + } + }; + + /** + * Returns the mean of the given numbers. + * @method _mean + * @private + * @param {Number[]} values + * @return {Number} the mean of given values. + */ + var _mean = function(values) { + var result = 0, + valuesLength = values.length; + + for (var i = 0; i < valuesLength; i += 1) { + result += values[i]; + } + + return (result / valuesLength) || 0; }; /* @@ -184,7 +283,7 @@ var Common = require('./Common'); */ /** - * Fired at the start of a tick, before any updates to the engine or timing + * Fired once at the start of the browser frame, before any engine updates. * * @event beforeTick * @param {} event An event object @@ -194,7 +293,7 @@ var Common = require('./Common'); */ /** - * Fired after engine timing updated, but just before update + * Fired once at the start of the browser frame, after `beforeTick`. * * @event tick * @param {} event An event object @@ -204,7 +303,7 @@ var Common = require('./Common'); */ /** - * Fired at the end of a tick, after engine update and after rendering + * Fired once at the end of the browser frame, after `beforeTick`, `tick` and after any engine updates. * * @event afterTick * @param {} event An event object @@ -214,7 +313,8 @@ var Common = require('./Common'); */ /** - * Fired before update + * Fired before each and every update in this browser frame (if any). + * There may be multiple calls per browser frame (or none), depending on framerate and engine delta. * * @event beforeUpdate * @param {} event An event object @@ -224,7 +324,8 @@ var Common = require('./Common'); */ /** - * Fired after update + * Fired after each and every update in this browser frame (if any). + * There may be multiple calls per browser frame (or none), depending on framerate and engine delta. * * @event afterUpdate * @param {} event An event object @@ -240,7 +341,31 @@ var Common = require('./Common'); */ /** - * A flag that specifies whether the runner is running or not. + * The fixed timestep size used for `Engine.update` calls in milliseconds. + * + * This `delta` value is recommended to be `16.666` ms or lower (equivalent to at least 60hz). + * + * Lower `delta` values provide a higher quality results at the cost of performance. + * + * This value should be held fixed during running, otherwise quality may be affected. + * + * For smoothest results choose an evenly divisible factor of your target framerates, this helps provide an equal number of updates per frame. + * + * Rounding to the nearest 1 Hz is recommended for smoother results (see `runner.frameDeltaSnapping`). + * + * For example with a `delta` of `1000 / 60` the runner will on average perform one update per frame for displays at 60 FPS, or one update every two frames for displays at 120 FPS. + * + * `Runner.run` will call multiple engine updates to simulate the time elapsed between frames, but the number of allowed calls may be limited by the runner's performance budgets. + * + * These performance budgets are specified by `runner.maxFrameTime`, `runner.maxUpdates`, `runner.maxFrameDelta`. See those properties for details. + * + * @property delta + * @type number + * @default 1000 / 60 + */ + + /** + * A flag that can be toggled to enable or disable tick calls on this runner, therefore pausing engine updates while the loop remains running. * * @property enabled * @type boolean @@ -248,23 +373,98 @@ var Common = require('./Common'); */ /** - * A `Boolean` that specifies if the runner should use a fixed timestep (otherwise it is variable). - * If timing is fixed, then the apparent simulation speed will change depending on the frame rate (but behaviour will be deterministic). - * If the timing is variable, then the apparent simulation speed will be constant (approximately, but at the cost of determininism). + * The accumulated time elapsed that has yet to be simulated, in milliseconds. + * This value is clamped within some limits (see `Runner.tick` code). * - * @property isFixed + * @private + * @property timeBuffer + * @type number + * @default 0 + */ + + /** + * The measured time elapsed between the last two browser frames in milliseconds. + * This value is clamped inside `runner.maxFrameDelta`. + * + * You may use this to estimate the browser FPS (for the current instant) whilst running use `1000 / runner.frameDelta`. + * + * @readonly + * @property frameDelta + * @type number + */ + + /** + * This option applies averaging to the frame delta to smooth noisy frame rates. + * + * @property frameDeltaSmoothing * @type boolean - * @default false + * @default true */ /** - * A `Number` that specifies the time step between updates in milliseconds. - * If `engine.timing.isFixed` is set to `true`, then `delta` is fixed. - * If it is `false`, then `delta` can dynamically change to maintain the correct apparent simulation speed. + * Rounds frame delta to the nearest 1 Hz. + * It follows that your choice of `runner.delta` should be rounded to the nearest 1 Hz for best results. + * This option helps smooth noisy refresh rates and simplify hardware differences e.g. 59.94Hz vs 60Hz display refresh rates. * - * @property delta + * @property frameDeltaSnapping + * @type boolean + * @default true + */ + + /** + * Clamps the maximum `runner.frameDelta` in milliseconds. + * This is to avoid simulating periods where the browser thread is paused e.g. whilst minimised. + * + * @property maxFrameDelta * @type number - * @default 1000 / 60 + * @default 500 + */ + + /** + * The runner will attempt to limit engine update rate should the browser frame rate drop below a set level (50 FPS using the default `runner.maxFrameTime` value `1000 / 50` milliseconds). + * + * This budget is intended to help maintain browser interactivity and help improve framerate recovery during temporary high CPU spikes. + * + * It will only cover time elapsed whilst executing the functions called in the scope of this runner, including `Engine.update` etc. and its related event callbacks. + * + * For any significant additional processing you perform on the same thread but outside the scope of this runner e.g. rendering time, you may wish to reduce this budget. + * + * See also `runner.maxUpdates`. + * + * @property maxFrameTime + * @type number + * @default 1000 / 50 + */ + + /** + * A hard limit for maximum engine updates allowed per frame. + * + * If not provided, it is automatically set by `Runner.create` based on `runner.delta` and `runner.maxFrameTime`. + * If you change `runner.delta` or `runner.maxFrameTime`, you may need to manually update this value afterwards. + * + * See also `runner.maxFrameTime`. + * + * @property maxUpdates + * @type number + * @default null + */ + + /** + * The timestamp of the last call to `Runner.tick`, used to measure `frameDelta`. + * + * @private + * @property timeLastTick + * @type number + * @default 0 + */ + + /** + * The id of the last call to `Runner._onNextFrame`. + * + * @private + * @property frameRequestId + * @type number + * @default null */ })();