0
0
Fork 0
mirror of https://github.com/liabru/matter-js.git synced 2025-01-15 16:38:43 -05:00

Merge pull request #1254 from liabru/runner-2

Improve Matter.Runner
This commit is contained in:
liabru 2024-06-22 19:19:36 +01:00 committed by GitHub
commit e0e3164541
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 471 additions and 127 deletions

View file

@ -116,7 +116,6 @@ var compare = function(examples, isDev) {
ticks += 1; ticks += 1;
var demoBuildInstance = demoBuild.example.instance; var demoBuildInstance = demoBuild.example.instance;
runner.isFixed = demoBuildInstance.runner.isFixed = true;
runner.delta = demoBuildInstance.runner.delta = 1000 / 60; runner.delta = demoBuildInstance.runner.delta = 1000 / 60;
window.Matter = MatterBuild; window.Matter = MatterBuild;

View file

@ -41,6 +41,22 @@ var demo = function(examples, isDev) {
document.title = 'Matter.js Demo' + (isDev ? ' ・ Dev' : ''); document.title = 'Matter.js Demo' + (isDev ? ' ・ Dev' : '');
if (isDev) { 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 // add compare button
var buttonSource = demo.dom.buttonSource, var buttonSource = demo.dom.buttonSource,
buttonCompare = buttonSource.cloneNode(true); buttonCompare = buttonSource.cloneNode(true);

View file

@ -41,6 +41,7 @@ module.exports = {
stress2: require('./stress2.js'), stress2: require('./stress2.js'),
stress3: require('./stress3.js'), stress3: require('./stress3.js'),
stress4: require('./stress4.js'), stress4: require('./stress4.js'),
substep: require('./substep.js'),
svg: require('./svg.js'), svg: require('./svg.js'),
terrain: require('./terrain.js'), terrain: require('./terrain.js'),
timescale: require('./timescale.js'), timescale: require('./timescale.js'),

View file

@ -34,9 +34,7 @@ Example.stress3 = function() {
Render.run(render); Render.run(render);
// create runner // create runner
var runner = Runner.create({ var runner = Runner.create();
isFixed: true
});
Runner.run(runner, engine); Runner.run(runner, engine);
// add bodies // add bodies

View file

@ -35,9 +35,7 @@ Example.stress4 = function() {
Render.run(render); Render.run(render);
// create runner // create runner
var runner = Runner.create({ var runner = Runner.create();
isFixed: true
});
Runner.run(runner, engine); Runner.run(runner, engine);

122
examples/substep.js Normal file
View file

@ -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;
}

View file

@ -24,6 +24,8 @@ var Body = require('../body/Body');
(function() { (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. * 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. * 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, timestamp: 0,
timeScale: 1, timeScale: 1,
lastDelta: 0, lastDelta: 0,
lastElapsed: 0 lastElapsed: 0,
lastUpdatesPerFrame: 0
} }
}; };
@ -89,6 +92,13 @@ var Body = require('../body/Body');
timestamp = timing.timestamp, timestamp = timing.timestamp,
i; 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 = typeof delta !== 'undefined' ? delta : Common._baseDelta;
delta *= timing.timeScale; delta *= timing.timeScale;

View file

@ -1,9 +1,9 @@
/** /**
* The `Matter.Runner` module is an optional utility which provides a game loop, * The `Matter.Runner` module is an optional utility that provides a game loop for running a `Matter.Engine` inside a browser environment.
* that handles continuously updating a `Matter.Engine` for you within a browser. * A runner will continuously update a `Matter.Engine` whilst synchronising engine updates with the browser frame rate.
* It is intended for development and debugging purposes, but may also be suitable for simple games. * This runner favours a smoother user experience over perfect time keeping.
* If you are using your own game loop instead, then you do not need the `Matter.Runner` module. * 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.
* Instead just call `Engine.update(engine, delta)` in your own loop. * 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). * See the included usage [examples](https://github.com/liabru/matter-js/tree/master/examples).
* *
@ -20,73 +20,59 @@ var Common = require('./Common');
(function() { (function() {
var _requestAnimationFrame, Runner._maxFrameDelta = 1000 / 15;
_cancelAnimationFrame; Runner._frameDeltaFallback = 1000 / 60;
Runner._timeBufferMargin = 1.5;
if (typeof window !== 'undefined') { Runner._elapsedNextEstimate = 1;
_requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame Runner._smoothingLowerBound = 0.1;
|| window.mozRequestAnimationFrame || window.msRequestAnimationFrame; Runner._smoothingUpperBound = 0.9;
_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);
};
}
/** /**
* 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 * @method create
* @param {} options * @param {} options
*/ */
Runner.create = function(options) { Runner.create = function(options) {
var defaults = { var defaults = {
fps: 60, delta: 1000 / 60,
deltaSampleSize: 60, frameDelta: null,
counterTimestamp: 0, frameDeltaSmoothing: true,
frameCounter: 0, frameDeltaSnapping: true,
deltaHistory: [], frameDeltaHistory: [],
timePrev: null, frameDeltaHistorySize: 100,
frameRequestId: null, frameRequestId: null,
isFixed: false, timeBuffer: 0,
timeLastTick: null,
maxUpdates: null,
maxFrameTime: 1000 / 30,
lastUpdatesDeferred: 0,
enabled: true enabled: true
}; };
var runner = Common.extend(defaults, options); var runner = Common.extend(defaults, options);
runner.delta = runner.delta || 1000 / runner.fps; // for temporary back compatibility only
runner.deltaMin = runner.deltaMin || 1000 / runner.fps; runner.fps = 0;
runner.deltaMax = runner.deltaMax || 1000 / (runner.fps * 0.5);
runner.fps = 1000 / runner.delta;
return runner; 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 * @method run
* @param {engine} engine * @param {runner} runner
* @param {engine} [engine]
* @return {runner} runner
*/ */
Runner.run = function(runner, engine) { Runner.run = function(runner, engine) {
// create runner if engine is first argument // initial time buffer for the first frame
if (typeof runner.positionIterations !== 'undefined') { runner.timeBuffer = Runner._frameDeltaFallback;
engine = runner;
runner = Runner.create();
}
(function run(time){ (function onFrame(time){
runner.frameRequestId = _requestAnimationFrame(run); runner.frameRequestId = Runner._onNextFrame(runner, onFrame);
if (time && runner.enabled) { if (time && runner.enabled) {
Runner.tick(runner, engine, time); 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'). * Performs a single runner tick as used inside `Runner.run`.
* Features delta smoothing, time correction and fixed or dynamic timing. * See module and properties descriptions for more information on this runner.
* Consider just `Engine.update(engine, delta)` if you're using your own loop. * Alternatively see `Engine.update` to step the engine directly inside your own game loop implementation.
* @method tick * @method tick
* @param {runner} runner * @param {runner} runner
* @param {engine} engine * @param {engine} engine
* @param {number} time * @param {number} time
*/ */
Runner.tick = function(runner, engine, time) { Runner.tick = function(runner, engine, time) {
var timing = engine.timing, var tickStartTime = Common.now(),
delta; engineDelta = runner.delta,
updateCount = 0;
if (runner.isFixed) { // find frame delta time since last call
// fixed timestep var frameDelta = time - runner.timeLastTick;
delta = runner.delta;
} else {
// dynamic timestep based on wall clock between calls
delta = (time - runner.timePrev) || runner.delta;
runner.timePrev = time;
// optimistically filter delta over a few frames, to improve stability // fallback for unusable frame delta values (e.g. 0, NaN, on first frame or long pauses)
runner.deltaHistory.push(delta); if (!frameDelta || !runner.timeLastTick || frameDelta > Math.max(Runner._maxFrameDelta, runner.maxFrameTime)) {
runner.deltaHistory = runner.deltaHistory.slice(-runner.deltaSampleSize); // reuse last accepted frame delta else fallback
delta = Math.min.apply(null, runner.deltaHistory); frameDelta = runner.frameDelta || Runner._frameDeltaFallback;
// limit delta
delta = delta < runner.deltaMin ? runner.deltaMin : delta;
delta = delta > runner.deltaMax ? runner.deltaMax : delta;
// update engine timing object
runner.delta = delta;
} }
// create an event object if (runner.frameDeltaSmoothing) {
// record frame delta over a number of frames
runner.frameDeltaHistory.push(frameDelta);
runner.frameDeltaHistory = runner.frameDeltaHistory.slice(-runner.frameDeltaHistorySize);
// sort frame delta history
var deltaHistorySorted = runner.frameDeltaHistory.slice(0).sort();
// 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);
}
// 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 = { var event = {
timestamp: timing.timestamp timestamp: engine.timing.timestamp
}; };
// tick events before update
Events.trigger(runner, 'beforeTick', event); Events.trigger(runner, 'beforeTick', 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;
}
Events.trigger(runner, 'tick', event); Events.trigger(runner, 'tick', event);
// update var updateStartTime = Common.now();
Events.trigger(runner, 'beforeUpdate', event);
Engine.update(engine, delta); // 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);
Events.trigger(runner, 'afterUpdate', event); // consume time simulated from buffer
runner.timeBuffer -= engineDelta;
updateCount += 1;
// find elapsed time during this tick
var elapsedTimeTotal = Common.now() - tickStartTime,
elapsedTimeUpdates = Common.now() - updateStartTime,
elapsedNextEstimate = elapsedTimeTotal + Runner._elapsedNextEstimate * elapsedTimeUpdates / updateCount;
// 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); 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. * Ends execution of `Runner.run` on the given `runner` by canceling the frame loop.
* If you wish to only temporarily pause the runner, see `runner.enabled` instead. * Alternatively to temporarily pause the runner, see `runner.enabled`.
* @method stop * @method stop
* @param {runner} runner * @param {runner} runner
*/ */
Runner.stop = function(runner) { Runner.stop = function(runner) {
_cancelAnimationFrame(runner.frameRequestId); Runner._cancelNextFrame(runner);
}; };
/** /**
* Alias for `Runner.run`. * Schedules the `callback` on this `runner` for the next animation frame.
* @method start * @private
* @method _onNextFrame
* @param {runner} runner * @param {runner} runner
* @param {engine} engine * @param {function} callback
* @return {number} frameRequestId
*/ */
Runner.start = function(runner, engine) { Runner._onNextFrame = function(runner, callback) {
Runner.run(runner, engine); 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.
*/
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 * @event beforeTick
* @param {} event An event object * @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 * @event tick
* @param {} event An event object * @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 * @event afterTick
* @param {} event An event object * @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 * @event beforeUpdate
* @param {} event An event object * @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 * @event afterUpdate
* @param {} event An event object * @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 * @property enabled
* @type boolean * @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). * The accumulated time elapsed that has yet to be simulated in milliseconds.
* If timing is fixed, then the apparent simulation speed will change depending on the frame rate (but behaviour will be deterministic). * This value is clamped within certain limits (see `Runner.tick` code).
* If the timing is variable, then the apparent simulation speed will be constant (approximately, but at the cost of determininism).
* *
* @property isFixed * @private
* @type boolean * @property timeBuffer
* @default false * @type number
* @default 0
*/ */
/** /**
* A `Number` that specifies the time step between updates in milliseconds. * The measured time elapsed between the last two browser frames measured in milliseconds.
* If `engine.timing.isFixed` is set to `true`, then `delta` is fixed. * This is useful e.g. to estimate the current browser FPS using `1000 / runner.frameDelta`.
* If it is `false`, then `delta` can dynamically change to maintain the correct apparent simulation speed.
* *
* @property delta * @readonly
* @property frameDelta
* @type number * @type number
* @default 1000 / 60 */
/**
* 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 frameDeltaSnapping
* @type boolean
* @default true
*/
/**
* 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 maxFrameTime
* @type number
* @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
*/ */
})(); })();

View file

@ -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 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. * 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: [], timestampElapsedHistory: [],
engineDeltaHistory: [], engineDeltaHistory: [],
engineElapsedHistory: [], engineElapsedHistory: [],
engineUpdatesHistory: [],
elapsedHistory: [] elapsedHistory: []
}, },
options: { options: {
@ -552,15 +553,19 @@ var Mouse = require('../core/Mouse');
elapsedHistory = timing.elapsedHistory, elapsedHistory = timing.elapsedHistory,
timestampElapsedHistory = timing.timestampElapsedHistory, timestampElapsedHistory = timing.timestampElapsedHistory,
engineDeltaHistory = timing.engineDeltaHistory, engineDeltaHistory = timing.engineDeltaHistory,
engineUpdatesHistory = timing.engineUpdatesHistory,
engineElapsedHistory = timing.engineElapsedHistory, engineElapsedHistory = timing.engineElapsedHistory,
lastEngineUpdatesPerFrame = engine.timing.lastUpdatesPerFrame,
lastEngineDelta = engine.timing.lastDelta; lastEngineDelta = engine.timing.lastDelta;
var deltaMean = _mean(deltaHistory), var deltaMean = _mean(deltaHistory),
elapsedMean = _mean(elapsedHistory), elapsedMean = _mean(elapsedHistory),
engineDeltaMean = _mean(engineDeltaHistory), engineDeltaMean = _mean(engineDeltaHistory),
engineUpdatesMean = _mean(engineUpdatesHistory),
engineElapsedMean = _mean(engineElapsedHistory), engineElapsedMean = _mean(engineElapsedHistory),
timestampElapsedMean = _mean(timestampElapsedHistory), timestampElapsedMean = _mean(timestampElapsedHistory),
rateMean = (timestampElapsedMean / deltaMean) || 0, rateMean = (timestampElapsedMean / deltaMean) || 0,
neededUpdatesPerFrame = Math.round(deltaMean / lastEngineDelta),
fps = (1000 / deltaMean) || 0; fps = (1000 / deltaMean) || 0;
var graphHeight = 4, var graphHeight = 4,
@ -572,7 +577,7 @@ var Mouse = require('../core/Mouse');
// background // background
context.fillStyle = '#0e0f19'; 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 // show FPS
Render.status( Render.status(
@ -590,17 +595,25 @@ var Mouse = require('../core/Mouse');
function(i) { return (engineDeltaHistory[i] / engineDeltaMean) - 1; } 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 // show engine update time
Render.status( 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', engineElapsedMean.toFixed(2) + ' ut',
1 - (engineElapsedMean / Render._goodFps), 1 - (lastEngineUpdatesPerFrame * engineElapsedMean / Render._goodFps),
function(i) { return (engineElapsedHistory[i] / engineElapsedMean) - 1; } function(i) { return (engineElapsedHistory[i] / engineElapsedMean) - 1; }
); );
// show render time // show render time
Render.status( 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', elapsedMean.toFixed(2) + ' rt',
1 - (elapsedMean / Render._goodFps), 1 - (elapsedMean / Render._goodFps),
function(i) { return (elapsedHistory[i] / elapsedMean) - 1; } function(i) { return (elapsedHistory[i] / elapsedMean) - 1; }
@ -608,7 +621,7 @@ var Mouse = require('../core/Mouse');
// show effective speed // show effective speed
Render.status( 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.toFixed(2) + ' x',
rateMean * rateMean * rateMean, rateMean * rateMean * rateMean,
function(i) { return (((timestampElapsedHistory[i] / deltaHistory[i]) / rateMean) || 0) - 1; } 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.unshift(timing.timestampElapsed);
timing.timestampElapsedHistory.length = Math.min(timing.timestampElapsedHistory.length, historySize); 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.unshift(engine.timing.lastElapsed);
timing.engineElapsedHistory.length = Math.min(timing.engineElapsedHistory.length, historySize); 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) * - average render frequency (e.g. 60 fps)
* - exact engine delta time used for last update (e.g. 16.66ms) * - 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 engine execution duration (e.g. 5.00ms)
* - average render execution duration (e.g. 0.40ms) * - average render execution duration (e.g. 0.40ms)
* - average effective play speed (e.g. '1.00x' is 'real-time') * - average effective play speed (e.g. '1.00x' is 'real-time')