Show:

File: src/core/Runner.js

                                /**
                                * 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
                                     */
                                
                                })();