JavaScript game development

Web is a convenient platform for a quick start in game development, especially for those of you who are familiar with JavaScript language. This is a feature-rich platform in content rendering and input processing from different resources.
First steps in game development for Web:

  • Sort out the game loop and rendering definitions.
  • Learn to process user input.
  • Create the main scene prototype of the game.
  • Add the rest of the game scenes.

Game Loop

Game cycle is a heart of the game, where user input and game logic processing occur along with current game state rendering. Schematically, game loop can be described in the following way:

game loop

As a code, the simplest implementation of the game cycle in JavaScript may look like this:

// game loop
setInterval(() => {
  update();
  render();
}, 1000 / 60);

Update() function is responsible for the game process logic and updating of game state, depending on the user input. Wherein, it absolutely doesn’t matter, with the help of which technologies the rendering itself is happening (Canvas, DOM, SVG, console etc.).

Please note that browser environment imposes certain restrictions on the work of this code:

  • Stable timer interval is not guaranteed, which means that the game process may occur with different speed.
  • The timer in inactive browser tabs may be stopped or repeatedly launched, when the tab is activated, which may lead to strange behavior of the game.
  • SetInterval is already outdated for such tasks. Today it is recommended to use requestAnimationFrame method, because it allows to achieve better productivity and decrease energy consumption.

Game loop implementation in browser

Let’s look into a few methods of game cycle implementation with the help of requestAnimationFrame approach. In the simplest version we will face certain issues that we’ll try to solve in the following implementations.

Simple but unreliable method

Use requestAnimationFrame and expect stable 60 FPS.

The code for such a game cycle could look like this:

requestAnimationFrame(() => {
  angle++;   // change the angle by 1 degree
  render();  // render the current state
  ...        // repeat the requestAnimationFrame call
});

It is worth considering smoothness of the game scene rendering (a so called „game brake”) and a rate of change in the scene (speed of events in the game).

According to specification requestAnimationFrame method should allow for rendering in the rate equal to display updating rate. Now it is often 60 FPS, however, in future it will probably allow for rendering frames at a higher rate. It is also useful to remember that some browsers support battery saving mode, one of the optimizations of which is requestAnimationFrame rate decrease.

It turns out that the specified FPS can not only be unstable but in some cases even hit the rate, twofold different than “perfect” 60 FPS — both upwards and downwards.

In the example below you can see how with primitive approach the game speed will depend on the frame rate – try moving the slider box:


  

demo

The result is extremely poor – game logic speed depends on the power and the load of device.

This approach is positively not recommended to use and it is shown here solely as an example.

Use RAF and estimate time among frames.

Let’s make our code more flexible – to do that we will track how much time has passed between a previous and a current requestAnimationFrame calls:

let last = performance.now();   // save the call time of the last frame in this variable 

requestAnimationFrame(() => {
  let now = performance.now(),  // set the current time
      dt = now - last;          // track the elapsed time among the frames

  angle += dt * 60 / 1000;      // change the angle in proportion to the elapsed time
  last = now;                   // save the time of the last frame rendering
  render();                     // repeat the requestAnimationFrame call
  ...                           // call requestAnimationFrame repeatedly
});

Now, at a sag or a change of performance the game speed will remain the same, except for the rendering smoothness that will change after all:


 

demo

The given approach works, but does it solve all the issues?

Use a fixed interval for the update()

The previous approach has really made our code more resistant to various requestAnimationFrame calls frequency, but with this method every property of the game logic will have to be modified in proportion to the elapsed time. It isn’t only very convenient but also does not fit to many games that use physics or intersection calculation, because in case of different update() calls frequency you can’t guarantee a full determinacy of the scene.

Is it possible to achieve a fixed interval if it is not supported by a browser? There is a way, but the code will have to become a little more complex:

let dt   = 0,                   // set the current time
    step = 1 / 60,              // the amount of time per frame
    last = performance.now();   // save the call time of the last frame in this variable

requestAnimationFrame(() => {
  let now = performance.now();  // set the current time
  dt += (now - last) / 1000;    // add the elapsed time difference
  while(dt > step) {
    dt -= step;                 // the main loop can call status update several times in a row
    angle++;                    // if more time elapsed than it was set per frame
  }
  last = now;                   // save the time of the last frame rendering
  render(dt);                   // render the current state
  ...                           // repeat the requestAnimationFrame call
});

The demo version is quite similar to the following example:



demo

Regular interval for update()

So, a fixed time step offers the following advantages:

  • Simplification of game logic code update().
  • Predictability of the game behavior, and accordingly, a possibility of creating a game scene replay.
  • Ability to slightly slow down or accelerate the game (slomo).
  • Stable work of physics.

Dependency of physics engines on FPS

If you intend to use physics engines, please keep in mind that the more frames per second they calculate, the higher simulation accuracy they will show. Some engines do not accept low FPS. In the example below you can compare simulation results in both low and overly high FPS. Please, use the slider box:



demo

The core of the game engine

There is one more issue to solve – inactive browser tabs. With the current code, if a user makes their tab inactive for a few minutes and then returns, the code for update() will be called many times for the entire time of absence while the game logic may “dash” far ahead. Of course, you can think through mechanisms like a game state pause but you should still get rid of a multiple update() call.

Similar cases may be controlled and they can solve a maximum delay among the calls to no longer than 1 second. Putting together everything mentioned above, we get a code that can be used as a template for creating a game:

let last = performance.now(),
    step = 1 / 60,
    dt = 0,
    now;

let frame = () => {
  now = performance.now();
  dt = dt + Math.min(1, (now - last) / 1000); // fixing the inactive tabs issue
  while(dt > step) {
    dt = dt - step;
    update(step);
  }
  last = now;

  render(dt);
  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

This code may be used as a base for a game cycle, which after only two functions will be left to realize – update() and render().

Various FPS for update() and render()

Game loop with a fixed time step allows to control a desired amount of FPS for game logic. It is quite useful because it lets decreasing the device load in games, where there is no need to calculate the logic 60 times per second. However, even with low FPS for the game logic you can continue the rendering with high FPS:



demo

Both of the squares change their position and angle <br/> in the frequency of 10 FPS and render in the frequency of 60 FPS.

In the example above, linear interpolation (LERP) is used. It allows to calculate intermediate values among the frames, which provides smoothness during rendering.

Using LERP is very easy — it will suffice to know the value of a certain property of the game object for two frames, previous and current, as well as to calculate in which time interval between the two frames the rendering is performed.
lerp
LERP makes it possible to obtain intermediate values for rendering when specifying a percentage from 0 to 1

Realization of LERP function:

let lerp = (start, finish, time) => {
  return start + (finish - start) * time;
};

Adding slow motion support

Having slightly altered the code of the game cycle, it is possible to achieve the slow motion support without changing the rest of the game code:

let last = performance.now(),
    fps = 60,
    slomo = 1, // slow motion multiplier
    step = 1 / fps,
    slowStep = slomo * step,
    dt = 0,
    now;

let frame = () => {
  now = performance.now();
  dt = dt + Math.min(1, (now - last) / 1000);
  while(dt > slowStep) {
    dt = dt - slowStep;
    update(step);
  }
  last = now;

  render(dt / slomo * fps);
  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

Let’s add slow motion to the previous demo. Using the slider box you can adjust the speed of the game scene:



demo

User input processing

Input processing in games is different from the one in classic web-applications. The main difference is that we do not react to various events at once, such as keydown or click, but we save the key state into an ordinary object:

let inputState = {
  UP: false,
  DOWN: false,
  LEFT: false,
  RIGHT: false,
  ROTATE: false
};

While a certain button is pressed, the value will be true, but as soon as the user releases the button, the value will return to false.

Then, when the next update() is called, we can react to the user input and change the game state:

let update = (step) => {
  if (inputState.LEFT)   posX--;
  if (inputState.RIGHT)  posX++;
  if (inputState.UP)     posY--;
  if (inputState.DOWN)   posY++;
  if (inputState.ROTATE) angle++;
};

Note: do not use pixels as a measuring unit for game logic. It will make more sense to create a constant, for instance const METER = 100; and calculate all the rest of the values from it, such as height of a character, speed etc. In this way you can disengage from rendering and do it for retina-devices painlessly. In the code samples in this article, for the sake of simplicity, the model value is directly bound to rendering.

Below is an example of user input realization. Please use W, S, A, D and R buttons for the movement and rotation of the square:



demo

Game structure. Scenes

Scenes in games are a convenient tool for code organization. They allow for dividing parts of the game into different components, each of which may have their own update() and render().

In the majority of the games you can observe the following set of scenes:
scenes

For scene organization it is handy to use ordinary classes, for example, the previous demo of the square control may be extracted into the following code:

class GameScene {
  constructor(game) {
    this.game = game;
    this.angle = 0;
    this.posX = game.canvas.width / 2;
    this.posY = game.canvas.height / 2;
  }
  update(dt) {
    if (this.game.keys['87']) this.posY--; // W
    if (this.game.keys['83']) this.posY++; // S
    if (this.game.keys['65']) this.posX--; // A
    if (this.game.keys['68']) this.posX++; // D
    if (this.game.keys['82']) this.angle++; // R
    if (this.game.keys['27']) this.game.setScene(MenuScene); // Back to menu
  }
  render(dt, ctx, canvas) {
    ...
    ctx.fillStyle = '#0d0';
    ctx.fillRect(posX, posY, rectSize, rectSize);
  }
}

Please pay attention to the setScene method call — it is generally located in the object of the game and allows for changing a current scene to a different one:

class Game {
  constructor() {
    this.setScene(IntroScene);
    this.initInput();
    this.startLoop();
  }
  initInput() {
    this.keys = {};
    document.addEventListener('keydown', e => { this.keys[e.which] = true; });
    document.addEventListener('keyup', e => { this.keys[e.which] = false; });
  }
  setScene(Scene) {
    this.activeScene = new Scene(this);
  }
  update(dt) {
    this.activeScene.update(dt);
  }
  render(dt) {
    this.activeScene.render(dt, this.ctx, this.canvas);
  }
}

With the use of such an approach you can create an intro scene and a scene menu for our enthralling game about the adventure of the square:



demo

Use W, S, A, D, R and ENTER for control.

Adding sound

Earlier, in order to play sounds, we had to use HTML5 <audio> tags, and solve issues of synchronization, rewinding sounds and simultaneous playing of several identical sounds. Now this is all behind, and for the work with sounds it is recommended to use a more flexible Web Audio API:

let context = new AudioContext();

fetch('sounds/music.mp3').then(response => {
  response.arrayBuffer().then(arrayBuffer => {
    context.decodeAudioData(arrayBuffer, buffer => {
      let source = context.createBufferSource();
      source.buffer = buffer;
      source.connect(context.destination);
      source.start(0);
    });
  });
});

You can get to know Web Audio API more closely with the help of the article at html5rocks.




demo

Instead of conclusions

If you are familiar with JavaScript, then using a little snippet for game loop realization, you can create a simple game within the shortest period of time. For the game cycle it is recommended to use a fixed time step, because it isn’t only convenient but also functional.

In case of a need to sort out how game engines work, it is advisable to give it a try and write a prototype of the game scene without the use of a third-party code. And if you require to quickly realize the game prototype, it makes sense to think over framework selection for this purpose as they already contain quite a number of tools that will allow to significantly speed up the development process.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>