diff --git a/demo/src/Compare.js b/demo/src/Compare.js index e93fdfca..f4bad673 100644 --- a/demo/src/Compare.js +++ b/demo/src/Compare.js @@ -116,7 +116,6 @@ var compare = function(examples, isDev) { ticks += 1; var demoBuildInstance = demoBuild.example.instance; - runner.isFixed = demoBuildInstance.runner.isFixed = true; runner.delta = demoBuildInstance.runner.delta = 1000 / 60; window.Matter = MatterBuild; diff --git a/demo/src/Demo.js b/demo/src/Demo.js index 986a8013..a811d929 100644 --- a/demo/src/Demo.js +++ b/demo/src/Demo.js @@ -41,6 +41,22 @@ var demo = function(examples, isDev) { document.title = 'Matter.js Demo' + (isDev ? ' ・ Dev' : ''); if (isDev) { + // add delta control + Matter.Common.chainPathAfter(MatterTools, 'Gui.create', function() { + this.datGui.__folders["Engine"] + .add(demo, "delta", 0, 1000 / 55) + .onChange(function() { + var runner = demo.example.instance.runner; + runner.delta = demo.delta; + }) + .step(0.001) + .listen(); + }); + + Matter.after('Runner.create', function() { + demo.delta = this.delta; + }); + // add compare button var buttonSource = demo.dom.buttonSource, buttonCompare = buttonSource.cloneNode(true); diff --git a/examples/index.js b/examples/index.js index 6e1a615d..e146895c 100644 --- a/examples/index.js +++ b/examples/index.js @@ -41,6 +41,7 @@ module.exports = { stress2: require('./stress2.js'), stress3: require('./stress3.js'), stress4: require('./stress4.js'), + substep: require('./substep.js'), svg: require('./svg.js'), terrain: require('./terrain.js'), timescale: require('./timescale.js'), diff --git a/examples/stress3.js b/examples/stress3.js index 3def2e5a..4feb37ff 100644 --- a/examples/stress3.js +++ b/examples/stress3.js @@ -34,9 +34,7 @@ Example.stress3 = function() { Render.run(render); // create runner - var runner = Runner.create({ - isFixed: true - }); + var runner = Runner.create(); Runner.run(runner, engine); // add bodies diff --git a/examples/stress4.js b/examples/stress4.js index bcbf251a..ea8cd9d5 100644 --- a/examples/stress4.js +++ b/examples/stress4.js @@ -35,9 +35,7 @@ Example.stress4 = function() { Render.run(render); // create runner - var runner = Runner.create({ - isFixed: true - }); + var runner = Runner.create(); Runner.run(runner, engine); diff --git a/examples/substep.js b/examples/substep.js new file mode 100644 index 00000000..d19f1530 --- /dev/null +++ b/examples/substep.js @@ -0,0 +1,122 @@ +var Example = Example || {}; + +Example.substep = function() { + var Engine = Matter.Engine, + Render = Matter.Render, + Runner = Matter.Runner, + Events = Matter.Events, + Composite = Matter.Composite, + MouseConstraint = Matter.MouseConstraint, + Mouse = Matter.Mouse, + Bodies = Matter.Bodies; + + // create engine + var engine = Engine.create(), + world = engine.world; + + // create renderer + var render = Render.create({ + element: document.body, + engine: engine, + options: { + width: 800, + height: 600, + wireframes: false, + showDebug: true, + background: '#000', + pixelRatio: 2 + } + }); + + Render.run(render); + + // create runner with higher precision timestep (requires >= v0.20.0 beta) + var runner = Runner.create({ + // 600Hz delta = 1.666ms = 10upf @ 60fps (i.e. 10x default precision) + delta: 1000 / (60 * 10), + // 50fps minimum performance target (i.e. budget allows up to ~20ms execution per frame) + maxFrameTime: 1000 / 50 + }); + + Runner.run(runner, engine); + + // demo substepping using very thin bodies (than is typically recommended) + Composite.add(world, [ + Bodies.rectangle(250, 250, 300, 1.25, { + frictionAir: 0, + friction: 0, + restitution: 0.9, + angle: 0.5, + render: { + lineWidth: 0.5, + fillStyle: '#f55a3c' + } + }), + Bodies.circle(200, 200, 2.25, { + frictionAir: 0, + friction: 0, + restitution: 0.9, + angle: 0.5, + render: { + fillStyle: '#fff' + } + }) + ]); + + // add bodies + Composite.add(world, [ + Bodies.rectangle(400, 0, 800, 50, { isStatic: true, render: { visible: false } }), + Bodies.rectangle(400, 600, 800, 50, { isStatic: true, render: { visible: false } }), + Bodies.rectangle(800, 300, 50, 600, { isStatic: true, render: { visible: false } }), + Bodies.rectangle(0, 300, 50, 600, { isStatic: true, render: { visible: false } }) + ]); + + // scene animation + Events.on(engine, 'afterUpdate', function(event) { + engine.gravity.scale = 0.00035; + engine.gravity.x = Math.cos(engine.timing.timestamp * 0.0005); + engine.gravity.y = Math.sin(engine.timing.timestamp * 0.0005); + }); + + // add mouse control + var mouse = Mouse.create(render.canvas), + mouseConstraint = MouseConstraint.create(engine, { + mouse: mouse, + constraint: { + stiffness: 0.2, + render: { + visible: false + } + } + }); + + Composite.add(world, mouseConstraint); + + // keep the mouse in sync with rendering + render.mouse = mouse; + + // fit the render viewport to the scene + Render.lookAt(render, { + min: { x: 0, y: 0 }, + max: { x: 800, y: 600 } + }); + + // context for MatterTools.Demo + return { + engine: engine, + runner: runner, + render: render, + canvas: render.canvas, + stop: function() { + Matter.Render.stop(render); + Matter.Runner.stop(runner); + } + }; +}; + +Example.substep.title = 'High Substeps (Low Delta)'; +Example.substep.for = '>=0.20.0'; + +if (typeof module !== 'undefined') { + module.exports = Example.substep; +} \ No newline at end of file diff --git a/src/core/Engine.js b/src/core/Engine.js index cdf03c4a..bf323920 100644 --- a/src/core/Engine.js +++ b/src/core/Engine.js @@ -24,6 +24,8 @@ var Body = require('../body/Body'); (function() { + Engine._deltaMax = 1000 / 60; + /** * Creates a new engine. The options parameter is an object that specifies any properties you wish to override the defaults. * All properties have default values, and many are pre-calculated automatically based on other properties. @@ -51,7 +53,8 @@ var Body = require('../body/Body'); timestamp: 0, timeScale: 1, lastDelta: 0, - lastElapsed: 0 + lastElapsed: 0, + lastUpdatesPerFrame: 0 } }; @@ -89,6 +92,13 @@ var Body = require('../body/Body'); timestamp = timing.timestamp, i; + // warn if high delta + if (delta > Engine._deltaMax) { + Common.warnOnce( + 'Matter.Engine.update: delta argument is recommended to be less than or equal to', Engine._deltaMax.toFixed(3), 'ms.' + ); + } + delta = typeof delta !== 'undefined' ? delta : Common._baseDelta; delta *= timing.timeScale; diff --git a/src/core/Runner.js b/src/core/Runner.js index f827cfe6..089c8077 100644 --- a/src/core/Runner.js +++ b/src/core/Runner.js @@ -1,9 +1,9 @@ /** -* 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. +* The `Matter.Runner` module is an optional utility that provides a game loop for running a `Matter.Engine` inside a browser environment. +* A runner will continuously update a `Matter.Engine` whilst synchronising engine updates with the browser frame rate. +* This runner favours a smoother user experience over perfect time keeping. +* This runner is optional and is used for development and debugging but could be useful as a starting point for implementing some games and experiences. +* Alternatively see `Engine.update` to step the engine directly inside your own game loop implementation as may be needed inside other environments. * * See the included usage [examples](https://github.com/liabru/matter-js/tree/master/examples). * @@ -20,73 +20,59 @@ 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._maxFrameDelta = 1000 / 15; + Runner._frameDeltaFallback = 1000 / 60; + Runner._timeBufferMargin = 1.5; + Runner._elapsedNextEstimate = 1; + 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: null, + frameDeltaSmoothing: true, + frameDeltaSnapping: true, + frameDeltaHistory: [], + frameDeltaHistorySize: 100, frameRequestId: null, - isFixed: false, + timeBuffer: 0, + timeLastTick: null, + maxUpdates: null, + maxFrameTime: 1000 / 30, + 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; + // for temporary back compatibility only + runner.fps = 0; return runner; }; /** - * Continuously ticks a `Matter.Engine` by calling `Runner.tick` on the `requestAnimationFrame` event. + * Runs a `Matter.Engine` whilst synchronising engine updates with the browser frame rate. + * See module and properties descriptions for more information on this runner. + * Alternatively see `Engine.update` to step the engine directly inside your own game loop implementation. * @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._frameDeltaFallback; - (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 +83,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. + * Performs a single runner tick as used inside `Runner.run`. + * See module and properties descriptions for more information on this runner. + * Alternatively see `Engine.update` to step the engine directly inside your own game loop implementation. * @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; + + // fallback for unusable frame delta values (e.g. 0, NaN, on first frame or long pauses) + if (!frameDelta || !runner.timeLastTick || frameDelta > Math.max(Runner._maxFrameDelta, runner.maxFrameTime)) { + // reuse last accepted frame delta else fallback + frameDelta = runner.frameDelta || Runner._frameDeltaFallback; + } - // 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); + 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; + } + + if (runner.frameDeltaSnapping) { + // snap frame delta to the nearest 1 Hz + frameDelta = 1000 / Math.round(1000 / frameDelta); } - // create an event object + // 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 frame + var maxUpdates = runner.maxUpdates || Math.ceil(runner.maxFrameTime / engineDelta); + + // 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; - } + var updateStartTime = Common.now(); - Events.trigger(runner, 'tick', event); + // simulate time elapsed between calls + while (engineDelta > 0 && runner.timeBuffer >= engineDelta * Runner._timeBufferMargin) { + // 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, + elapsedTimeUpdates = Common.now() - updateStartTime, + elapsedNextEstimate = elapsedTimeTotal + Runner._elapsedNextEstimate * elapsedTimeUpdates / updateCount; - Events.trigger(runner, 'afterUpdate', event); + // defer updates if over performance budgets for this frame + if (updateCount >= maxUpdates || elapsedNextEstimate > 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. + * Alternatively 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 the `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._onNextFrame = function(runner, callback) { + if (typeof window !== 'undefined' && window.requestAnimationFrame) { + runner.frameRequestId = window.requestAnimationFrame(callback); + } else { + throw new Error('Matter.Runner: missing required global window.requestAnimationFrame.'); + } + + return runner.frameRequestId; + }; + + /** + * Cancels the last callback scheduled by `Runner._onNextFrame` on this `runner`. + * @private + * @method _cancelNextFrame + * @param {runner} runner + */ + Runner._cancelNextFrame = function(runner) { + if (typeof window !== 'undefined' && window.cancelAnimationFrame) { + window.cancelAnimationFrame(runner.frameRequestId); + } else { + throw new Error('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. */ - Runner.start = function(runner, engine) { - Runner.run(runner, engine); + 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 +276,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 +286,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 +296,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 +306,8 @@ var Common = require('./Common'); */ /** - * Fired before update + * Fired before each and every engine update in this browser frame (if any). + * There may be multiple engine update calls per browser frame (or none) depending on framerate and timestep delta. * * @event beforeUpdate * @param {} event An event object @@ -224,7 +317,8 @@ var Common = require('./Common'); */ /** - * Fired after update + * Fired after each and every engine update in this browser frame (if any). + * There may be multiple engine update calls per browser frame (or none) depending on framerate and timestep delta. * * @event afterUpdate * @param {} event An event object @@ -240,7 +334,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, known as `delta`. + * + * This value is recommended to be `1000 / 60` ms or smaller (i.e. equivalent to at least 60hz). + * + * Smaller `delta` values provide higher quality results at the cost of performance. + * + * You should usually avoid changing `delta` during running, otherwise quality may be affected. + * + * For smoother frame pacing choose a `delta` that is an even multiple of each display FPS you target, i.e. `1000 / (n * fps)` as this helps distribute an equal number of updates over each display frame. + * + * For example with a 60 Hz `delta` i.e. `1000 / 60` the runner will on average perform one update per frame on displays running 60 FPS and one update every two frames on displays running 120 FPS, etc. + * + * Where as e.g. using a 240 Hz `delta` i.e. `1000 / 240` the runner will on average perform four updates per frame on displays running 60 FPS and two updates per frame on displays running 120 FPS, etc. + * + * Therefore `Runner.run` will call multiple engine updates (or none) as needed to simulate the time elapsed between browser frames. + * + * In practice the number of updates in any particular frame may be restricted to respect the runner's performance budgets. These are specified by `runner.maxFrameTime` and `runner.maxUpdates`, 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 and events while the runner loop remains running. * * @property enabled * @type boolean @@ -248,23 +366,88 @@ 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 certain limits (see `Runner.tick` code). + * + * @private + * @property timeBuffer + * @type number + * @default 0 + */ + + /** + * The measured time elapsed between the last two browser frames measured in milliseconds. + * This is useful e.g. to estimate the current browser FPS using `1000 / runner.frameDelta`. + * + * @readonly + * @property frameDelta + * @type number + */ + + /** + * Enables averaging to smooth frame rate measurements and therefore stabilise play rate. + * + * @property frameDeltaSmoothing + * @type boolean + * @default true + */ + + /** + * Rounds measured browser frame delta to the nearest 1 Hz. + * This option can help smooth frame rate measurements and simplify handling hardware timing differences e.g. 59.94Hz and 60Hz displays. + * For best results you should also round your `runner.delta` equivalent to the nearest 1 Hz. * - * @property isFixed + * @property frameDeltaSnapping * @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. + * A performance budget that limits execution time allowed for this runner per browser frame in milliseconds. + * + * To calculate the effective browser FPS at which this throttle is applied use `1000 / runner.maxFrameTime`. + * + * This performance budget is intended to help maintain browser interactivity and help improve framerate recovery during temporary high CPU usage. + * + * This budget only covers the measured time elapsed executing the functions called in the scope of the runner tick, including `Engine.update` and its related user event callbacks. + * + * You may also reduce this budget to allow for any significant additional processing you perform on the same thread outside the scope of this runner tick, e.g. rendering time. + * + * See also `runner.maxUpdates`. * - * @property delta + * @property maxFrameTime * @type number - * @default 1000 / 60 + * @default 1000 / 30 + */ + + /** + * An optional limit for maximum engine update count allowed per frame tick in addition to `runner.maxFrameTime`. + * + * Unless you set a value it is automatically chosen based on `runner.delta` and `runner.maxFrameTime`. + * + * 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 */ })(); diff --git a/src/render/Render.js b/src/render/Render.js index 3cfd80a4..c4ef4596 100644 --- a/src/render/Render.js +++ b/src/render/Render.js @@ -1,5 +1,5 @@ /** -* The `Matter.Render` module is a simple canvas based renderer for visualising instances of `Matter.Engine`. +* The `Matter.Render` module is a lightweight, optional utility which provides a simple canvas based renderer for visualising instances of `Matter.Engine`. * It is intended for development and debugging purposes, but may also be suitable for simple games. * It includes a number of drawing options including wireframe, vector with support for sprites and viewports. * @@ -61,6 +61,7 @@ var Mouse = require('../core/Mouse'); timestampElapsedHistory: [], engineDeltaHistory: [], engineElapsedHistory: [], + engineUpdatesHistory: [], elapsedHistory: [] }, options: { @@ -552,15 +553,19 @@ var Mouse = require('../core/Mouse'); elapsedHistory = timing.elapsedHistory, timestampElapsedHistory = timing.timestampElapsedHistory, engineDeltaHistory = timing.engineDeltaHistory, + engineUpdatesHistory = timing.engineUpdatesHistory, engineElapsedHistory = timing.engineElapsedHistory, + lastEngineUpdatesPerFrame = engine.timing.lastUpdatesPerFrame, lastEngineDelta = engine.timing.lastDelta; var deltaMean = _mean(deltaHistory), elapsedMean = _mean(elapsedHistory), engineDeltaMean = _mean(engineDeltaHistory), + engineUpdatesMean = _mean(engineUpdatesHistory), engineElapsedMean = _mean(engineElapsedHistory), timestampElapsedMean = _mean(timestampElapsedHistory), rateMean = (timestampElapsedMean / deltaMean) || 0, + neededUpdatesPerFrame = Math.round(deltaMean / lastEngineDelta), fps = (1000 / deltaMean) || 0; var graphHeight = 4, @@ -572,7 +577,7 @@ var Mouse = require('../core/Mouse'); // background context.fillStyle = '#0e0f19'; - context.fillRect(0, 50, gap * 4 + width * 5 + 22, height); + context.fillRect(0, 50, gap * 5 + width * 6 + 22, height); // show FPS Render.status( @@ -590,17 +595,25 @@ var Mouse = require('../core/Mouse'); function(i) { return (engineDeltaHistory[i] / engineDeltaMean) - 1; } ); + // show engine updates per frame + Render.status( + context, x + (gap + width) * 2, y, width, graphHeight, engineUpdatesHistory.length, + lastEngineUpdatesPerFrame + ' upf', + Math.pow(Common.clamp((engineUpdatesMean / neededUpdatesPerFrame) || 1, 0, 1), 4), + function(i) { return (engineUpdatesHistory[i] / engineUpdatesMean) - 1; } + ); + // show engine update time Render.status( - context, x + (gap + width) * 2, y, width, graphHeight, engineElapsedHistory.length, + context, x + (gap + width) * 3, y, width, graphHeight, engineElapsedHistory.length, engineElapsedMean.toFixed(2) + ' ut', - 1 - (engineElapsedMean / Render._goodFps), + 1 - (lastEngineUpdatesPerFrame * engineElapsedMean / Render._goodFps), function(i) { return (engineElapsedHistory[i] / engineElapsedMean) - 1; } ); // show render time Render.status( - context, x + (gap + width) * 3, y, width, graphHeight, elapsedHistory.length, + context, x + (gap + width) * 4, y, width, graphHeight, elapsedHistory.length, elapsedMean.toFixed(2) + ' rt', 1 - (elapsedMean / Render._goodFps), function(i) { return (elapsedHistory[i] / elapsedMean) - 1; } @@ -608,7 +621,7 @@ var Mouse = require('../core/Mouse'); // show effective speed Render.status( - context, x + (gap + width) * 4, y, width, graphHeight, timestampElapsedHistory.length, + context, x + (gap + width) * 5, y, width, graphHeight, timestampElapsedHistory.length, rateMean.toFixed(2) + ' x', rateMean * rateMean * rateMean, function(i) { return (((timestampElapsedHistory[i] / deltaHistory[i]) / rateMean) || 0) - 1; } @@ -1433,6 +1446,9 @@ var Mouse = require('../core/Mouse'); timing.timestampElapsedHistory.unshift(timing.timestampElapsed); timing.timestampElapsedHistory.length = Math.min(timing.timestampElapsedHistory.length, historySize); + timing.engineUpdatesHistory.unshift(engine.timing.lastUpdatesPerFrame); + timing.engineUpdatesHistory.length = Math.min(timing.engineUpdatesHistory.length, historySize); + timing.engineElapsedHistory.unshift(engine.timing.lastElapsed); timing.engineElapsedHistory.length = Math.min(timing.engineElapsedHistory.length, historySize); @@ -1722,6 +1738,7 @@ var Mouse = require('../core/Mouse'); * * - average render frequency (e.g. 60 fps) * - exact engine delta time used for last update (e.g. 16.66ms) + * - average updates per frame (e.g. 1) * - average engine execution duration (e.g. 5.00ms) * - average render execution duration (e.g. 0.40ms) * - average effective play speed (e.g. '1.00x' is 'real-time')