/**
* @author Wolfgang Kowarschick <kowa@hs-augsburg.de>
* @copyright 2017
* @license CC-BY-NC-SA-4.0
* @based_on https://isaacsukin.com/news/2015/01/detailed-explanation-javascript-game-loops-and-timing
*/
import EventDispatcher from './EventDispatcher.js';
const C_STATE_HAS_CHANGED_EVENT = 'stated_has_changed_event',
C_STOPPED = 1,
C_PAUSED = 2,
C_RUNNING = 3;
let
v_game_loop = null, // There exists at most on game loop object,
// which is stored here.
// As GameLoop is a singleton class, all private members are stored
// directly in the module.
v_document = null, // the document where the view is to be displayed
f_update, // the model update function
f_render, // the view render function
f_reset, // a function the resets the game to an initial state
f_panic, // the panic function which is called, if f_update
// was not called for at least one second.
v_state = null, // the current state of the loop
v_raf_id = null, // the id of the current requestAnimationFrame call
// The model should be updated exactly 60 times per seconds.
v_model_fps = 60,
v_model_step_ms = 1000/v_model_fps, // = 16,6667 ms (duration of
v_model_step_s = 1/v_model_fps, // = 0,016667 s a model frame)
v_time_last_run_ms, // the time the loop was run last
v_delta_ms; // the accumulated time that hasn't been simulated yet
// Create a new event
function f_new_event()
{
return new CustomEvent(C_STATE_HAS_CHANGED_EVENT);
}
// The default panic function.
function f_panic_default()
{
console.log('Panic: No model updates for at least on second.');
v_game_loop.pause();
v_delta_ms = 0;
}
// The game loop function.
function f_loop(p_current_time_ms)
{
// Track the accumulated time that hasn't been simulated yet.
v_delta_ms += p_current_time_ms - v_time_last_run_ms; // please note +=
v_time_last_run_ms = p_current_time_ms;
// Simulate the total elapsed time in fixed-size chunks.
let l_counter_updates = 0;
while (v_delta_ms >= v_model_step_ms)
{
f_update(v_model_step_s); // Update the model world.
// v_model_step_s doesn't change over time.
v_delta_ms -= v_model_step_ms; // Reduce the time that hasn't been simulated.
l_counter_updates++;
if (l_counter_updates > v_model_fps) // less then one frame per second
f_panic();
}
// Visualize the current state of the model.
// Use the time that has not been simulated in the model
// to interpolate the current position.
f_render(v_delta_ms/v_model_step_ms);
v_raf_id = requestAnimationFrame(f_loop);
}
class GameLoop extends EventDispatcher
{
/**
* @class GameLoop
* A singleton class that enables running a game loop which guarantees
* exactly 60 model updates per second (in most cases), even if the
* view update rate is significantly lower.
*/
/**
* @constructor
* @param p_update {Function}
* A model update function that is to be called exactly 60 times
* per second.
* @param p_render {Function}
* A view render function that is to be called as often as possible
* up to 60 times per second.
* @param p_reset {Function}
* A function to reset the game to an initial state.
* @param p_panic {Function}
* A function that is called if the state of the loop is
* <code>GameLoop.RUNNING</code> and the model update function
* is not called for at least one second.
* @param p_document {Object}
* The document where the views are to be displayed.
* If the document looses the visibility focus, the
* game loop automatically is paused.
*/
constructor(p_update, p_render, p_reset = null,
p_panic = f_panic_default,
p_document = window.document
)
{
super({stated_has_changed_event: f_new_event()});
if (v_document === null)
{
v_document = p_document;
v_document.addEventListener
( 'visibilitychange',
function()
{
if (v_document.hidden)
v_game_loop.pause();
else if (v_state === C_PAUSED)
v_game_loop.goOn();
}
);
}
f_update = p_update;
f_render = p_render;
f_reset = p_reset;
f_panic = p_panic;
// Make the game loop class to be a singleton class.
if (v_game_loop === null)
v_game_loop = this;
// Reset or initialize the current game loop.
v_game_loop.stop();
return v_game_loop;
}
/** State of the game loop when the game loop has been stopped by
* means of the function <code>stop</code>.
* This is the default state, after the game loop has been initialized.
*/
static get STATE_HAS_CHANGED_EVENT() { return C_STATE_HAS_CHANGED_EVENT; }
/**
* State of the game loop when the game loop has been stopped by
* means of the function <code>stop</code>.
* This is the default state, after the game loop has been initialized.
*/
static get STOPPED() { return C_STOPPED; }
/**
* State of the game loop when the game loop has been paused by
* means of the function <code>pause</code>.
*/
static get PAUSED() { return C_PAUSED; }
/**
* State of the game loop when the game loop has been started or
* continued by means of the function <code>start</code>
* or <code>continue</code>.
*/
static get RUNNING() { return C_RUNNING; }
/**
* @return {number} The current state of the game loop. The value is either
* <code>GameLoop.STOPPED</code>,
* <code>GameLoop.PAUSED</code>, or
* <code>GameLoop.RUNNING</code>
*/
static get state() { return v_state; }
/**
* Function to start the game loop.
* This function has only an effect, if the current state of
* the game loop is <code>gameLoop.STOPPED</code>.
*/
start()
{
if (v_state === C_STOPPED) // Don't start a game twice.
{
v_state = C_RUNNING;
this.dispatchEvent(f_new_event());
// Initialize and start the game loop.
v_raf_id =
requestAnimationFrame
( // Initialize all variables needed.
function (p_current_time_ms)
{
f_render(); // Visualize the initial state of the game.
v_time_last_run_ms = p_current_time_ms;
v_delta_ms = 0;
// Now really start the game loop;
v_raf_id = requestAnimationFrame(f_loop);
}
);
}
}
/**
* Function to pause the game loop.
* This function has only an effect, if the current state of
* the game loop is <code>gameLoop.RUNNING</code>.
*/
pause()
{
if (v_state === C_RUNNING) // Don't pause a game that ist not running.
{
v_state = C_PAUSED;
this.dispatchEvent(f_new_event());
cancelAnimationFrame(v_raf_id);
v_raf_id = null;
}
}
/**
* Function to continue the game loop.
* This function has only an effect, if the current state of
* the game loop is <code>GameLoop.PAUSED</code>.
*/
goOn()
{
if (v_state === C_PAUSED) // The game is not paused, so it cannot be continued.
{
v_state = C_RUNNING;
this.dispatchEvent(f_new_event());
// Initialize and restart the game loop.
v_raf_id =
requestAnimationFrame
( // Initialize all variables needed.
function (p_current_time_ms)
{
v_time_last_run_ms = p_current_time_ms;
v_raf_id = requestAnimationFrame(f_loop);
}
);
}
}
/**
* Function to continue the game loop.
* This function has only an effect, if the current state of
* the game loop is not already <code>GameLoop.STOPPED</code>.
*
* @fires GameLoop:state_has_changed
*/
stop()
{
if (v_state !== C_STOPPED) // Don't stop a game twice.
{
v_state = C_STOPPED;
this.dispatchEvent(f_new_event());
cancelAnimationFrame(v_raf_id);
v_raf_id = null;
// Reset the app and visualize the new state;
if (f_reset !== null)
f_reset();
f_render();
}
}
}
export default GameLoop;