kontra
Version:
Kontra HTML5 game development library
171 lines (154 loc) • 4.34 kB
JavaScript
import { noop } from './utils.js'
import { emit } from './events.js'
import { getContext, getCanvas } from './core.js'
/**
* Clear the canvas.
*/
function clear() {
let canvas = getCanvas();
getContext().clearRect(0, 0, canvas.width, canvas.height);
}
/**
* The game loop updates and renders the game every frame. The game loop is stopped by default and will not start until the loops `start()` function is called.
*
* The game loop uses a time-based animation with a fixed `dt` to [avoid frame rate issues](http://blog.sklambert.com/using-time-based-animation-implement/). Each update call is guaranteed to equal 1/60 of a second.
*
* This means that you can avoid having to do time based calculations in your update functions and instead do fixed updates.
*
* ```js
* import { Sprite, GameLoop } from 'kontra';
*
* let sprite = Sprite({
* x: 100,
* y: 200,
* width: 20,
* height: 40,
* color: 'red'
* });
*
* let loop = GameLoop({
* update: function(dt) {
* // no need to determine how many pixels you want to
* // move every second and multiple by dt
* // sprite.x += 180 * dt;
*
* // instead just update by how many pixels you want
* // to move every frame and the loop will ensure 60FPS
* sprite.x += 3;
* },
* render: function() {
* sprite.render();
* }
* });
*
* loop.start();
* ```
* @sectionName GameLoop
*
* @param {Object} properties - Properties of the game loop.
* @param {Function} properties.update - Function called every frame to update the game. Is passed the fixed `dt` as a parameter.
* @param {Function} properties.render - Function called every frame to render the game.
* @param {Number} [properties.fps=60] - Desired frame rate.
* @param {Boolean} [properties.clearCanvas=true] - Clear the canvas every frame before the `render()` function is called.
*/
export default function GameLoop({fps = 60, clearCanvas = true, update, render} = {}) {
// check for required functions
// @if DEBUG
if ( !(update && render) ) {
throw Error('You must provide update() and render() functions');
}
// @endif
// animation variables
let accumulator = 0;
let delta = 1E3 / fps; // delta between performance.now timings (in ms)
let step = 1 / fps;
let clearFn = clearCanvas ? clear : noop
let last, rAF, now, dt, loop;
/**
* Called every frame of the game loop.
*/
function frame() {
rAF = requestAnimationFrame(frame);
now = performance.now();
dt = now - last;
last = now;
// prevent updating the game with a very large dt if the game were to lose focus
// and then regain focus later
if (dt > 1E3) {
return;
}
emit('tick');
accumulator += dt;
while (accumulator >= delta) {
loop.update(step);
accumulator -= delta;
}
clearFn();
loop.render();
}
// game loop object
loop = {
/**
* Called every frame to update the game. Put all of your games update logic here.
* @memberof GameLoop
* @function update
*
* @param {Number} dt - The fixed dt time of 1/60 of a frame.
*/
update,
/**
* Called every frame to render the game. Put all of your games render logic here.
* @memberof GameLoop
* @function render
*/
render,
/**
* If the game loop is currently stopped.
*
* ```js
* import { GameLoop } from 'kontra';
*
* let loop = GameLoop({
* // ...
* });
* console.log(loop.isStopped); //=> true
*
* loop.start();
* console.log(loop.isStopped); //=> false
*
* loop.stop();
* console.log(loop.isStopped); //=> true
* ```
* @memberof GameLoop
* @property {Boolean} isStopped
*/
isStopped: true,
/**
* Start the game loop.
* @memberof GameLoop
* @function start
*/
start() {
last = performance.now();
this.isStopped = false;
requestAnimationFrame(frame);
},
/**
* Stop the game loop.
* @memberof GameLoop
* @function stop
*/
stop() {
this.isStopped = true;
cancelAnimationFrame(rAF);
},
// expose properties for testing
// @if DEBUG
_frame: frame,
set _last(value) {
last = value;
}
// @endif
};
return loop;
};