The Game Loop: Fundamentals of Video Game Code Architecture

Every modern game is built on top of a core engine code that provides the most basic functionality a video game needs. It provides services like graphics rendering, audio playback, input detection, memory management, physics, file system operations, animation, concurrency, error handling, events, and many more.

At the heart of every game engine runs a loop that executes the core code that allows the game to function. The game loop is an infinite loop that prevents the application from exiting unless the user requests it by pressing some form of button. Each iteration of the loop represents a single frame displayed on the monitor. The game loop has four general functions.

Game Loop Function 1: Capturing and Handling User Input

First, it captures user input, either as an event from the operating system or by manually checking the state of connected hardware components. User input can come in various forms such as keyboard key presses, mouse button clicks and wheel scrolls, joystick movement, controller buttons, or any other human interface devices connected to the system. The captured input events are then passed on to the game’s subsystems for further handling and creation of specific behaviors.

There are a few types of input that can be received from the input device. The most common type is a digital button, which has two possible states – pressed and released. This type of input can come from devices like keyboards, mice buttons, and console controller buttons.

Another type of input is analog data, coming from devices like joysticks. Simply put, analog input can have a range of values, as opposed to digital input, which is binary. A good example of this is the stick in a joystick device.

The stick can move on a 2D plane and produce a range of values on two different axes. When the stick is moved, the game does not receive the state of the stick, but rather the strength at which it was pulled on each axis. The game then reacts based on the strength of the input. Analog input can also come from devices like accelerometers and pressure buttons.

Game Loop Function 2: Updating All Entities in the Game

The second function of the loop is to update all entities in the game. Taking the user input into account, the code now has to update all object data for the next frame. The update function can include changing object transforms based on their velocity and other physics properties, creating or removing objects from the scene, modifying object-specific game parameters, etc.

Updating the game may sound like an easy task, but it’s actually very complex, especially in modern 3D games, where a scene may contain hundreds or even thousands of objects. For example, detecting collisions between thousands of objects requires a very efficient and robust physics system.

Modern games typically use an Entity-Component system (ECS) to manage objects currently found in the scene. In a nutshell, an entity-component system splits objects into their components and arranges similar components to be in the same place in memory.

For example, if a scene contains a thousand objects, and every object has a transform in 3D space, it is much more efficient to update all objects if their transform components are found in the same place in memory, as it leverages the CPU’s caching capabilities. This goes directly against the traditional Object-Oriented Programming approach taught in traditional programming, where each object encapsulates it’s own data.

The update process also needs to apply specific behavior for every object in the scene. This is done by attaching scripts to objects. A script defines the behavior of an object over time. During the update process, scripts are executed, and their data is modified to reflect the current state of the object.

During script execution, the code has to take into account objects that were created or destroyed in the same or previous frame. New objects that are not taken into account may miss collision detection or interaction with other objects, while destroyed objects can cause the code to break because of an attempt to access a deleted object.

After the user input is handled and the game data is updated, it’s time to render the scene and display it on the screen.

Game Loop Function 3: Rendering the Scene

The third function of the loop is to render the scene. This is usually the computationally intense part of the loop and sometimes creates drops in the number of frames drawn each second. Each frame you see on the screen is rendered by the GPU (unless the game starts with very specific CPU rendering settings). But the game itself, as with every piece of code, runs on the CPU. So how does the graphics information flow from the CPU to the GPU?

Nowadays, there are several Graphics APIs games use to communicate between the CPU and the GPU. Some notable examples of Graphics APIs are OpenGL, DirectX, Vulkan, Metal, and WebGPU. Each one of these APIs specifies a set of functions allowing the game’s code to transfer commands and data between the CPU and the GPU.

The game’s code uses these low-level functions, provided by the GPU manufacturer, to send the needed graphics data from the CPU to the GPU, each frame. After updating the GPU with the necessary graphics changes, the code will tell the GPU to start drawing the scene, at which point it will use the data stored in its memory to render the requested objects.

A standard computer monitor has a refresh rate of 60 Hz, which means it redraws the screen 60 times per second. We can’t really see the screen refreshing because our eyes integrate the information from multiple refresh cycles to perceive smooth motion. This integration happens because of a phenomenon called persistence of vision, where the image remains in our retina for a brief period after the light stimulus is removed.

If the monitor draws the screen 60 times per second, it means that the GPU must render the scene roughly 60 times per second as well. If the GPU renders less than 60 frames per second, the game will not run smoothly, and might hurt the player’s experience. So if each iteration of the game loop is a single frame, the loop has to execute all code inside it 60 times per second. This means that each iteration must be done in 16.66 milliseconds or less!

But this whole system creates a bit of a problem. If a computer with slower hardware runs a modern 3D game, the number of loop iterations it can execute each second can be far less than a faster computer. While faster hardware can easily run at 60 FPS and update the game at normal speed, slower hardware will update the game data at a lower speed, which will cause the game itself to run slower. This brings us to the fourth function of the game loop – frame rate control.



Game Loop Function 4: Controlling the Frame Rate

Let’s look at a simple example to understand the problem better. A standard game frame rate is 60 FPS, which means it executes roughly 60 game loop iterations per second. A game has a 2D player character that can move left or right. Pressing the right arrow moves the character 4 pixels to the right, and pressing the left arrow moves it 4 pixels to the left. Remember, user input is checked only once per frame, so the character will move exactly 4 pixels each frame.

On a fast computer, pressing the right key for 1 second should result in the player moving 240 pixels to the right. On the other hand, if the game runs on a slow computer at 30 FPS, the player will only move 120 pixels. This effectively makes the game speed dependent on the hardware.

While there is no easy way of making slow computers run faster, there is a relatively easy way of locking the game speed to a constant rate, regardless of the performance of the underlying hardware. This is done by coupling the entity update process with the frame rate.

At the beginning of each frame, a timer is started to measure the duration of that frame. The duration of a frame is famously known as the “Delta Time”. During the next frame, the delta time value is passed to the ‘Update’ method, which then subsequently passed down to all sub-modules. Then, each object needs to take the delta time into account when updating the transform, physics, or any other framerate-dependent variables.

Now we are no longer talking about moving the character a constant number of pixels each frame, but rather moving it at a certain rate, namely – pixels per second. To lock the character’s movement rate, all we need to do is multiply the target number of pixels per second by the delta time. In our example, we want the character’s movement rate to be 240 pixels per second. Here is the calculation:

Fast Hardware: 240 pixels * 0.01666 second/frame = 4 pixels*second/frame * 60 frame/second = 240 pixels
Slow Hardware: 240 pixels * 0.03333 second/frame = 8 pixels*second/frame * 30 frame/second = 240 pixels

You can see that the slower computer moves the character 8 pixels each frame, instead of 4 pixels, to compensate for the low frame rate. After 1 second, the character on both computers moved the same 240 pixels. So while the game on the slower hardware might not run as smoothly as on the faster hardware, the speed of the game is guaranteed to stay the same.

So the code can deal with the performance differences between computers, but what about limiting the frame rate on a single computer? After all, we don’t want games to run at the maximum possible speed; that’s a waste of resources.

Running at high speeds means that the game loop will be executed more than the monitor’s refresh rate, which in our previous example was 60 times per second. To limit the frame rate, the code will intentionally hold back the execution of the loop.

The code already measures the frame duration, and we know that the target duration of a frame is constant. So all that is needed is a non-blocking delay function that will wait for the remainder of the time and keep the actual total frame duration constant.

For example, if a game runs at 120 FPS, each frame takes 8.33 milliseconds to execute. If we want a frame rate of 60, we need to make the frame last for 16.66 milliseconds. So the code simply calls an external function that will cause a delay of another 8.33 milliseconds at the end of the loop, and now each frame takes 16.66 milliseconds, making the game run at a maximum of 60 FPS.

Implementing the Game Loop in Code

So how would the game loop look in code? Let’s take a look at some pseudo-code that represents the logic we discussed.

deltaTime = 0
while (true)
{
    frameStartTime = GetCurrentTime();
    HandleUserInput();
    if (QuitButtonPressed())
    {
        break;      // Exit the game loop
    }

    UpdateScene(deltaTime)
    RenderScene();

    // Calculate the frame duration
    deltaTime = GetCurrentTime() - frameStartTime;
}

This game loop is far from complete and certainly doesn’t represent modern games and game engines accurately. There are many more tweaks and optimizations that game developers apply when creating the infrastructure of a video game.

There is a problem in game physics which somewhat breaks the logic in this loop. Let’s say there is a very thin character running at 240 pixels per second from the left side of a room to the right side. Every frame, the character moves 8 pixels, assuming a low-end computer running the game at 30 FPS. At the center of the room, there is a wall with a width of 4 pixels.

Since the number of pixels the character moves each frame is larger than the width of the wall, the character can potentially be on the left side of the wall in one frame and appear on the right side of the wall on the next frame without detecting any collision between them.

The obvious conclusion from this example is that the physics system must check for collisions more than once per frame while interpolating the character’s transform. So in this example, if a game runs at 30 frames per second, collision needs to be checked about 60 times per second. But that’s not really possible because the character’s transform is only updated 30 times per second. So what’s the solution? I’ll leave that question for you to think about.

The Inspirtaion for This Article

This article was inspired by the incredible book “Game Engine Architecture” by Jason Gregory. This book will teach you everything about how modern video games and game engines work at the lowest level, including system architecture, development tools, debugging, math, physics, graphics rendering, animation, body collision detection, audio, concurrency and many more subjects.

This book is like a masters degree in computer science, with a focus on game development. It is not a book to read in a week and put back on a high shelf, but a book to learn, keep as a guide and consult with when needed.

As a software engineer and a game developer, I found it very valuable in my projects, so I highly recommend reading it. It will give you a detailed view of how these complex things, called video games, actually work. You can get it on Amazon.

If you’ve read the whole article, you’re awesome! To learn more about game development, visit the Night Quest Games Blog. There, you’ll find many articles about games and game development. Good luck!

Leave a Comment

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