Create your own
Lesson illustration

Optimizing Game Loops with requestAnimationFrame

Hello! Welcome back to our JRPG engine-building journey.

In the last lesson, we designed the high-level architecture for our engine, establishing a blueprint for a game loop, a scene manager, and a rendering pipeline. We decided on a crucial pattern for our game loop: a fixed-step update with variable rendering. This ensures our game simulation is deterministic and stable, while the visual presentation remains as smooth as the hardware allows.

Today, we move from blueprint to foundation. Our learning outcome is to implement a game loop using requestAnimationFrame, managing state updates and rendering with delta time. We will write the code for the engine's heartbeat, turning the architectural theory from our last lesson into a working, ticking core for our game.

The Right Tool for the Job: requestAnimationFrame

In front-end development, you're familiar with different ways to schedule asynchronous code, like setTimeout and setInterval. For a game loop running in a browser, the clear winner is requestAnimationFrame.

How to make a game loop for your idle game

The article 'How to make a game loop for your idle game' has a great section comparing the different scheduling methods in the context of a game. It will quickly confirm why we're choosing requestAnimationFrame.

Please find and read the sections starting with 'Improvements: Maybe try another scheduler?', where you can review the scheduler options, and 'Real Better Timing with Animation Frames', where you will learn about animation frame timing. Focus on the pros and cons of each scheduler and why requestAnimationFrame is the most suitable for smooth, efficient animation.

As the article explains, requestAnimationFrame has two major advantages:

  1. Browser Optimization: It allows the browser to schedule the render for the optimal moment, often synchronizing with the monitor's refresh rate, leading to smoother animations and better performance.
  2. High-Precision Timestamp: It provides a high-resolution timestamp as an argument to our callback function. This is the elapsed time since the page was loaded and is perfect for calculating our delta time.

A First Attempt: The Variable Timestep Loop

Let's start with the most straightforward implementation. We'll create a loop that calculates the time elapsed since the last frame (deltaTime) and uses that value to update the game state.

let lastTime = 0;

function gameLoop(currentTime) {
  // If lastTime is 0, this is the first frame.
  if (lastTime === 0) {
    lastTime = currentTime;
  }
  
  const deltaTime = currentTime - lastTime;
  lastTime = currentTime;

  update(deltaTime);
  render();

  requestAnimationFrame(gameLoop);
}

// Start the loop
requestAnimationFrame(gameLoop);

This is simple, but as we discussed in the last lesson, it has a serious flaw: it makes the simulation non-deterministic. A large deltaTime (from a slow frame) can cause physics objects to take a giant step, potentially "tunneling" through walls or other objects.

To see a practical demonstration of this exact problem, the following video is excellent.

Writing a Proper Game Timing Mechanism in JavaScript

The video 'Writing a Proper Game Timing Mechanism in JavaScript' by Meth Meth Method provides a clear, code-driven demonstration of why a variable timestep is problematic.

Starting around the 50-second mark, watch the Pong demonstration. The video shows a Pong game where, with a large delta time, the ball moves so far in one frame that it passes completely through the paddle, failing the collision check. This is the exact 'tunneling' issue we want to avoid.

The Robust Solution: Fixed-Step Updates with an Accumulator

To solve the determinism problem, we will implement the pattern we designed: the game logic will update in fixed, discrete time steps, regardless of how much real time has passed. To do this, we introduce an accumulator.

The concept is simple:

  1. On every frame, we add the real deltaTime to our accumulator.
  2. We then run our update function in a while loop, as many times as we can "fit" a fixed timeStep into the accumulator.
  3. For each update, we subtract the timeStep from the accumulator.
  4. Finally, we call render just once.

This decouples the simulation from the frame rate. If the game lags and a large deltaTime occurs, the while loop will simply run more times to catch the simulation up to the current time.

This pattern is a cornerstone of modern game development. The following article provides the definitive explanation.

Game Loop ยท Sequencing Patterns

The 'Game Loop' chapter from the book Game Programming Patterns by Robert Nystrom explains this concept beautifully. We'll focus on the section that introduces the fixed-step implementation.

Please read the section titled 'Play catch up'. It introduces the concept of using a lag variable (our accumulator) and shows the pseudocode for the pattern. This is the exact logic we are about to implement.

Now, let's see how this looks in JavaScript, building on our initial code. The video we watched earlier implements this exact solution.

Writing a Proper Game Timing Mechanism in JavaScript

Let's return to the 'Writing a Proper Game Timing Mechanism in JavaScript' video to see the accumulator pattern put into practice.

About five minutes in, watch the video segment that walks through the implementation of the accumulator logic and explains how it solves the problem from the first segment. Pay close attention to the while loop and how accumulator and step are used.

Building Our Engine Class

Now, let's encapsulate this logic inside the Engine class we blueprinted in the previous lesson. This keeps our code organized and self-contained.

Here is the implementation of our Engine with the complete, robust game loop. We'll include placeholder update and render methods that simply log to the console for now.

class Engine {
  constructor() {
    console.log("Engine starting up...");
    
    // --- Timing State ---
    // The amount of time (in ms) that should pass between each update.
    this.timeStep = 1000 / 60; // We want 60 updates per second

    // Timestamp of the last time the game loop was run.
    this.lastTime = 0;
    
    // Accumulates elapsed time. When it exceeds timeStep, we run an update.
    this.accumulator = 0;

    // We'll bind the gameLoop method to `this` instance to ensure
    // it has the correct context when called by requestAnimationFrame.
    this.gameLoop = this.gameLoop.bind(this);
  }

  // A placeholder for our game logic update
  update(deltaTime) {
    console.log(`Updating with deltaTime: ${deltaTime.toFixed(2)}ms`);
  }

  // A placeholder for our rendering logic
  render() {
    // We will implement this in the next lesson!
    // console.log("Rendering..."); 
  }

  // The core game loop
  gameLoop(currentTime) {
    // Initialize lastTime on the first frame
    if (this.lastTime === 0) {
      this.lastTime = currentTime;
    }

    // --- Core fixed-step logic ---
    const deltaTime = currentTime - this.lastTime;
    this.lastTime = currentTime;
    this.accumulator += deltaTime;

    while (this.accumulator >= this.timeStep) {
      // Run the simulation with a fixed time step
      this.update(this.timeStep);
      
      // Decrease the accumulator by the fixed step
      this.accumulator -= this.timeStep;
    }
    
    // Always render once per frame
    this.render();

    // Schedule the next frame
    requestAnimationFrame(this.gameLoop);
  }

  // Public method to start the engine
  start() {
    requestAnimationFrame(this.gameLoop);
  }
}

// --- To run the engine ---
// const gameEngine = new Engine();
// gameEngine.start();

This code is the living implementation of our architectural design. It's stable, efficient, and provides the predictable foundation we need for game mechanics like movement, combat timers, and status effects.

Test your understanding!

Imagine our timeStep is set to 16ms. The game hits a performance spike, and a single frame takes 50ms to execute (so the deltaTime for that frame is 50).

  1. How many times will the update() method be called for that frame?
  2. What will the value of accumulator be just before render() is called?
Show answer
  1. The update() method will be called 3 times.
    • Initial accumulator = 50.
    • After 1st update: accumulator = 50 - 16 = 34. (34 >= 16, so loop continues)
    • After 2nd update: accumulator = 34 - 16 = 18. (18 >= 16, so loop continues)
    • After 3rd update: accumulator = 18 - 16 = 2. (2 < 16, so loop stops)
  2. The value of accumulator will be 2ms. This leftover time is carried into the next frame, ensuring no time is lost.

Conclusion

In this lesson, we have successfully built the heart of our game engine. We've gone beyond theory and implemented a robust, professional-grade game loop from scratch.

Here are the key takeaways:

  • We use requestAnimationFrame to schedule our game loop, letting the browser optimize rendering for maximum smoothness and efficiency.
  • We've implemented a fixed-step update loop using an accumulator. This critical pattern ensures our game simulation is deterministic and runs at a consistent speed, regardless of the user's hardware performance.
  • Our engine logic is now cleanly encapsulated in an Engine class, providing a start method to kick things off and an internal gameLoop that perpetuates itself.

We now have a ticking clock. The next logical step is to make it draw something.

Preview of the next lesson:

With our game loop in place, we will bring our engine to life visually. In the next lesson, we will implement a canvas rendering system for drawing sprites from a tileset and spritesheet. We'll finally see something on the screen

Can't find a good explanation? Sign up and we'll make it for you

Sign up