├── .gitignore
├── README.md
├── dist
├── bundle.cjs.js
├── bundle.esm.js
└── bundle.umd.js
├── docs
├── css
│ └── styles.css
├── dist
│ └── bundle.umd.js
├── index.html
└── js
│ ├── setup-demo.js
│ ├── setup-stats.js
│ ├── setup-threejs.js
│ └── setup-vanilla.js
├── package.json
├── rollup.config.js
└── src
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | npm-debug.log*
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # game-loop-js :video_game: :repeat:
2 |
3 | [](https://www.npmjs.com/package/@wmcmurray/game-loop-js)
4 | [](https://lgtm.com/projects/g/wmcmurray/game-loop-js/context:javascript)
5 |
6 | [](https://wmcmurray.github.io/game-loop-js/)
7 |
8 | - Lightweight
9 | - Built with speed in mind
10 | - Ensure **consistent frame-rates** (based on the targeted FPS of your choosing)
11 | - The targeted FPS is **tweakable on-the-fly** (while the game is running)
12 | - This implementation **does not have a requestAnimationFrame loop**, it let you implement it yourself *or not* (because in some cases it's handled by the framework, ex: threejs in a WebXR project)
13 | - "Normalize" the deltaTime at the source to make sure 1 equals 1 second (instead of 1 millisecond), because many calculations are expressed in seconds instead of milliseconds (ex: gravity force is -9.81 meters per seconds) so this prevents you from "/ 1000" everywhere in your actual code, it's done at the source, one time.
14 |
15 |
16 | ## Usage
17 |
18 | ```
19 | npm install @wmcmurray/game-loop-js
20 | ```
21 |
22 | ### Three.js / requestAnimationFrame()
23 |
24 | ```javascript
25 | import { createGameLoop } from '@wmcmurray/game-loop-js'
26 |
27 | const myGameLoop = function(deltaTime) {
28 | // ...
29 | };
30 |
31 | // how to create a game loop with a targeted 60 FPS :
32 | const gameLoop = createGameLoop(myGameLoop, 60);
33 |
34 | // how to change the targeted FPS :
35 | gameLoop.fps = 144;
36 |
37 | // how to get the targeted FPS :
38 | const targetFps = gameLoop.fps;
39 |
40 |
41 | // how to register the loop :
42 |
43 | // using three.js
44 | renderer.setAnimationLoop( gameLoop.loop );
45 |
46 | // -- or --
47 |
48 | // using requestAnimationFrame
49 | function animate( time ) {
50 | gameLoop.loop( time );
51 | requestAnimationFrame( animate );
52 | }
53 | requestAnimationFrame( animate );
54 | ```
55 |
56 |
57 | ### Browser
58 |
59 | ```html
60 |
61 |
62 |
75 | ```
76 |
--------------------------------------------------------------------------------
/dist/bundle.cjs.js:
--------------------------------------------------------------------------------
1 | "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.createGameLoop=function(e,t=60){let o=0,r=0,s=0,n=0,p=0,u=0;function c(e){o=e,r=1e3/o}return c(t),{get fps(){return o},set fps(e){c(e)},loop(t){u=t-s,u>=r&&(p=n,n=u%r,s=t-n,u-=p,u*=.001,e(u))}}};
2 |
--------------------------------------------------------------------------------
/dist/bundle.esm.js:
--------------------------------------------------------------------------------
1 | function t(t,e=60){let n=0,o=0,r=0,f=0,p=0,u=0;function s(t){n=t,o=1e3/n}return s(e),{get fps(){return n},set fps(t){s(t)},loop(e){u=e-r,u>=o&&(p=f,f=u%o,r=e-f,u-=p,u*=.001,t(u))}}}export{t as createGameLoop};
2 |
--------------------------------------------------------------------------------
/dist/bundle.umd.js:
--------------------------------------------------------------------------------
1 | !function(e,o){"object"==typeof exports&&"undefined"!=typeof module?o(exports):"function"==typeof define&&define.amd?define(["exports"],o):o((e="undefined"!=typeof globalThis?globalThis:e||self).gameLoopJs={})}(this,(function(e){"use strict";e.createGameLoop=function(e,o=60){let t=0,n=0,f=0,i=0,s=0,p=0;function u(e){t=e,n=1e3/t}return u(o),{get fps(){return t},set fps(e){u(e)},loop(o){p=o-f,p>=n&&(s=i,i=p%n,f=o-i,p-=s,p*=.001,e(p))}}},Object.defineProperty(e,"__esModule",{value:!0})}));
2 |
--------------------------------------------------------------------------------
/docs/css/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0px;
3 | overflow: hidden;
4 | background-color: #111;
5 | font-family: sans-serif;
6 | color: cyan;
7 | }
8 |
9 | canvas {
10 | width: 100% !important;
11 | height: 100% !important;
12 | }
13 |
14 | a {
15 | color: rgb(0,255,0);
16 | }
17 |
18 | #panel {
19 | position: fixed;
20 | top: 0px;
21 | right: 0px;
22 | background-color: rgba(50,50,50, 0.5);
23 | text-align: center;
24 | font-size: 20px;
25 | }
26 | #panel > .panel-item {
27 | padding: 10px 40px;
28 | cursor: pointer;
29 | transition: all 0.2s ease-out;
30 | }
31 | #panel > .panel-item:hover {
32 | background-color: rgba(0,255,0, 0.05);
33 | }
34 | #panel > .panel-item.active {
35 | background-color: rgba(255,0,255, 0.1);
36 | color: magenta;
37 | }
38 |
39 | #result {
40 | display: flex;
41 | flex-wrap: wrap;
42 | align-items: center;
43 | justify-content: center;
44 | flex-direction: column;
45 | position: fixed;
46 | top: 0px;
47 | left: 0px;
48 | width: 100%;
49 | height: 100%;
50 | font-size: 40px;
51 | }
52 |
53 | #infos {
54 | position: fixed;
55 | bottom: 0px;
56 | left: 0px;
57 | width: 100%;
58 | padding: 20px;
59 | text-align: center;
60 | }
61 |
--------------------------------------------------------------------------------
/docs/dist/bundle.umd.js:
--------------------------------------------------------------------------------
1 | !function(e,o){"object"==typeof exports&&"undefined"!=typeof module?o(exports):"function"==typeof define&&define.amd?define(["exports"],o):o((e="undefined"!=typeof globalThis?globalThis:e||self).gameLoopJs={})}(this,(function(e){"use strict";e.createGameLoop=function(e,o=60){let t=0,n=0,f=0,i=0,s=0,p=0;function u(e){t=e,n=1e3/t}return u(o),{get fps(){return t},set fps(e){u(e)},loop(o){p=o-f,p>=n&&(s=i,i=p%n,f=o-i,p-=s,p*=.001,e(p))}}},Object.defineProperty(e,"__esModule",{value:!0})}));
2 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | game-loop-js
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
27 |
28 |
Both elapsed time should stay in sync.
29 |
The cube should rotate 90°/sec consistently across time and frame-rates.
30 |
( Note : you might not be able to go higher than 60 fps if your monitor does not support a higher refresh-rate )
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/docs/js/setup-demo.js:
--------------------------------------------------------------------------------
1 | const resultDomElem = document.getElementById('result');
2 | const resultTimeGame = document.createElement('div');
3 | const resultTimeInterval = document.createElement('div');
4 | resultDomElem.appendChild(resultTimeGame);
5 | resultDomElem.appendChild(resultTimeInterval);
6 |
7 | function selectProperItemInPanel() {
8 | document.querySelectorAll('.panel-item').forEach(function(item) {
9 | if(Number(item.attributes['data-fps'].value) === gameLoop.fps) {
10 | item.classList.add('active')
11 | } else {
12 | item.classList.remove('active')
13 | }
14 | });
15 | }
16 | selectProperItemInPanel();
17 |
18 | // panel click listener
19 | document.addEventListener('click', function (evt) {
20 | if(evt.target.matches('.panel-item')) {
21 | gameLoop.fps = Number(evt.target.attributes['data-fps'].value);
22 | selectProperItemInPanel();
23 | }
24 | }, false);
25 |
--------------------------------------------------------------------------------
/docs/js/setup-stats.js:
--------------------------------------------------------------------------------
1 | const stats = new Stats();
2 | document.body.appendChild(stats.dom);
3 |
--------------------------------------------------------------------------------
/docs/js/setup-threejs.js:
--------------------------------------------------------------------------------
1 | const renderer = new THREE.WebGLRenderer({
2 | antialias: true,
3 | });
4 | renderer.setPixelRatio( window.devicePixelRatio );
5 | renderer.setSize( window.innerWidth, window.innerHeight );
6 | document.body.appendChild( renderer.domElement );
7 |
8 | const scene = new THREE.Scene();
9 | const cube = new THREE.Mesh( new THREE.BoxBufferGeometry( 1, 1, 1 ), new THREE.MeshNormalMaterial() );
10 | cube.position.y = 0.2;
11 | cube.rotation.x = 0.5;
12 | scene.add( cube );
13 |
14 | const camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000 );
15 | camera.position.z = 2;
16 |
17 | // window resize listener
18 | window.addEventListener('resize', function() {
19 | camera.aspect = window.innerWidth / window.innerHeight;
20 | camera.updateProjectionMatrix();
21 | renderer.setSize( window.innerWidth, window.innerHeight );
22 | }, false);
23 |
24 | // -----
25 |
26 | let gameTimeElapsed = 0;
27 | // let intervalTimeElapsed = 0;
28 |
29 | const cubeRotationRadian = 90 * (Math.PI / 180); // 90 degrees
30 | const cubeRotationPerSeconds = cubeRotationRadian;
31 | const easing = function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 };
32 |
33 | let isFirst = true;
34 | const gameLoop = gameLoopJs.createGameLoop(function(deltaTime) {
35 | gameTimeElapsed += deltaTime;
36 | cube.rotation.y = cubeRotationRadian * easing(gameTimeElapsed % 1);
37 |
38 | renderer.render( scene, camera );
39 | stats.update();
40 |
41 | if(Math.floor(gameTimeElapsed)) {
42 | resultTimeGame.innerHTML = Math.floor(gameTimeElapsed) + ' sec (game)';
43 | }
44 |
45 | if(isFirst) {
46 | isFirst = false;
47 | const firstA = Math.floor(gameTimeElapsed);
48 |
49 | // sync my interval with the game loop
50 | setTimeout(function() {
51 | const intervalStartedAt = Date.now();
52 | const displayIntervalTime = function() {
53 | // resultTimeInterval.innerHTML = Math.floor(++intervalTimeElapsed + firstA) + ' sec (interval)';
54 | resultTimeInterval.innerHTML = Math.floor( ((Date.now() - intervalStartedAt) * 0.001) + firstA + 1 ) + ' sec (interval)';
55 | };
56 | setInterval(displayIntervalTime, 1000);
57 | displayIntervalTime();
58 | }, 1000 * (1 - (gameTimeElapsed - firstA)));
59 | }
60 | }, 60);
61 |
62 | // start the demo at the exact start of a second
63 | setTimeout(function() {
64 | renderer.setAnimationLoop( gameLoop.loop );
65 | }, 1000 - (new Date()).getMilliseconds());
66 |
--------------------------------------------------------------------------------
/docs/js/setup-vanilla.js:
--------------------------------------------------------------------------------
1 | let timeElapsed = 0;
2 |
3 | const gameLoop = gameLoopJs.createGameLoop(function(deltaTime) {
4 | timeElapsed += deltaTime;
5 |
6 | document.getElementById('result').innerHTML = Math.round(timeElapsed * 1000) / 1000;
7 |
8 | stats.update();
9 | }, 60);
10 |
11 | // start the demo
12 | function onAnimationFrame( time ) {
13 | gameLoop.loop( time );
14 | requestAnimationFrame( onAnimationFrame );
15 | }
16 | requestAnimationFrame( onAnimationFrame );
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wmcmurray/game-loop-js",
3 | "version": "1.0.0",
4 | "description": "Reliable and versatile game loop implementation",
5 | "main": "dist/bundle.cjs.js",
6 | "module": "dist/bundle.esm.js",
7 | "browser": "dist/bundle.umd.js",
8 | "scripts": {
9 | "build": "rollup -c",
10 | "dev": "rollup -c -w",
11 | "test": "http-server ./docs/ --silent -c-1 -o /index.html"
12 | },
13 | "files": [
14 | "dist"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/wmcmurray/game-loop-js.git"
19 | },
20 | "keywords": [
21 | "game",
22 | "loop",
23 | "gameloop",
24 | "renderloop",
25 | "requestAnimationFrame"
26 | ],
27 | "author": "William McMurray ",
28 | "license": "ISC",
29 | "bugs": {
30 | "url": "https://github.com/wmcmurray/game-loop-js/issues"
31 | },
32 | "homepage": "https://github.com/wmcmurray/game-loop-js",
33 | "devDependencies": {
34 | "http-server": "^0.12.3",
35 | "rollup": "^2.32.0",
36 | "rollup-plugin-copy": "^3.3.0",
37 | "rollup-plugin-terser": "^7.0.2"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { terser } from 'rollup-plugin-terser'
2 | import copy from 'rollup-plugin-copy'
3 | import pkg from './package.json'
4 |
5 | export default {
6 | input: 'src/index.js',
7 | output: [
8 | // UMD build (browser-friendly)
9 | {
10 | file: pkg.browser,
11 | format: 'umd',
12 | name: 'gameLoopJs',
13 | },
14 |
15 | // CommonJS build (for Node)
16 | {
17 | file: pkg.main,
18 | format: 'cjs',
19 | exports: 'named',
20 | },
21 |
22 | // ES module build (for bundlers)
23 | {
24 | file: pkg.module,
25 | format: 'es',
26 | },
27 | ],
28 | plugins: [
29 | terser(),
30 | copy({
31 | targets: [
32 | // move the UMD build into the docs folder (so it can be used by the live demo)
33 | {
34 | src: pkg.browser,
35 | dest: 'docs/dist',
36 | },
37 | ],
38 | }),
39 | ],
40 | };
41 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Wraps a loop function so it can be executed at a specific frame-rate
3 | * func {Function} = The function you want to execute each frames
4 | * fps {Number} = The targeted frame rate
5 | */
6 | export function createGameLoop(func, fps = 60) {
7 | let targetFps = 0, fpsInterval = 0;
8 | let lastTime = 0, lastOverTime = 0, prevOverTime = 0, deltaTime = 0;
9 |
10 | function updateFps(value) {
11 | targetFps = value;
12 | fpsInterval = 1000 / targetFps;
13 | }
14 |
15 | updateFps(fps);
16 |
17 | return {
18 | // getter/setter for targeted frame rate
19 | get fps() {
20 | return targetFps;
21 | },
22 | set fps(value) {
23 | updateFps(value);
24 | },
25 |
26 | // the frame-capped loop function
27 | loop(time) {
28 | deltaTime = time - lastTime;
29 |
30 | if(deltaTime >= fpsInterval) {
31 | prevOverTime = lastOverTime;
32 | lastOverTime = deltaTime % fpsInterval;
33 | lastTime = time - lastOverTime;
34 |
35 | // keep time elapsed in sync with real life
36 | deltaTime -= prevOverTime;
37 |
38 | // "normalize" the delta time (so 1 equals to 1 second)
39 | deltaTime *= 0.001;
40 |
41 | func(deltaTime);
42 | }
43 | },
44 | };
45 | }
46 |
--------------------------------------------------------------------------------