Thoughts on the Javascript game loop.

The game loop is the heart of the video game : each ‘pulse’, any video game calls a function that will :

  1.  handle user input.
  2.  update the world of the game.
  3.  draw the world on the screen.

the looping function is called run and is called by a timer, typically 60 times a second.
Quite often the user input handling and the world update is done with a single function, update. In Javascript, most games spend more time on drawing than on the update,
which gives a sequence diagram which looks like that :

SeqDiag

The number of images per second is the frame rate. Any frame rate below 15 images per second is not feeled as fluid by the player.

The common javascript code could look like :

var ga={}; // Game Alchemist namespace.

ga.Game = function(newFps) {
  this.fps = newFps;
  this.frameTime = 1000 / newFps ;
  this.runInterval = null;
  this.ctx = null;
};

ga.Game.protoype = { 
        run         : function () {       
                                  this.update (this.frameTime);
                                  this.draw   (this.ctx);
        },
        launchGame  : function() {
                        this.init(); 
                        this.runInterval 
                            = setupInterval(this.run.bind(this)) ;
        },
        init         : function( some parameters ) {
                              // get the 2D Context
                              // initialize the world
        }, 
        update       : function(frameTime) {
        },
        draw         : function(ctx) {
        }
};

So this would be a basic game loop. But as obvious as this code might seem, we can improve it…

First Issue : This will not work on every screen.

Some screens have a 50Hz refresh rate. Some 60, some others, on phone, are 30Hz, some high-end or 3D monitors are 75Hz, 100Hz or more. How can we match all those rates easily ??

Hopefully, we have a function that allows us to synchronise on the display : requestAnimationFrame. It works somehow like setTimeout : the argument you give is the function that will get called next time the display is available for a draw. So you cannot miss the next display refresh : it will be noticed to you by requestAnimationFrame. ( Some might know this principle under the name of ‘V-sync’, or vertical synchronisation.)

Rq : You might want to bind() your run function to have it keep its context.  Remember that bind() creates a new function each time, so in order to create no garbage, bind the run() in the init and use the bound function.

Now the code will look like :

ga.Game = function(newFps) {
  this.fps = newFps;
  this.ctx = null;
  this.boundGameRun = null;
};

ga.Game.protoype.init = function() {
  // get the context
  // init the world
  this.boundGameRun = this.gameRun.bind(this, this.ctx);
};

ga.Game.prototype.gameRun = function() {
  this.run();
  requestAnimationFrame(this.boundGameRun);
};

Second issue : We should draw, then update. In this order. .

When the game starts, it will initialize all its values (the player/monsters position, speed, … ) then it will call run, so if we follow the update-draw order, the first thing done … will be to update the world : the inital status will never get drawned.
So we have better permutate the order within the run function:  a draw, then an update.

Another reason to switch order : The draw is NEVER on time with the update-draw order.

If the update is before the draw, whatever the time the update takes, this time will add up BEFORE the drawn is made so the drawing will never be on time, right when the display is available -remember it is the purpose of requestAnimationFrame to say ‘hey, the screen is ready’, so when it is ready… use it with no delay !

Notice that you don’t have all the frame time to draw. First thing is that, if you are drawing in x ms, you draw should start before 20 – x ms. But this might not be enough : most devices have double or triple buffering; and have a given time range during which they can draw. Otherwise the effective display will be in fact postponed, leading to various strange artifacts. Again : jump on the train as soon as possible.

Last good reason to revert update/draw order : the Garbage collector

If the system needs to reclaim so memory, then a garbage collection occurs, which stops the current processing for a short time (and sometimes not so short).
But guess what ? when an intensive update occurs, it raises the chances that the garbage collector will be invoked, and the update will take even longer, thus making the two consequent draw nearer… before returning to the normal frame time. One way of describing the effect is that the game will seem to be very fast the time of a glimpse.
On the other hand, the draw() does not create garbage, and does computation. Put the draw first, and it will not create garbage, then the update that follows will create garbage, then during the free time that follows, the garbage collector can ‘breathe’ and use the free time to garbage collect. With the right order we minimise the chances that the garbage collector occurs before a draw and get noticed.

So from now on we’ll use the good order : draw, then update.
Game.run becomes :

Game.protoype.run = function() {
       this.draw(this.ctx);
       this.update(this.frameTime);
};

But even for a basic game loop, relying on requestAnimationFrame introduces some issues that leads us to handle time by ourselves.

Why we nee to handle a game time

1) Because we don’t know about the time any more

With requestAnimationFrame, the loop might be called 20 to 100 times a second. You cannot do any fixed computation, like for instance this one on position :

   if (rightKeyPressed) position.x = position.x + 10;

If you made this loop with a 60Hz device in mind, it will be 3 times slower on a 20Hz one, and almost 2 times faster on a 100Hz screen.
So you need to know the frame time, and do your computation based on this time :

if (rightKeyPressed)  position.x = position.x 
                                      + velocity.x * frameTime ;

2) Because there are very fast screens.

If we have a very fast screen, for instance a 100Hz screen, we need to skip one frame on two, and switch to 50Hz, because the 100Hz refresh will drive the CPU and its fan crazy, your game will eat quite some power. Not so good. We need to be able to detect a too fast frame rate.

3) Think about slow devices / long update() : frame miss.

If the device is slow or the update is taking time, or the draw, even, is very long, we might not always be able to keep the fps pace, and miss some frame, so when relying on requestAnimationFrame, counting the frames cannot give you the time elapsed, even if you know the screen refresh rate. So we need to know each time how many time REALLY elapsed since last update.

4) Sudden slow down / Pause of the game
If the user switch tabs in a Browser, or do some operation sudddenly takes all CPU, the game will stop without knowing it. Same happens if the game is paused (which happens automatically on some browsers/devices when focus is lost).
Notice that there are quite some common scenarios that require a pause : your player looking in the inventory / looking a map / a part of the plot is revealed / …
Whatever the sleep time the game should just quietly resume, and consider just one frame elapsed.

So we need a small object that will handle the time, and measure the frame time. This object will also keep track of the current game time.

ga.gameTime =  {  // we define just an object since 
                  //  there is only one instance.
  lastTime     : Date.now(),
  frameTime    : 0 ,
  typicalFrameTime : 20,
  minFrameTime : 12 , 
  time         : 0
};

// move the clock one tick. return true if new frame, 
//      false otherwise.
 ga.gameTime.tick = function() {
    var now = Date.now();
    var delta = now - this.lastTime;
    if (delta < this.minFrameTime ) return false;
    if (delta > 2*this.typicalFrameTime) { // +1 frame if too much time elapsed
       this.frameTime = this.typicalFrameTime;
    } else
    {  this.frameTime = delta;      
    };   
    this.time+=this.frameTime;
    this.lastTime = now ;
    return true;
}

ga.Game.prototype.gameRun = function() {
  if (!gameTime.tick()) {
                     requestAnimationFrame(this.boundGameRun);
                     return; }
  this.run();
  requestAnimationFrame(this.boundGameRun);
};

ga.Game.protoype.run = function() {
       this.draw(this.ctx);
       this.update(gameTime.frameTime);
};

Beware : from now on, you should use game time and not the real time in all your time related computations.

Imagine we have a boucing ball. x and y defines the position on screen ( say x=0..640, y=0..480 ) and vx, vy  defines velocity. The update of the ball position shouldn’t be a constant increase, but rather depend on the current frame time.

Ball update should looks like :

  Ball.update = function ( frameTime) {
                               this.x = this.x + vx*frameTime;
                               this.y = this.y + vy*frameTime;        };
// velocity { vx, vy } is set in Kpixel per millisecond in this example.
// frameTime = ga.gameTime.frameTime

Another example of time dependency : Imagine now you have a bouncing object, the code might loook like :

SomeObj.update = function( frameTime ) {
       this.x = / some constant /;
       this.y = this.y0 + 
                this.amplitude * sin( this.freq * Date.now());
};

The bad thing about this code is that, if a pause ever happens, when the game resume, Date.now() will have a random value, hence the bouncing object will suddenly jump to an other value. The only way to avoid this is to rely on game time, which will stop when the game pauses.
New code :

SomeObj.update = function( frameTime ) {
        this.x = / some constant /;
        this.y = this.y0 + 
           this.amplitude * sin( this.freq * ga.gameTime.time ); 
};

A remark though : if you plan on implementing multiplayer games, you should NOT skip frames when a too big time elapsed. And you should have an update function that can handle a big time elapsed properly. Which means, on the other hand, have a program that can deal with a player’s momentary silence.

What about handling Update on a different timer ?

Some people suggest that the only way to handle properly the logic and the draw is to have them run on separate timing : the draw would be synced on rAf, while the update would have its own timer.

So first thing about this : do NOT rely on setTimeout to perform a  regular update. setTimeout’s accuracy might be less than 12ms on some browsers/devices, and since 50Hz, for instance, is 20ms, this is just a no-go, and i don’t even mention the overhead of setting a timer : rely on setInterval, way way more accurate.

But if you think about handling update on a different timer, just ask yourself : why would i draw again when no update occured ? or why would you update when you cannot have no time to draw the update result ?

The arguments against rAf are that 1) it might slow down if battery policy requires so, and 2) it might even stop if the game gets out of focus. But why would a game perform some computations if they do not lead to a display refresh ?
The real solution to this real problem is : you should handle game time by yourself, and perform computations based only on the game time. And the second aspect of the solution is to resume properly after a long time of inactivity, for multiplayer games.

For a single player game, resuming properly is simple : just resume ONE frame later when the player comes back.
For multi-player game, the answer is more complicated : and it might be in fact more simple to handle iteration by small steps only : imagine a game where trees grows, get cut, … in ‘real’ time. So in this case we would have an update loop running on a setInterval, which will indeed perform update *only if* the rAf update did not happened for a while. – In fact this might be the subject of a whole article, so i’ll stop here 🙂 –

But let’s get back to the point : having a separate timer for updates and draws leads to a much higher complexity, since you must both get sure the useless draw / useless updates / or the synchronisation logic in between update and draw is not leading the solution to be worse than the problem…

Some code you might copy-paste

So here’s a piece of code you might use to do a simple-yet-not-so-bad animation loop :

(function() {
  var w=window,   foundRaF = w.requestAnimationFrame ||
                   w.mozRequestAnimationFrame ||  w.webkitRequestAnimationFrame || 
                   w.msRequestAnimationFrame  ||  function (cb) { setTimeout (cb, 16) };
  window.requestAnimationFrame = requestAnimationFrame;
})();

var ga = {};

ga.gameTime =  {  // we define just an object since 
                  //  there is only one instance.
  lastTime     : Date.now(),
  frameTime    : 0 ,
  typicalFrameTime : 20,
  minFrameTime : 12 , 
  time         : 0
};

// move the clock one tick. return true if new frame, 
//      false otherwise.
 ga.gameTime.tick = function() {
    var now = Date.now();
    var delta = now - this.lastTime;
    if (delta < this.minFrameTime ) return false;     if (delta > 2*this.typicalFrameTime) { // +1 frame if too much time elapsed
       this.frameTime = this.typicalFrameTime;
    } else
    {  this.frameTime = delta;      
    };   
    this.time+=this.frameTime;
    this.lastTime = now ;
    return true;
}

ga.Game = function(nCtx, nUpdate, nDraw) {
  this.ctx = nCtx;
  this.update = nUpdate;
  this.draw = nDraw; 
  this.boundGameRun = this.gameRun.bind(this);
};

ga.Game.prototype.gameRun = function() {
  if (ga.gameTime.tick()) { this.run(); }
  requestAnimationFrame(this.boundGameRun);
};

ga.Game.prototype.run = function() {
       this.draw(this.ctx);
       this.update(ga.gameTime.frameTime);
};

var myUpdate = function(dt) {
  // put here your update computations, relative to dt	
};

var myDraw = function(ctx) {
  // ...
};

var myGame = new ga.Game(ctx, myUpdate, myDraw);

myGame.gameRun();

That’s about it, thanks for reading, and any comment welcome.

Advertisements
This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s