mirror of
https://github.com/schibo/1964js.git
synced 2025-04-02 10:52:54 -04:00
756 lines
No EOL
35 KiB
JavaScript
756 lines
No EOL
35 KiB
JavaScript
/*
|
|
* A main loop useful for games and other animated applications.
|
|
*/
|
|
(function(root) {
|
|
|
|
// The amount of time (in milliseconds) to simulate each time update()
|
|
// runs. See `MainLoop.setSimulationTimestep()` for details.
|
|
var simulationTimestep = 1000 / 60,
|
|
|
|
// The cumulative amount of in-app time that hasn't been simulated yet.
|
|
// See the comments inside animate() for details.
|
|
frameDelta = 0,
|
|
|
|
// The timestamp in milliseconds of the last time the main loop was run.
|
|
// Used to compute the time elapsed between frames.
|
|
lastFrameTimeMs = 0,
|
|
|
|
// An exponential moving average of the frames per second.
|
|
fps = 60,
|
|
|
|
// A factor that affects how heavily to weight more recent seconds'
|
|
// performance when calculating the average frames per second. Valid values
|
|
// range from zero to one inclusive. Higher values result in weighting more
|
|
// recent seconds more heavily.
|
|
fpsAlpha = 0.9,
|
|
|
|
// The minimum duration between updates to the frames-per-second estimate.
|
|
// Higher values increase accuracy, but result in slower updates.
|
|
fpsUpdateInterval = 1000,
|
|
|
|
// The timestamp (in milliseconds) of the last time the `fps` moving
|
|
// average was updated.
|
|
lastFpsUpdate = 0,
|
|
|
|
// The number of frames delivered since the last time the `fps` moving
|
|
// average was updated (i.e. since `lastFpsUpdate`).
|
|
framesSinceLastFpsUpdate = 0,
|
|
|
|
// The number of times update() is called in a given frame. This is only
|
|
// relevant inside of animate(), but a reference is held externally so that
|
|
// this variable is not marked for garbage collection every time the main
|
|
// loop runs.
|
|
numUpdateSteps = 0,
|
|
|
|
// The minimum amount of time in milliseconds that must pass since the last
|
|
// frame was executed before another frame can be executed. The
|
|
// multiplicative inverse caps the FPS (the default of zero means there is
|
|
// no cap).
|
|
minFrameDelay = 0,
|
|
|
|
// Whether the main loop is running.
|
|
running = false,
|
|
|
|
// `true` if `MainLoop.start()` has been called and the most recent time it
|
|
// was called has not been followed by a call to `MainLoop.stop()`. This is
|
|
// different than `running` because there is a delay of a few milliseconds
|
|
// after `MainLoop.start()` is called before the application is considered
|
|
// "running." This delay is due to waiting for the next frame.
|
|
started = false,
|
|
|
|
// Whether the simulation has fallen too far behind real time.
|
|
// Specifically, `panic` will be set to `true` if too many updates occur in
|
|
// one frame. This is only relevant inside of animate(), but a reference is
|
|
// held externally so that this variable is not marked for garbage
|
|
// collection every time the main loop runs.
|
|
panic = false,
|
|
|
|
// The object most likely to have `requestAnimationFrame` attached is
|
|
// `window`, if it's available in this environment. Otherwise, fall back to
|
|
// the root context.
|
|
windowOrRoot = typeof window === 'object' ? window : root,
|
|
|
|
// The function that runs the main loop. The unprefixed version of
|
|
// `window.requestAnimationFrame()` is available in all modern browsers
|
|
// now, but node.js doesn't have it, so fall back to timers. The polyfill
|
|
// is adapted from the MIT-licensed
|
|
// https://github.com/underscorediscovery/realtime-multiplayer-in-html5
|
|
requestAnimationFrame = windowOrRoot.requestAnimationFrame || (function() {
|
|
var lastTimestamp = Date.now(),
|
|
now,
|
|
timeout;
|
|
return function(callback) {
|
|
now = Date.now();
|
|
// The next frame should run no sooner than the simulation allows,
|
|
// but as soon as possible if the current frame has already taken
|
|
// more time to run than is simulated in one timestep.
|
|
timeout = Math.max(0, simulationTimestep - (now - lastTimestamp));
|
|
lastTimestamp = now + timeout;
|
|
return setTimeout(function() {
|
|
callback(now + timeout);
|
|
}, timeout);
|
|
};
|
|
})(),
|
|
|
|
// The function that stops the main loop. The unprefixed version of
|
|
// `window.cancelAnimationFrame()` is available in all modern browsers now,
|
|
// but node.js doesn't have it, so fall back to timers.
|
|
cancelAnimationFrame = windowOrRoot.cancelAnimationFrame || clearTimeout,
|
|
|
|
// In all major browsers, replacing non-specified functions with NOOPs
|
|
// seems to be as fast or slightly faster than using conditions to only
|
|
// call the functions if they are specified. This is probably due to empty
|
|
// functions being optimized away. http://jsperf.com/noop-vs-condition
|
|
NOOP = function() {},
|
|
|
|
// A function that runs at the beginning of the main loop.
|
|
// See `MainLoop.setBegin()` for details.
|
|
begin = NOOP,
|
|
|
|
// A function that runs updates (i.e. AI and physics).
|
|
// See `MainLoop.setUpdate()` for details.
|
|
update = NOOP,
|
|
|
|
// A function that draws things on the screen.
|
|
// See `MainLoop.setDraw()` for details.
|
|
draw = NOOP,
|
|
|
|
// A function that runs at the end of the main loop.
|
|
// See `MainLoop.setEnd()` for details.
|
|
end = NOOP,
|
|
|
|
// The ID of the currently executing frame. Used to cancel frames when
|
|
// stopping the loop.
|
|
rafHandle;
|
|
|
|
/**
|
|
* Manages the main loop that runs updates and rendering.
|
|
*
|
|
* The main loop is a core part of any application in which state changes
|
|
* even if no events are handled. In games, it is typically responsible for
|
|
* computing physics and AI as well as drawing the result on the screen.
|
|
*
|
|
* The body of this particular loop is run every time the browser is ready to
|
|
* paint another frame. The frequency with which this happens depends primarily
|
|
* on the monitor's refresh rate, which is typically 60 frames per second. Most
|
|
* applications aim to run at 60 FPS for this reason, meaning that the main
|
|
* loop runs about once every 16.7 milliseconds. With this target, everything
|
|
* that happens in the main loop (e.g. all updates and drawing) needs to occur
|
|
* within the "budget" of 16.7 milliseconds. See
|
|
* `MainLoop.setSimulationTimestep()` for more information about typical
|
|
* monitor refresh rates and frame rate targets.
|
|
*
|
|
* The main loop can be started and stopped, but there can only be one MainLoop
|
|
* (except that each Web Worker can have its own MainLoop). There are four main
|
|
* parts of the loop: {@link #setBegin begin}(), {@link #setUpdate update}(),
|
|
* {@link #setDraw draw}(), and {@link #setEnd end}(), in that order. See the
|
|
* functions that set each of them for descriptions of what they are used for.
|
|
* Note that update() can run zero or more times per loop.
|
|
*
|
|
* @class MainLoop
|
|
*/
|
|
root.MainLoop = {
|
|
/**
|
|
* Gets how many milliseconds should be simulated by every run of update().
|
|
*
|
|
* See `MainLoop.setSimulationTimestep()` for details on this value.
|
|
*
|
|
* @return {Number}
|
|
* The number of milliseconds that should be simulated by every run of
|
|
* {@link #setUpdate update}().
|
|
*/
|
|
getSimulationTimestep: function() {
|
|
return simulationTimestep;
|
|
},
|
|
|
|
/**
|
|
* Sets how many milliseconds should be simulated by every run of update().
|
|
*
|
|
* The perceived frames per second (FPS) is effectively capped at the
|
|
* multiplicative inverse of the simulation timestep. That is, if the
|
|
* timestep is 1000 / 60 (which is the default), then the maximum perceived
|
|
* FPS is effectively 60. Decreasing the timestep increases the maximum
|
|
* perceived FPS at the cost of running {@link #setUpdate update}() more
|
|
* times per frame at lower frame rates. Since running update() more times
|
|
* takes more time to process, this can actually slow down the frame rate.
|
|
* Additionally, if the amount of time it takes to run update() exceeds or
|
|
* very nearly exceeds the timestep, the application will freeze and crash
|
|
* in a spiral of death (unless it is rescued; see `MainLoop.setEnd()` for
|
|
* an explanation of what can be done if a spiral of death is occurring).
|
|
*
|
|
* The exception to this is that interpolating between updates for each
|
|
* render can increase the perceived frame rate and reduce visual
|
|
* stuttering. See `MainLoop.setDraw()` for an explanation of how to do
|
|
* this.
|
|
*
|
|
* If you are considering decreasing the simulation timestep in order to
|
|
* raise the maximum perceived FPS, keep in mind that most monitors can't
|
|
* display more than 60 FPS. Whether humans can tell the difference among
|
|
* high frame rates depends on the application, but for reference, film is
|
|
* usually displayed at 24 FPS, other videos at 30 FPS, most games are
|
|
* acceptable above 30 FPS, and virtual reality might require 75 FPS to
|
|
* feel natural. Some gaming monitors go up to 144 FPS. Setting the
|
|
* timestep below 1000 / 144 is discouraged and below 1000 / 240 is
|
|
* strongly discouraged. The default of 1000 / 60 is good in most cases.
|
|
*
|
|
* The simulation timestep should typically only be changed at
|
|
* deterministic times (e.g. before the main loop starts for the first
|
|
* time, and not in response to user input or slow frame rates) to avoid
|
|
* introducing non-deterministic behavior. The update timestep should be
|
|
* the same for all players/users in multiplayer/multi-user applications.
|
|
*
|
|
* See also `MainLoop.getSimulationTimestep()`.
|
|
*
|
|
* @param {Number} timestep
|
|
* The number of milliseconds that should be simulated by every run of
|
|
* {@link #setUpdate update}().
|
|
*/
|
|
setSimulationTimestep: function(timestep) {
|
|
simulationTimestep = timestep;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns the exponential moving average of the frames per second.
|
|
*
|
|
* @return {Number}
|
|
* The exponential moving average of the frames per second.
|
|
*/
|
|
getFPS: function() {
|
|
return fps;
|
|
},
|
|
|
|
/**
|
|
* Gets the maximum frame rate.
|
|
*
|
|
* Other factors also limit the FPS; see `MainLoop.setSimulationTimestep`
|
|
* for details.
|
|
*
|
|
* See also `MainLoop.setMaxAllowedFPS()`.
|
|
*
|
|
* @return {Number}
|
|
* The maximum number of frames per second allowed.
|
|
*/
|
|
getMaxAllowedFPS: function() {
|
|
return 1000 / minFrameDelay;
|
|
},
|
|
|
|
/**
|
|
* Sets a maximum frame rate.
|
|
*
|
|
* See also `MainLoop.getMaxAllowedFPS()`.
|
|
*
|
|
* @param {Number} [fps=Infinity]
|
|
* The maximum number of frames per second to execute. If Infinity or not
|
|
* passed, there will be no FPS cap (although other factors do limit the
|
|
* FPS; see `MainLoop.setSimulationTimestep` for details). If zero, this
|
|
* will stop the loop, and when the loop is next started, it will return
|
|
* to the previous maximum frame rate. Passing negative values will stall
|
|
* the loop until this function is called again with a positive value.
|
|
*
|
|
* @chainable
|
|
*/
|
|
setMaxAllowedFPS: function(fps) {
|
|
if (typeof fps === 'undefined') {
|
|
fps = Infinity;
|
|
}
|
|
if (fps === 0) {
|
|
this.stop();
|
|
}
|
|
else {
|
|
// Dividing by Infinity returns zero.
|
|
minFrameDelay = 1000 / fps;
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Reset the amount of time that has not yet been simulated to zero.
|
|
*
|
|
* This introduces non-deterministic behavior if called after the
|
|
* application has started running (unless it is being reset, in which case
|
|
* it doesn't matter). However, this can be useful in cases where the
|
|
* amount of time that has not yet been simulated has grown very large
|
|
* (for example, when the application's tab gets put in the background and
|
|
* the browser throttles the timers as a result). In applications with
|
|
* lockstep the player would get dropped, but in other networked
|
|
* applications it may be necessary to snap or ease the player/user to the
|
|
* authoritative state and discard pending updates in the process. In
|
|
* non-networked applications it may also be acceptable to simply resume
|
|
* the application where it last left off and ignore the accumulated
|
|
* unsimulated time.
|
|
*
|
|
* @return {Number}
|
|
* The cumulative amount of elapsed time in milliseconds that has not yet
|
|
* been simulated, but is being discarded as a result of calling this
|
|
* function.
|
|
*/
|
|
resetFrameDelta: function() {
|
|
var oldFrameDelta = frameDelta;
|
|
frameDelta = 0;
|
|
return oldFrameDelta;
|
|
},
|
|
|
|
/**
|
|
* Sets the function that runs at the beginning of the main loop.
|
|
*
|
|
* The begin() function is typically used to process input before the
|
|
* updates run. Processing input here (in chunks) can reduce the running
|
|
* time of event handlers, which is useful because long-running event
|
|
* handlers can sometimes delay frames.
|
|
*
|
|
* Unlike {@link #setUpdate update}(), which can run zero or more times per
|
|
* frame, begin() always runs exactly once per frame. This makes it useful
|
|
* for any updates that are not dependent on time in the simulation.
|
|
* Examples include adjusting HUD calculations or performing long-running
|
|
* updates incrementally. Compared to {@link #setEnd end}(), generally
|
|
* actions should occur in begin() if they affect anything that
|
|
* {@link #setUpdate update}() or {@link #setDraw draw}() use.
|
|
*
|
|
* @param {Function} begin
|
|
* The begin() function.
|
|
* @param {Number} [begin.timestamp]
|
|
* The current timestamp (when the frame started), in milliseconds. This
|
|
* should only be used for comparison to other timestamps because the
|
|
* epoch (i.e. the "zero" time) depends on the engine running this code.
|
|
* In engines that support `DOMHighResTimeStamp` (all modern browsers
|
|
* except iOS Safari 8) the epoch is the time the page started loading,
|
|
* specifically `performance.timing.navigationStart`. Everywhere else,
|
|
* including node.js, the epoch is the Unix epoch (1970-01-01T00:00:00Z).
|
|
* @param {Number} [begin.delta]
|
|
* The total elapsed time that has not yet been simulated, in
|
|
* milliseconds.
|
|
*/
|
|
setBegin: function(fun) {
|
|
begin = fun || begin;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the function that runs updates (e.g. AI and physics).
|
|
*
|
|
* The update() function should simulate anything that is affected by time.
|
|
* It can be called zero or more times per frame depending on the frame
|
|
* rate.
|
|
*
|
|
* As with everything in the main loop, the running time of update()
|
|
* directly affects the frame rate. If update() takes long enough that the
|
|
* frame rate drops below the target ("budgeted") frame rate, parts of the
|
|
* update() function that do not need to execute between every frame can be
|
|
* moved into Web Workers. (Various sources on the internet sometimes
|
|
* suggest other scheduling patterns using setTimeout() or setInterval().
|
|
* These approaches sometimes offer modest improvements with minimal
|
|
* changes to existing code, but because JavaScript is single-threaded, the
|
|
* updates will still block rendering and drag down the frame rate. Web
|
|
* Workers execute in separate threads, so they free up more time in the
|
|
* main loop.)
|
|
*
|
|
* This script can be imported into a Web Worker using importScripts() and
|
|
* used to run a second main loop in the worker. Some considerations:
|
|
*
|
|
* - Profile your code before doing the work to move it into Web Workers.
|
|
* It could be the rendering that is the bottleneck, in which case the
|
|
* solution is to decrease the visual complexity of the scene.
|
|
* - It doesn't make sense to move the *entire* contents of update() into
|
|
* workers unless {@link #setDraw draw}() can interpolate between frames.
|
|
* The lowest-hanging fruit is background updates (like calculating
|
|
* citizens' happiness in a city-building game), physics that doesn't
|
|
* affect the scene (like flags waving in the wind), and anything that is
|
|
* occluded or happening far off screen.
|
|
* - If draw() needs to interpolate physics based on activity that occurs
|
|
* in a worker, the worker needs to pass the interpolation value back to
|
|
* the main thread so that is is available to draw().
|
|
* - Web Workers can't access the state of the main thread, so they can't
|
|
* directly modify objects in your scene. Moving data to and from Web
|
|
* Workers is a pain. The fastest way to do it is with Transferable
|
|
* Objects: basically, you can pass an ArrayBuffer to a worker,
|
|
* destroying the original reference in the process.
|
|
*
|
|
* You can read more about Web Workers and Transferable Objects at
|
|
* [HTML5 Rocks](http://www.html5rocks.com/en/tutorials/workers/basics/).
|
|
*
|
|
* @param {Function} update
|
|
* The update() function.
|
|
* @param {Number} [update.delta]
|
|
* The amount of time in milliseconds to simulate in the update. In most
|
|
* cases this timestep never changes in order to ensure deterministic
|
|
* updates. The timestep is the same as that returned by
|
|
* `MainLoop.getSimulationTimestep()`.
|
|
*/
|
|
setUpdate: function(fun) {
|
|
update = fun || update;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the function that draws things on the screen.
|
|
*
|
|
* The draw() function gets passed the percent of time that the next run of
|
|
* {@link #setUpdate update}() will simulate that has actually elapsed, as
|
|
* a decimal. In other words, draw() gets passed how far between update()
|
|
* calls it is. This is useful because the time simulated by update() and
|
|
* the time between draw() calls is usually different, so the parameter to
|
|
* draw() can be used to interpolate motion between frames to make
|
|
* rendering appear smoother. To illustrate, if update() advances the
|
|
* simulation at each vertical bar in the first row below, and draw() calls
|
|
* happen at each vertical bar in the second row below, then some frames
|
|
* will have time left over that is not yet simulated by update() when
|
|
* rendering occurs in draw():
|
|
*
|
|
* update() timesteps: | | | | | | | | |
|
|
* draw() calls: | | | | | | |
|
|
*
|
|
* To interpolate motion for rendering purposes, objects' state after the
|
|
* last update() must be retained and used to calculate an intermediate
|
|
* state. Note that this means renders will be up to one update() behind.
|
|
* This is still better than extrapolating (projecting objects' state after
|
|
* a future update()) which can produce bizarre results. Storing multiple
|
|
* states can be difficult to set up, and keep in mind that running this
|
|
* process takes time that could push the frame rate down, so it's often
|
|
* not worthwhile unless stuttering is visible.
|
|
*
|
|
* @param {Function} draw
|
|
* The draw() function.
|
|
* @param {Number} [draw.interpolationPercentage]
|
|
* The cumulative amount of time that hasn't been simulated yet, divided
|
|
* by the amount of time that will be simulated the next time update()
|
|
* runs. Useful for interpolating frames.
|
|
*/
|
|
setDraw: function(fun) {
|
|
draw = fun || draw;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets the function that runs at the end of the main loop.
|
|
*
|
|
* Unlike {@link #setUpdate update}(), which can run zero or more times per
|
|
* frame, end() always runs exactly once per frame. This makes it useful
|
|
* for any updates that are not dependent on time in the simulation.
|
|
* Examples include cleaning up any temporary state set up by
|
|
* {@link #setBegin begin}(), lowering the visual quality if the frame rate
|
|
* is too low, or performing long-running updates incrementally. Compared
|
|
* to begin(), generally actions should occur in end() if they use anything
|
|
* that update() or {@link #setDraw draw}() affect.
|
|
*
|
|
* @param {Function} end
|
|
* The end() function.
|
|
* @param {Number} [end.fps]
|
|
* The exponential moving average of the frames per second. This is the
|
|
* same value returned by `MainLoop.getFPS()`. It can be used to take
|
|
* action when the FPS is too low (or to restore to normalcy if the FPS
|
|
* moves back up). Examples of actions to take if the FPS is too low
|
|
* include exiting the application, lowering the visual quality, stopping
|
|
* or reducing activities outside of the main loop like event handlers or
|
|
* audio playback, performing non-critical updates less frequently, or
|
|
* increasing the simulation timestep (by calling
|
|
* `MainLoop.setSimulationTimestep()`). Note that this last option
|
|
* results in more time being simulated per update() call, which causes
|
|
* the application to behave non-deterministically.
|
|
* @param {Boolean} [end.panic=false]
|
|
* Indicates whether the simulation has fallen too far behind real time.
|
|
* Specifically, `panic` will be `true` if too many updates occurred in
|
|
* one frame. In networked lockstep applications, the application should
|
|
* wait for some amount of time to see if the user can catch up before
|
|
* dropping the user. In networked but non-lockstep applications, this
|
|
* typically indicates that the user needs to be snapped or eased to the
|
|
* current authoritative state. When this happens, it may be convenient
|
|
* to call `MainLoop.resetFrameDelta()` to discard accumulated pending
|
|
* updates. In non-networked applications, it may be acceptable to allow
|
|
* the application to keep running for awhile to see if it will catch up.
|
|
* However, this could also cause the application to look like it is
|
|
* running very quickly for a few frames as it transitions through the
|
|
* intermediate states. An alternative that may be acceptable is to
|
|
* simply ignore the unsimulated elapsed time by calling
|
|
* `MainLoop.resetFrameDelta()` even though this introduces
|
|
* non-deterministic behavior. In all cases, if the application panics
|
|
* frequently, this is an indication that the main loop is running too
|
|
* slowly. However, most of the time the drop in frame rate will probably
|
|
* be noticeable before a panic occurs. To help the application catch up
|
|
* after a panic caused by a spiral of death, the same steps can be taken
|
|
* that are suggested above if the FPS drops too low.
|
|
*/
|
|
setEnd: function(fun) {
|
|
end = fun || end;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Starts the main loop.
|
|
*
|
|
* Note that the application is not considered "running" immediately after
|
|
* this function returns; rather, it is considered "running" after the
|
|
* application draws its first frame. The distinction is that event
|
|
* handlers should remain paused until the application is running, even
|
|
* after `MainLoop.start()` is called. Check `MainLoop.isRunning()` for the
|
|
* current status. To act after the application starts, register a callback
|
|
* with requestAnimationFrame() after calling this function and execute the
|
|
* action in that callback. It is safe to call `MainLoop.start()` multiple
|
|
* times even before the application starts running and without calling
|
|
* `MainLoop.stop()` in between, although there is no reason to do this;
|
|
* the main loop will only start if it is not already started.
|
|
*
|
|
* See also `MainLoop.stop()`.
|
|
*/
|
|
start: function() {
|
|
if (!started) {
|
|
// Since the application doesn't start running immediately, track
|
|
// whether this function was called and use that to keep it from
|
|
// starting the main loop multiple times.
|
|
started = true;
|
|
|
|
// In the main loop, draw() is called after update(), so if we
|
|
// entered the main loop immediately, we would never render the
|
|
// initial state before any updates occur. Instead, we run one
|
|
// frame where all we do is draw, and then start the main loop with
|
|
// the next frame.
|
|
rafHandle = requestAnimationFrame(function(timestamp) {
|
|
// Render the initial state before any updates occur.
|
|
draw(1);
|
|
|
|
// The application isn't considered "running" until the
|
|
// application starts drawing.
|
|
running = true;
|
|
|
|
// Reset variables that are used for tracking time so that we
|
|
// don't simulate time passed while the application was paused.
|
|
lastFrameTimeMs = timestamp;
|
|
lastFpsUpdate = timestamp;
|
|
framesSinceLastFpsUpdate = 0;
|
|
|
|
// Start the main loop.
|
|
rafHandle = requestAnimationFrame(animate);
|
|
});
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Stops the main loop.
|
|
*
|
|
* Event handling and other background tasks should also be paused when the
|
|
* main loop is paused.
|
|
*
|
|
* Note that pausing in multiplayer/multi-user applications will cause the
|
|
* player's/user's client to become out of sync. In this case the
|
|
* simulation should exit, or the player/user needs to be snapped to their
|
|
* updated position when the main loop is started again.
|
|
*
|
|
* See also `MainLoop.start()` and `MainLoop.isRunning()`.
|
|
*/
|
|
stop: function() {
|
|
running = false;
|
|
started = false;
|
|
cancelAnimationFrame(rafHandle);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns whether the main loop is currently running.
|
|
*
|
|
* See also `MainLoop.start()` and `MainLoop.stop()`.
|
|
*
|
|
* @return {Boolean}
|
|
* Whether the main loop is currently running.
|
|
*/
|
|
isRunning: function() {
|
|
return running;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* The main loop that runs updates and rendering.
|
|
*
|
|
* @param {DOMHighResTimeStamp} timestamp
|
|
* The current timestamp. In practice this is supplied by
|
|
* requestAnimationFrame at the time that it starts to fire callbacks. This
|
|
* should only be used for comparison to other timestamps because the epoch
|
|
* (i.e. the "zero" time) depends on the engine running this code. In engines
|
|
* that support `DOMHighResTimeStamp` (all modern browsers except iOS Safari
|
|
* 8) the epoch is the time the page started loading, specifically
|
|
* `performance.timing.navigationStart`. Everywhere else, including node.js,
|
|
* the epoch is the Unix epoch (1970-01-01T00:00:00Z).
|
|
*
|
|
* @ignore
|
|
*/
|
|
function animate(timestamp) {
|
|
// Run the loop again the next time the browser is ready to render.
|
|
// We set rafHandle immediately so that the next frame can be canceled
|
|
// during the current frame.
|
|
rafHandle = requestAnimationFrame(animate);
|
|
|
|
// Throttle the frame rate (if minFrameDelay is set to a non-zero value by
|
|
// `MainLoop.setMaxAllowedFPS()`).
|
|
if (timestamp < lastFrameTimeMs + minFrameDelay) {
|
|
return;
|
|
}
|
|
|
|
// frameDelta is the cumulative amount of in-app time that hasn't been
|
|
// simulated yet. Add the time since the last frame. We need to track total
|
|
// not-yet-simulated time (as opposed to just the time elapsed since the
|
|
// last frame) because not all actually elapsed time is guaranteed to be
|
|
// simulated each frame. See the comments below for details.
|
|
frameDelta += timestamp - lastFrameTimeMs;
|
|
lastFrameTimeMs = timestamp;
|
|
|
|
// Run any updates that are not dependent on time in the simulation. See
|
|
// `MainLoop.setBegin()` for additional details on how to use this.
|
|
begin(timestamp, frameDelta);
|
|
|
|
// Update the estimate of the frame rate, `fps`. Approximately every
|
|
// second, the number of frames that occurred in that second are included
|
|
// in an exponential moving average of all frames per second. This means
|
|
// that more recent seconds affect the estimated frame rate more than older
|
|
// seconds.
|
|
if (timestamp > lastFpsUpdate + fpsUpdateInterval) {
|
|
// Compute the new exponential moving average.
|
|
fps =
|
|
// Divide the number of frames since the last FPS update by the
|
|
// amount of time that has passed to get the mean frames per second
|
|
// over that period. This is necessary because slightly more than a
|
|
// second has likely passed since the last update.
|
|
fpsAlpha * framesSinceLastFpsUpdate * 1000 / (timestamp - lastFpsUpdate) +
|
|
(1 - fpsAlpha) * fps;
|
|
|
|
// Reset the frame counter and last-updated timestamp since their
|
|
// latest values have now been incorporated into the FPS estimate.
|
|
lastFpsUpdate = timestamp;
|
|
framesSinceLastFpsUpdate = 0;
|
|
}
|
|
// Count the current frame in the next frames-per-second update. This
|
|
// happens after the previous section because the previous section
|
|
// calculates the frames that occur up until `timestamp`, and `timestamp`
|
|
// refers to a time just before the current frame was delivered.
|
|
framesSinceLastFpsUpdate++;
|
|
|
|
/*
|
|
* A naive way to move an object along its X-axis might be to write a main
|
|
* loop containing the statement `obj.x += 10;` which would move the object
|
|
* 10 units per frame. This approach suffers from the issue that it is
|
|
* dependent on the frame rate. In other words, if your application is
|
|
* running slowly (that is, fewer frames per second), your object will also
|
|
* appear to move slowly, whereas if your application is running quickly
|
|
* (that is, more frames per second), your object will appear to move
|
|
* quickly. This is undesirable, especially in multiplayer/multi-user
|
|
* applications.
|
|
*
|
|
* One solution is to multiply the speed by the amount of time that has
|
|
* passed between rendering frames. For example, if you want your object to
|
|
* move 600 units per second, you might write `obj.x += 600 * delta`, where
|
|
* `delta` is the time passed since the last frame. (For convenience, let's
|
|
* move this statement to an update() function that takes `delta` as a
|
|
* parameter.) This way, your object will move a constant distance over
|
|
* time. However, at low frame rates and high speeds, your object will move
|
|
* large distances every frame, which can cause it to do strange things
|
|
* such as move through walls. Additionally, we would like our program to
|
|
* be deterministic. That is, every time we run the application with the
|
|
* same input, we would like exactly the same output. If the time between
|
|
* frames (the `delta`) varies, our output will diverge the longer the
|
|
* program runs due to accumulated rounding errors, even at normal frame
|
|
* rates.
|
|
*
|
|
* A better solution is to separate the amount of time simulated in each
|
|
* update() from the amount of time between frames. Our update() function
|
|
* doesn't need to change; we just need to change the delta we pass to it
|
|
* so that each update() simulates a fixed amount of time (that is, `delta`
|
|
* should have the same value each time update() is called). The update()
|
|
* function can be run multiple times per frame if needed to simulate the
|
|
* total amount of time passed since the last frame. (If the time that has
|
|
* passed since the last frame is less than the fixed simulation time, we
|
|
* just won't run an update() until the the next frame. If there is
|
|
* unsimulated time left over that is less than our timestep, we'll just
|
|
* leave it to be simulated during the next frame.) This approach avoids
|
|
* inconsistent rounding errors and ensures that there are no giant leaps
|
|
* through walls between frames.
|
|
*
|
|
* That is what is done below. It introduces a new problem, but it is a
|
|
* manageable one: if the amount of time spent simulating is consistently
|
|
* longer than the amount of time between frames, the application could
|
|
* freeze and crash in a spiral of death. This won't happen as long as the
|
|
* fixed simulation time is set to a value that is high enough that
|
|
* update() calls usually take less time than the amount of time they're
|
|
* simulating. If it does start to happen anyway, see `MainLoop.setEnd()`
|
|
* for a discussion of ways to stop it.
|
|
*
|
|
* Additionally, see `MainLoop.setUpdate()` for a discussion of performance
|
|
* considerations.
|
|
*
|
|
* Further reading for those interested:
|
|
*
|
|
* - http://gameprogrammingpatterns.com/game-loop.html
|
|
* - http://gafferongames.com/game-physics/fix-your-timestep/
|
|
* - https://gamealchemist.wordpress.com/2013/03/16/thoughts-on-the-javascript-game-loop/
|
|
* - https://developer.mozilla.org/en-US/docs/Games/Anatomy
|
|
*/
|
|
numUpdateSteps = 0;
|
|
while (frameDelta >= simulationTimestep) {
|
|
update(simulationTimestep);
|
|
frameDelta -= simulationTimestep;
|
|
|
|
/*
|
|
* Sanity check: bail if we run the loop too many times.
|
|
*
|
|
* One way this could happen is if update() takes longer to run than
|
|
* the time it simulates, thereby causing a spiral of death. For ways
|
|
* to avoid this, see `MainLoop.setEnd()`. Another way this could
|
|
* happen is if the browser throttles serving frames, which typically
|
|
* occurs when the tab is in the background or the device battery is
|
|
* low. An event outside of the main loop such as audio processing or
|
|
* synchronous resource reads could also cause the application to hang
|
|
* temporarily and accumulate not-yet-simulated time as a result.
|
|
*
|
|
* 240 is chosen because, for any sane value of simulationTimestep, 240
|
|
* updates will simulate at least one second, and it will simulate four
|
|
* seconds with the default value of simulationTimestep. (Safari
|
|
* notifies users that the script is taking too long to run if it takes
|
|
* more than five seconds.)
|
|
*
|
|
* If there are more updates to run in a frame than this, the
|
|
* application will appear to slow down to the user until it catches
|
|
* back up. In networked applications this will usually cause the user
|
|
* to get out of sync with their peers, but if the updates are taking
|
|
* this long already, they're probably already out of sync.
|
|
*/
|
|
if (++numUpdateSteps >= 240) {
|
|
panic = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Render the screen. We do this regardless of whether update() has run
|
|
* during this frame because it is possible to interpolate between updates
|
|
* to make the frame rate appear faster than updates are actually
|
|
* happening. See `MainLoop.setDraw()` for an explanation of how to do
|
|
* that.
|
|
*
|
|
* We draw after updating because we want the screen to reflect a state of
|
|
* the application that is as up-to-date as possible. (`MainLoop.start()`
|
|
* draws the very first frame in the application's initial state, before
|
|
* any updates have occurred.) Some sources speculate that rendering
|
|
* earlier in the requestAnimationFrame callback can get the screen painted
|
|
* faster; this is mostly not true, and even when it is, it's usually just
|
|
* a trade-off between rendering the current frame sooner and rendering the
|
|
* next frame later.
|
|
*
|
|
* See `MainLoop.setDraw()` for details about draw() itself.
|
|
*/
|
|
draw(frameDelta / simulationTimestep);
|
|
|
|
// Run any updates that are not dependent on time in the simulation. See
|
|
// `MainLoop.setEnd()` for additional details on how to use this.
|
|
end(fps, panic);
|
|
|
|
panic = false;
|
|
}
|
|
|
|
// AMD support
|
|
if (typeof define === 'function' && define.amd) {
|
|
define(root.MainLoop);
|
|
}
|
|
// CommonJS support
|
|
else if (typeof module === 'object' && module !== null && typeof module.exports === 'object') {
|
|
module.exports = root.MainLoop;
|
|
}
|
|
|
|
})(this); |