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:
- 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.
- 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:
- On every frame, we add the real
deltaTimeto ouraccumulator. - We then run our
updatefunction in awhileloop, as many times as we can "fit" a fixedtimeStepinto theaccumulator. - For each update, we subtract the
timeStepfrom theaccumulator. - Finally, we call
renderjust 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).
- How many times will the
update()method be called for that frame? - What will the value of
accumulatorbe just beforerender()is called?
Show answer
- 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)
- Initial
- The value of
accumulatorwill 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
requestAnimationFrameto 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
Engineclass, providing astartmethod to kick things off and an internalgameLoopthat 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