A Note on Animation in Canvas with WebAssembly

Giving back control of the game rendering loop to the WebAssembly module. Partly.


In the previous post about building a 2D Video Game in AssemblyScript, we have inversed the control loop and gave the JS glue code the job to render the game animation.

This means, the browser, not the game, is in control of the frame rate:

const updateCall = () => update(wasm, mem, imageData, argb);
setInterval(updateCall, 100); // frame rate

// ...

function update(wasm, mem, imageData, argb) {
  // call the Wasm module to update the game
  wasm.update(control);

  // render the game on canvas
  argb.set(mem.subarray(0, WIDTH * HEIGHT));
  ctx.putImageData(imageData, 0, 0);
}

This works fine for the example, but in a real-world scenario, we usually don’t want to move the game settings (such as frame rate) out of the game logic.

Synchronous canvas rendering

A naive approach would be to put the rendering loop into the Wasm module:

export declare function render(): void

// ...

start(): void {
  // ...

  while (true) {
    this.update();  // update the game
    render();       // request rendering
    this.sleep();   // wait based on FPS
  }
}

The function render would be imported into the module from JS and would take care of the canvas rendering:

const wasm = await WebAssembly
  .instantiateStreaming(fetch('../build/optimized.wasm'), {
    render: () => render(wasm, imageData, argb),
    // ...

function render(wasm, imageData, argb) {
  const mem = new Uint32Array(wasm.memory.buffer);

  argb.set(mem.subarray(0, WIDTH * HEIGHT));
  ctx.putImageData(imageData, 0, 0);
}

The problem with this approach is that it won’t work. In fact, nothing will be rendered as calling ctx.putImageData is truly synchronous and will wait until the script ends. And that never happens because of while(true).

Giving control back to the game

Obviously, we cannot get rid of the rendering loop in JS but at least we can control the frame rate within the game.

We will use Window.requestAnimationFrame() for the loop that renders the game usually 60 times per second:

const updateCall = () => {
  update(wasm, mem, imageData, argb);
  window.requestAnimationFrame(updateCall);
}
window.requestAnimationFrame(updateCall);

60 FPS is definitely too much for our demo game. We have to control the rate in the game. We will check if the game is ready to update and only then we perform the update. We simply return otherwise:

update(control: Controls): void {
  if (!this.readyToUpdate()) {
    return;
  }
  // update the game
  // ...
}

private readyToUpdate(): boolean {
  const now = Date.now();
  if (now > this.lastUpdate + 1000 / FPS) {
    this.lastUpdate = now;
    return true;
  }
  return false;
}

Now, the browser still controls the animation loop, but the game defines its frame rate.

Check out the source code in an alternative branch on my GitHub.

Happy animating!