Best way for simple game-loop in Javascript?
Asked Answered
N

7

25

Is there a simple way to make a game loop in JavaScript? something like...

onTimerTick() {
  // update game state
}
Nifty answered 23/12, 2009 at 22:32 Comment(3)
In the event-driven world of JavaScript you probably don't need to do this. Why do you think you need this?Flowerage
@Jon, how would you go about updating a game when no event has been triggered? Many games are doing things even when you aren't...Fulgurating
There is comprehensive explanation on how to implement game loop in javascript isaacsukin.com/news/2015/01/…Pieria
F
24
setInterval(onTimerTick, 33); // 33 milliseconds = ~ 30 frames per sec

function onTimerTick() {
    // Do stuff.
}
Fulgurating answered 23/12, 2009 at 22:34 Comment(4)
You'd probably want to reverse the order of the first line and the rest. ;)Rhigolene
@Dav: There's no reason to do that. Function declarations take effect before the step-by-step code in the same scope.Go
causes jitters and is just a stub.Kermis
For modern browsers and various js game engines are now using requestAnimationFrameReactive
K
33

There are a varied amount of ways to achieve this using JavaScript depending on your application. A setInterval() or even with a while() statement would do the trick. This will not work for a game loop. JavaScript interpreted by the browser, so it is prone to interrupts. Interrupts will make the play back of your game feel jittery.

The webkitRequestAnimationFrame properties of CSS3 aims to correct this by managing the rendering loop itself. However this is still not the most efficient way to do this and will be prone to jitters if you have alot of objects being updated.

This is a good website to get started with:

http://nokarma.org/2011/02/02/javascript-game-development-the-game-loop/index.html

This site has some good information on the basics of making a game loop. It does not touch upon any sort of object oriented design by any means. The most accurate way to achieve precise timings is by using the date function.

while ((new Date).getTime() > nextGameTick && loops < maxFrameSkip) {
  Game.update();
  nextGameTick += skipTicks;
  loops++;
}

This does not take into account how setTimeout drifts at high frequencies. This will also lead to things getting out of sync and becoming jittery. JavaScript will drift +/- 18ms per second.

var start, tick = 0;
var f = function() {
    if (!start) start = new Date().getTime();
    var now = new Date().getTime();
    if (now < start + tick*1000) {
        setTimeout(f, 0);
    } else {
        tick++;
        var diff = now - start;
        var drift = diff % 1000;
        $('<li>').text(drift + "ms").appendTo('#results');
        setTimeout(f, 990);
    }
};

setTimeout(f, 990);

Now lets put all of this into a working example. We want to inject our game loop into WebKit’s managed rendering loop. This will help smooth out the rendered graphics. We also want to split up the draw and update functions. This will update the objects in our rendering scene before calculating when the next frame should be drawn. The game loop should also skip draw frames if updating takes to long.

Index.html

<html>
    <head>
        <!--load scripts--> 
    </head>
    <!-- 
        render canvas into body, alternative you can use div, but disable 
        right click and hide cursor on parent div
    -->
    <body oncontextmenu="return false" style="overflow:hidden;cursor:none;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;">
        <script type="text/javascript" charset="utf-8">
            Game.initialize();
            window.onEachFrame(Game.run);
        </script>
    </body>
</html>

Game.js

var Game = {};

Game.fps = 60;
Game.maxFrameSkip = 10;
Game.skipTicks = 1000 / Game.fps;

Game.initialize = function() {
    this.entities = [];
    this.viewport = document.body;

    this.input = new Input();

    this.debug = new Debug();
    this.debug.initialize(this.viewport);

    this.screen = new Screen();
    this.screen.initialize(this.viewport);
    this.screen.setWorld(new World());
};

Game.update = function(tick) {
    Game.tick = tick;
    this.input.update();
    this.debug.update();
    this.screen.update();
};

Game.draw = function() {
    this.debug.draw();
    this.screen.clear();
    this.screen.draw();
};

Game.pause = function() {
    this.paused = (this.paused) ? false : true;
};

/*
 * Runs the actual loop inside browser
 */
Game.run = (function() {
    var loops = 0;
    var nextGameTick = (new Date).getTime();
    var startTime = (new Date).getTime();
    return function() {
        loops = 0;
        while (!Game.paused && (new Date).getTime() > nextGameTick && loops < Game.maxFrameSkip) {
            Game.update(nextGameTick - startTime);
            nextGameTick += Game.skipTicks;
            loops++;
        }
        Game.draw();
    };
})();

(function() {
    var onEachFrame;
    if (window.requestAnimationFrame) {
       onEachFrame = function(cb) {
          var _cb = function() {
                cb();
             requestAnimationFrame(_cb);
          };
          _cb();
       };
    } else if (window.webkitRequestAnimationFrame) {
       onEachFrame = function(cb) {
          var _cb = function() {
             cb();
             webkitRequestAnimationFrame(_cb);
          };
          _cb();
       };
    } else if (window.mozRequestAnimationFrame) {
        onEachFrame = function(cb) {
            var _cb = function() {
                cb();
                mozRequestAnimationFrame(_cb);
            };
            _cb();
        };
    } else {
        onEachFrame = function(cb) {
            setInterval(cb, Game.skipTicks);
        };
    }

    window.onEachFrame = onEachFrame;
})();

Even More Information

You can find a full working example, and all of the code here. I have convert this answer into a downloadable javascript framework you can build your games off from.

https://code.google.com/p/twod-js/

Kermis answered 16/5, 2013 at 6:3 Comment(4)
-1 This is a poorly formatted, poorly expressed rambling mess of an answer with countless spelling and grammatical errors. Please have a read of Answering How-To.Counterspy
Your answer is the most correct and up-to-date one on the page, but now you have way too much example code. Future readers will not care about the HTML layout, world.js, player.js, camera.js, or tile.js. Please only include the elements of your answer relevant to handling timing for a game loop using requestAnimationFrame.Irritability
i removed some of the code and classes as you suggessted. Please note that now this is not a fully working example, as you will need to add your world loading, player, and camera logic. The html layout as you call it, is actually the host page and is required by the example to show you you inject the game loop into the browser using webkitKermis
BTW, i came across this npm package which does the same stuff but rolled up into an easy to use module. github.com/IceCreamYou/MainLoop.jsKermis
F
24
setInterval(onTimerTick, 33); // 33 milliseconds = ~ 30 frames per sec

function onTimerTick() {
    // Do stuff.
}
Fulgurating answered 23/12, 2009 at 22:34 Comment(4)
You'd probably want to reverse the order of the first line and the rest. ;)Rhigolene
@Dav: There's no reason to do that. Function declarations take effect before the step-by-step code in the same scope.Go
causes jitters and is just a stub.Kermis
For modern browsers and various js game engines are now using requestAnimationFrameReactive
M
23

requestAnimationFrame is a great alternative available in most browsers now. It does what you're doing with setInterval, but in a baked in way. Probably the nicest thing about it is that it only runs while your tab is focused. That could be a reason not to use it if you want things to run in the background, but often (especially for render loops that only matter when seen) it's great to not be using resources when the tab's not active.

Usage is pretty simple:

function gameLoop(){
  window.requestAnimationFrame(gameLoop);
  Game.update();
}

I hate to post on old threads, but this is a top google result for "javascript game loop" so it really needs to include requestAnimationFrame.

Pollyfill for old browsers copied from paulirish:

(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };

    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());

Deeper explanation and usage tips on creativeJS

Megilp answered 28/5, 2015 at 19:58 Comment(1)
"I hate to post on old threads" StackOverflow isnt a forum, it is a dictionary. Questions are never old, unless the tech actually just doesnt exist anymore !Judoka
W
12

Many of these answers are outdated and suboptimal. The recommended way to do this now is using requestAnimationFrame.

For example:

let previousTime = 0.0;

const loop = time => {
  // Compute the delta-time against the previous time
  const dt = time - previousTime;

  // Update the previous time
  previousTime = time;

  // Update your game
  update(dt);

  // Render your game
  render();

  // Repeat
  window.requestAnimationFrame(loop);
};

// Launch
window.requestAnimationFrame(time => {
  previousTime = time;

  window.requestAnimationFrame(loop);
});

Fixed time-step

If you want to achieve a fixed time-step, simply accumulate delta-time until it reaches some threshold, then update your game.

const timeStep = 1.0 / 60.0;

let previousTime = 0.0;
let delta = 0.0;

const loop = time => {
  // Compute the delta-time against the previous time
  const dt = time - previousTime;

  // Accumulate delta time
  delta = delta + dt;

  // Update the previous time
  previousTime = time;

  // Update your game
  while (delta > timeStep) {
    update(timeStep);

    delta = delta - timeStep;
  }

  // Render your game
  render();

  // Repeat
  window.requestAnimationFrame(loop);
};

// Launch
window.requestAnimationFrame(time => {
  previousTime = time;

  window.requestAnimationFrame(loop);
});
Weinberger answered 8/6, 2019 at 13:56 Comment(1)
It seems like it might be poor form to use requestAnimationFrame to handle non-animation related/physics logic. Is there any official guidance you have seen on that, or any negatives/reason it might not be a great idea?Judoka
K
4

Yep. You want setInterval:

function myMainLoop () {
  // do stuff...
}
setInterval(myMainLoop, 30);
Keenan answered 23/12, 2009 at 22:35 Comment(0)
F
4

Would this do?

setInterval(updateGameState, 1000 / 25);

Where 25 is your desired FPS. You could also put there the amount of milliseconds between frames, which at 25 fps would be 40ms (1000 / 25 = 40).

Flower answered 23/12, 2009 at 22:35 Comment(2)
you prolly want to pass the function, not its resultGinnifer
does not handle drifting and will cause latency. Does not use webkitKermis
C
0

not sure how well this will work, but here is one method that uses a while loop and sleeps the thread. This is just an example, you can replace the game loop below with a better one, based on how you would do it in any other language. You can also find a better while loop here

let gameRunning = false;

async function runGameThread(){
    if(!gameRunning){
        gameRunning = true;

        // this function allows the thread to sleep (found this a long time ago on an old stack overflow post)
        const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

        // the game loop
        while(gameRunning){
            let start = new Date().getTime();
            gameUpdate();
            await sleep(start + 20 - new Date().getTime());
        }

    }
}

function stopGameThread(){
    while(gameRunning){
        try{
            gameRunning = false;
        }catch(e){}
    }
}

function gameUpdate(){
    // do stuff...
}
Caricature answered 19/1, 2020 at 21:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.