/**
* 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).
*
* @class Runner
*/
var Runner = {};
module.exports = Runner;
var Events = require('./Events');
var Engine = require('./Engine');
var Common = require('./Common');
(function() {
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.
* 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 = {
delta: 1000 / 60,
frameDelta: null,
frameDeltaSmoothing: true,
frameDeltaSnapping: true,
frameDeltaHistory: [],
frameDeltaHistorySize: 100,
frameRequestId: null,
timeBuffer: 0,
timeLastTick: null,
maxUpdates: null,
maxFrameTime: 1000 / 30,
lastUpdatesDeferred: 0,
enabled: true
};
var runner = Common.extend(defaults, options);
// for temporary back compatibility only
runner.fps = 0;
return runner;
};
/**
* 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 {runner} runner
* @param {engine} [engine]
* @return {runner} runner
*/
Runner.run = function(runner, engine) {
// initial time buffer for the first frame
runner.timeBuffer = Runner._frameDeltaFallback;
(function onFrame(time){
runner.frameRequestId = Runner._onNextFrame(runner, onFrame);
if (time && runner.enabled) {
Runner.tick(runner, engine, time);
}
})();
return runner;
};
/**
* 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 tickStartTime = Common.now(),
engineDelta = runner.delta,
updateCount = 0;
// 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;
}
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 = {
timestamp: engine.timing.timestamp
};
// tick events before update
Events.trigger(runner, 'beforeTick', event);
Events.trigger(runner, 'tick', event);
var updateStartTime = Common.now();
// 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);
// 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);
// 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 frame loop.
* Alternatively to temporarily pause the runner, see `runner.enabled`.
* @method stop
* @param {runner} runner
*/
Runner.stop = function(runner) {
Runner._cancelNextFrame(runner);
};
/**
* Schedules the `callback` on this `runner` for the next animation frame.
* @private
* @method _onNextFrame
* @param {runner} runner
* @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.
*/
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;
};
/*
*
* Events Documentation
*
*/
/**
* Fired once at the start of the browser frame, before any engine updates.
*
* @event beforeTick
* @param {} event An event object
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
*/
/**
* Fired once at the start of the browser frame, after `beforeTick`.
*
* @event tick
* @param {} event An event object
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
*/
/**
* Fired once at the end of the browser frame, after `beforeTick`, `tick` and after any engine updates.
*
* @event afterTick
* @param {} event An event object
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
*/
/**
* 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
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
*/
/**
* 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
* @param {number} event.timestamp The engine.timing.timestamp of the event
* @param {} event.source The source object of the event
* @param {} event.name The name of the event
*/
/*
*
* Properties Documentation
*
*/
/**
* 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
* @default true
*/
/**
* 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 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
*/
})();