G-Engine #3: Game Loop

In my last G-Engine post, I did some setup work and finally got a basic OS window appearing that could be moved around, minimized/maximized, and closed. Good start!

In this post, we’ll do a bit more planning, and then we’ll structure our code into a high-level class (GEngine) that’ll be more conducive to building an engine than just shoving everything into the main function. We’ll also implement our “delta time” calculations, which will be critical for updating the state of our game as we move forward.

A Bit More Planning

With any coding project, you’ve got to make a few decisions before moving forward with a flurry of coding. Not all these things need to be locked down before you begin, but I thought I’d share a few additional considerations I took into account.

Coding Conventions

I chose the following high-level coding conventions:

  • Files and classes are named in UpperCamelCase (e.g. GEngine or InputManager), and the file name reflects the primary class in that file.
  • Headers use a .h extension, and source files use a .cpp extension (e.g. GEngine.h and GEngine.cpp).
  • Variables are named in lowerCamelCase (e.g. screenWidth, currentPos).
  • Class member variables have an m prefix (e.g. mRenderer, mInputManager). Other variants: global variables have a g prefix and static variables have a s prefix.

Header Layout

The layout of a header is in this order:

  • Comment with description of class.
  • #pragma once, to avoid multiple header includes.
  • Standard library includes (like <vector>), alphabetically.
  • Project includes (like "InputManager.h"), alphabetically.
  • Forward declarations (like class MyClass;), alphabetically.
  • Any supporting structs or classes for the main class.
  • The main class.
    • Public variables and methods (statics first)
    • Private variables and methods (statics first)
  • Any “inline” function definitions that don’t fit well into the main class body.

I use #pragma once instead of standard include guards, since they are simpler and widely supported. But standard include guards are fine too, if you prefer.

The order of my includes is somewhat arbitrary, though based on some guidance I’ve read around the internet. I think the most important thing here is to be consistent about it, so that each file isn’t completely different.

I also choose to put my “forward declarations” at the top of the file, instead of inline at the point of first use. I think this is cleaner, and makes the forward declarations clearer and easier to modify.

Within a class, I put public methods and variables above private ones. The main reason for this is to emphasize the public interface of the class. C++ requires both the public and private parts of an interface to be in the header file - ideally, the private bits would all be in the cpp file, but we are limited by the language in this regard.

There is a reasonable question about whether you should order functions and variables by public/protected/private access type, or by logical similarity of the functions. I’ve boldly decided on the former - actually, if a class is only supposed to have a single responsibility, then everything should be logically similar anyway (though this doesn’t always pan out in practice). We’ll see how it goes!

Implementation Layout

The layout here is mostly similar to the header layout, with a few small differences:

  • No #pragma once line.
  • The very first #include should be the header file. Otherwise, header orders are the same.
  • Ideally, functions appear in the .cpp file in the same order they appeared in the .h file.

That’s enough planning for now…let’s move on to the good stuff!

The Engine Class

At the end of the last post, we had all our code just sitting in our main function. That’s OK for small programs, utilities, code snippets, or samples. It is, however, not how we want to be structuring our code for a complicated program like a game engine!

How shall we structure our code? Well, we want to move in an object-oriented direction. So, splitting our code into a few C++ classes is a good start. Ideally, each class will have a single, well defined responsibility.

For now, we don’t have much code. We’ll start by creating a single class as a starting point: GEngine.

Our GEngine class is our engine’s “point of entry” class - it will be responsible for initializing, and then running, and then shutting down our entire engine.

Our main() function will create our GEngine instance, and then call three functions, in order:

  • Initialize()
  • Run()
  • Shutdown()

And that’s pretty much it for main() - anything else of interest or consequence will happen in these three GEngine functions, or in functions called by these functions.

Initialize() and Shutdown() are two sides of the same coin. One will be responsible for initializing all the engine’s subsystems, and the other will be responsible for tearing it all down when the program is exiting.

Initialization tends to encompass a number of start up tasks for the application. For example, our window creation would happen during initialization. Other initialization tasks might include initializing the rendering system (OpenGL, DirectX, etc), preparing input devices, creating memory allocators, and verifying that assets are available for use.

It’s possible for initialization to fail. Have you ever launched a game, only for it to crash immediately with an error message box? That’s likely what we’d do if the initialization steps failed! Initialization can fail for obvious reasons (the game’s asset files are missing) or very esoteric reasons (couldn’t initialize graphics due to a driver issue).

The shutdown process will usually do the opposite of what initialization did, and often in reverse order. Created an OpenGL context or a window during initialization? Destroy or release those resources here. Allocated a memory pool on engine startup? Delete it here.

If something in shutdown fails…well, the engine is already in the process of shutting down. You can probably keep going and hope the operating system will handle it for you. Sometimes, developers don’t realize that an error occurs on shutdown, since the game is in the process of closing anyway. Displaying an OS error dialog or writing the error to the output or a log could be a good way to catch errors here. If running with the debugger connected, it’s also pretty obvious.

That leaves the Run() function, which is where we will implement our “game loop.”

The Game Loop

The info in this section is very similar to what we teach in ITP 380 at USC. Thanks to Professor Sanjay Madhav for presenting this information so clearly and cleanly in his original slides!

A C++ program begins when main() is called, and it ends when main() returns. If you create a very simple “Hello World!” C++ application, you’ll find that the program quits almost immediately after starting, living long enough only to output “Hello World!” to your console.

To turn this program into something that doesn’t just quit right away, we need to add a loop somewhere. This is a loop that runs repeatedly for the entire duration of our application. The loop stops our program from reaching the end of main(). We only break out of the loop when either the user signals they want to quit, or some unrecoverable error occurs.

int main(int argc, const char * argv[])
{
    // Our loop, which stops the program from closing.
    while(true) { }

    // Done!
    return 0;
}

The above code snippet runs, and it never exits, and furthermore, you will need to force-quit this program! So, not a great program, but it is the first thing we need to move towards a game loop.

Everything that we do in our game will be contained within this loop. At a high-level, this loop will do three things, in this order:

  1. Process Input
  2. Update the Simulation
  3. Generate Outputs

Each step is vital to having a game, and must occur in this order.

A game must process inputs to be interactive - otherwise, it’s just a non-interactive simulation.

The simulation (aka “state of the game world”) must update regularly, and that update must be, in part, based upon the inputs from step 1. If not, we again have something that is not really interactive.

Outputs, such as graphics and sound, are necessary as a form of feedback for our senses. Without this data, we don’t see that the simulation is updating, and we don’t see our input’s effects on the game world, and so…it’s not a very fun game.

The order is important because each step relies on the previous. Our simulation needs inputs to update, and we need to know the latest simulation state to generate accurate outputs. Additionally, inputs rely on the generated outputs, since you, as a player, will make decisions about what inputs you should perform based on the output of the previous loop.

It’s a circle of life. A feedback loop.

Implementing the Game Loop

Our game loop, as well as initialization and shutdown, will be orchestrated by our GEngine class.

The header file:

#pragma once
#include <SDL2/SDL.h>

class Engine
{
public:
    // Inits and shuts down the engine.
    bool Initialize();
    void Shutdown();

    // Runs our game loop.
    void Run();

private:
    // Is the engine running? While true, we loop.
    bool mRunning = false;

    // Our SDL window handle.
    SDL_Window* mWindow = nullptr;

    // The three phases of our game loop.
    void ProcessInput();
    void Update();
    void GenerateOutputs();
};

Based on this structure, we can split our basic window code from the last post between these various functions. The implementation file:

#include "Engine.h"

bool Engine::Initialize()
{
    // Init SDL video subsystem.
    if(SDL_InitSubSystem(SDL_INIT_VIDEO) != 0)
    {
        return false;
    }

    // Create a window.
    mWindow = SDL_CreateWindow("GK3", 100, 100, 1024, 768, 0);
    if(window == nullptr)
    {
        return false;
    }

    // Initialized successfully.
    return true;
}

void Engine::Shutdown()
{
    // Destroy created window.
    if(mWindow != nullptr)
    {
        SDL_DestroyWindow(window);
    }

    // De-initialize SDL.
    SDL_Quit();
}

void Engine::Run()
{
    // We are running!
    mRunning = true;

    // Loop until not running anymore.
    while(mRunning)
    {
        ProcessInput();
        Update();
        GenerateOutputs();
    }
}

void Engine::ProcessInput()
{
    // We'll poll for events here. Catch the quit event.
    SDL_Event event;
    while(SDL_PollEvent(&event))
    {
        switch(event.type)
        {
            case SDL_QUIT:
                mRunning = false;
                break;
        }
    }
}

void Engine::Update()
{
    //TODO: Update the simulation.
}

void Engine::GenerateOutputs()
{
    //TODO: Generate the outputs.
}

This effectively splits our code from the first post into a more structured class. Note that we now need to store our SDL_Window* as a class member variable, since it is created in Initialize() and then destroyed in Shutdown().

The last step is to update main() to call these functions:

#include "GEngine.h"

int main(int argc, const char * argv[])
{
    // Create the engine.
    GEngine engine;

    // If init succeeds, we can "run" the engine.
    // If init fails, the program ends immediately (skip Run()).
    bool initSucceeded = engine.Initialize();
    if(initSucceeded)
    {
        engine.Run();
    }

    // Clean everything up.
    engine.Shutdown();

    // Program exits.
    return 0;
}

One mildly interesting question: should we create the GEngine in our main function, which allocates it on the stack? Or should we create it as a global variable, which puts it in our executables “data” segments (and thus, allocates it in memory before the program starts running)?

In fact, either will work. There may be some benefit one way or the other - I found that using a global variable increases the executable size slightly, but it avoids allocating the engine on the stack. At this early stage, it’s hard to say what’s better. I’ll err towards avoiding global variables for now. But on the flip side, perhaps I shouldn’t waste stack space! We’ll see.

Frames and Delta Time

The game loop that we’ve defined with just run over and over and over until the game quits. Each time the game completes one loop, we call that a frame.

At the moment, our loop is extremely simple, so your computer is likely able to run this loop many, many times per second. However, as we start to add more code to be executed during this loop (such as input processing, world updates, and graphics rendering), it will take longer and longer for your computer to execute all those instructions and complete a single frame.

The number of times per second that your computer can complete this loop is referred to as the frames per second or FPS of your game.

The number of frames per second has an effect on the gameplay experience. If this number is too low, the game will appear choppy or unresponsive, since the game is not completing game loops quickly enough. If this number is too high, it’s less problematic, but can still cause issues, like excessive power usage or battery drain (doing a lot of work per second = more power used).

There are common “target” values for FPS. Usually, the minimum desired value is 30 FPS, which equates to each frame taking 0.033 seconds. A common target is 60 FPS, where each frame takes 0.016 seconds. Some games even target 120 FPS, where each frame takes 0.0083 seconds!

A higher FPS means that you are completing more frames per second; thus, the duration of each frame gets smaller and smaller as your FPS increases. These two equations can be used to calculate FPS or seconds per frame:

secondsPerFrame = 1 / framesPerSecond
framesPerSecond = 1 / secondsPerFrame

The amount of time it takes for a frame to complete is often called the frame delta time or, more simply, delta time. It is also sometimes called a time step. It is the change in time, usually in seconds, that has occurred from the start of the previous frame to the start of the current frame. Obviously, you only know this value after the frame has completed!

The delta time value is very important to us. It is one of our primary tools for updating our simulation each frame.

Let’s say we have an object in our game that moves at a speed of 10 m/s. Each frame, we need to change its position by some amount to give the appearance of motion. But by what amount? To calculate a position change from a velocity, we need to know how much time has passed since the last position change (the delta time). At 30 FPS, 10m/s * 0.033s = 0.33m change in position. At 60 FPS, 10m/s * 0.016s = 0.16m change in position. The change in position applied depends on how long ago we last updated the position.

Some older games from the 80s or early 90s did not take delta time into account, and were instead built with a particular CPU speed in mind. As a result, such games are considered to be frame locked, which means that they only function correctly if they run at a particular FPS. Not good!

There are still scenarios today where it makes sense to either “frame lock” your game (e.g. this game ALWAYS runs at 30 FPS), or have a “fixed update loop” (e.g. this update loop runs every 0.03 seconds). Both these approaches are actually somewhat different from “assume a frame lasts for X seconds,” which is what older games sometimes did. You should make sure you don’t make the same mistake!

For now, we are going to focus on calculating a basic “variable time-step update loop”. To do this, we just have to keep track of what time our previous frame started, look at our current time, and calculate the difference between the two. And we do that every frame.

Calculating Delta Time

The calculation of delta time is not particularly complicated, BUT there are several potential pitfalls and choices to make!

First, here is our most basic calculation:

void GEngine::Update()
{
    // Tracks the last ticks value each time we run this loop.
    static uint32_t lastTicks = 0;

    // Get current ticks value, in milliseconds.
    uint32_t currentTicks = SDL_GetTicks();

    // Calculate change from current to last, and convert to seconds.
    uint32_t deltaTicks = currentTicks - lastTicks;
    float deltaTime = deltaTicks * 0.001f;

    // Save ticks value for next frame.
    lastTicks = currentTicks;
}

I’m making use of a static variable to hold “lastTicks”. This is doable because (1) the value persists between function calls, (2) the value is only used in this function, and (3) we only ever have one GEngine instance. A very specific case where a static function variable is useful! It could also be a member variable, however.

SDL_GetTicks is an SDL function that returns the number of milliseconds since SDL_Init was called. It is basically a wrapper around the platform-specific code we’d have to write to get this data from the operating system. The Windows code for this uses QueryPerformanceCounter, while the Mac/Linux code uses clock_gettime, mach_absolute_time, or gettimeofday, depending on what’s available.

To calculate our delta time, we take the difference between our current and last values, convert to a float, and multiply by 0.001f (or divide by 1000.0f, if you prefer). This converts our milliseconds delta to a seconds delta.

Floating-Point Approximation Concerns

We keep track of currentTicks and lastTicks as unsigned 32-bit integers and subtract them to get a precise (as possible) millisecond difference between the current frame and the previous frame. We then convert this to a floating-point value, in seconds, after calculating the difference.

I recommend keeping currentTicks and lastTicks as integers until after calculating the difference. This is due to floating-point approximation issues that will start to occur as these two values become large - and remember, SDL_GetTicks returns the number of milliseconds since the start of the program, so these values WILL get larger and larger over time!

To see this problem in action, consider this sample code:

// Get current ticks (milliseconds since program start).
uint32_t currentTicks = SDL_GetTicks();

// Let's simulate REALLY LARGE tick values with this addition value.
uint32_t addition = 100000000;

// Really large float current/last ticks.
float curTicksFloat = currentTicks + addition;
float lastTicksFloat = lastTicks + addition;

// Really large integer current/last ticks.
uint32_t curTicksInt = currentTicks + addition;
uint32_t lastTicksInt = lastTicks + addition;

// Calculate diffs (our delta time) in milliseconds.
float actual = currentTicks - lastTicks;
float floatVersion = curTicksFloat - lastTicksFloat;
float intVersion = curTicksInt - lastTicksInt;
SDL_Log("Actual: %f Float: %f, Int: %f", actual, floatVersion, intVersion);

// Save current ticks to use next loop.
lastTicks = currentTicks;

The addition value simulates the ticks values we’d get after running the program for 100,000,000 milliseconds (about 27.7 hours). If our addition variable is smaller (less than about 100,000,000 milliseconds), then the various results do not differ - that’s good!

But above 100,000,000 or so, we start to see some variation in the returned values. The “float” version strays further and further from the correct values. The in-game result would be objects moving faster or slower than they should, since the delta time value is becoming more and more incorrect.

27.7 hours seems like a really large number - who’s going to play a game for that long? Is this a practical concern? On one hand, you could argue that it isn’t a big deal. On the other hand, it’s possible for people to leave their games idling for a pretty long time. With modern suspend/resume functionality, it’s also possible for for a game to stay “on” for days and days and days.

As floating point numbers become larger, they start to lose decimal precision. The difference between our “current” and “last” values will usually only be a couple milliseconds, so we start get some serious approximation issues.

Somewhere between 200,000,000 milliseconds (~55.5 hours) and 300,000,000 milliseconds (~83 hours) we start to see some major discrepancies, to the point where some frames report as 0ms (too small!) and some report as 32ms (double!).

We can easily avoid this problem if we keep our ticks values as unsigned integers until after the difference has been calculated. Unsigned integers don’t have such approximation concerns.

How Long Will This Work?

Given max values for unsigned 32-bit integers, we can continue to accumulate these values for ~49 days before they will “roll over” or “wrap” back to zero.

The result will be one frame where the delta time value is a very large, negative number! Our lastTicks will be something like 4,294,967,295. Our currentTicks will be something like 1000. The result is then 1000 - 4,294,967,295 = -4,294,966,295.

For this reason, it may be a good idea to clamp our delta time value to a minimum value of zero. If we do this, our delta time logic can run indefinitely, though there will be one frame after ~49 days that errantly reports a 0s delta time.

Another possibility would be to use an unsigned 64-bit integer to store these values. This value would almost certainly never roll over. The max value is 18,446,744,073,709,551,615. This value would roll over after an incredible 584,942,417 years. Nobody’s playing your game that long! However, the ability to use such a value is dictated by the value type your OS (or SDL) returns.

Why Seconds?

You may wonder why we’d bother to convert delta time to seconds anyway, if floating-point values have approximation issues.

In one sense, it is a convention that is used by many other engines. So, we do it too. But that alone isn’t a great justification.

One reason is that we’ll often be dealing with other values that are expressed in seconds. Velocities use units/s, accelerations use units/s/s. Animations are also often expressed in “frames per second”. Therefore, it is a convenient unit to be working in.

Our seconds value must be floating-point, since most frames will take only a fraction of a second to complete (16ms or 30ms if our performance is good). When doing calculations later, we may often need to deal with fractional amounts of movement, so having our delta time value already in a floating-point value is helpful for that, too.

Frame Limiting

One problem with the above loop is that it can potentially run very, very quickly - at hundreds or thousands of FPS on some systems.

We want our game to run as quickly as possible, but at some point, it becomes a bit excessive! Especially when we start rendering, we’ll be updating WAY faster than any human eye can perceive. It’s kind of a waste, and it can have negative impacts on power usage.

To combat this, one thing we can do is to limit how quickly our loop runs. One way we could do this might be to “sleep” our current thread for a certain amount of time. But another way to do this is with a simple “while” loop (sometimes called a “busy wait”).

Here’s our updated code, but now limited to running at 60FPS max (or 16ms per frame):

void GEngine::Update()
{
    // Tracks next "GetTicks" value that's acceptable to perform an update.
    static int nextTicks = 0;

    // Tracks the last ticks value each time we run this loop.
    static uint32_t lastTicks = 0;

    // Limit to ~60FPS. "nextTicks" is always +16 at start of frame.
    // If we get here again and 16ms have not passed, we wait.
    while(SDL_GetTicks() < nextTicks) { }

    // Get current ticks value. Save next ticks for +16ms.
    uint32_t currentTicks = SDL_GetTicks();
    nextTicks = currentTicks + 16;

    // Calculate change from current to last, and convert to seconds.
    uint32_t deltaTicks = currentTicks - lastTicks;
    float deltaTime = deltaTicks * 0.001f;

    // Save ticks value for next frame.
    lastTicks = currentTicks;
}

This code implements a “busy wait” (empty while loop) until at least 16ms have passed, ensuring that the maximum FPS will be 60.

Delta Time Capping

On the flip side, we may want to ensure that our delta time is never too large.

One instance where delta time can grow quite large is during debugging, when paused at breakpoints. If you pause on a breakpoint for two minutes, and then hit “continue,” you will end up with a delta time value that is about two minutes long! This’ll usually translate into some strange in-game results.

When not debugging, you could argue that there’s not a good reason to cap the delta time value. In that case, you may want to exclude the cap logic from release builds.

Implementing this is pretty simple:

void GEngine::Update()
{
    // Tracks next "GetTicks" value that's acceptable to perform an update.
    static int nextTicks = 0;

    // Tracks the last ticks value each time we run this loop.
    static uint32_t lastTicks = 0;

    // Limit to ~60FPS. "nextTicks" is always +16 at start of frame.
    // If we get here again and 16ms have not passed, we wait.
    while(SDL_GetTicks() < nextTicks) { }

    // Get current ticks value. Save next ticks for +16ms.
    uint32_t currentTicks = SDL_GetTicks();
    nextTicks = currentTicks + 16;

    // Calculate change from current to last, and convert to seconds.
    uint32_t deltaTicks = currentTicks - lastTicks;
    float deltaTime = deltaTicks * 0.001f;

    // Save ticks value for next frame.
    lastTicks = currentTicks;

    // Ensure delta time is never negative.
    if(deltaTime < 0.0f) { deltaTime = 0.0f; }

    // Limit the time delta to 0.05 seconds (about 20FPS).
    if(deltaTime > 0.05f) { deltaTime = 0.05f; }
}

This is a simple “if statement” to ensure the delta time is never more than 0.05 seconds. Again, it’s totally arguable that this should not be present in release builds, where an FPS below 10 or 5 can (unfortunately) be very real.

I also snuck in a “negative delta time” check. Again, this would only be possible (we hope) if the ticks value “rolls over” after ~49 days. These two statements could also be implemented with a Clamp function, if desired.

Conclusion

In this post, we started to architect our game engine by creating our first class - exciting!

We also implemented our delta time calculations. Calculating the delta time value accurately is critical for the game to run correctly. It’s also somewhat fraught with peril!

Our next step will be to flesh out our math library, which will provide critical classes to finally get something, anything, rendering to screen!

comments powered by Disqus