Game loops, and why they’re important

Hello again! As promised, this post will discuss the game loop implementation I’ve decided on for Halogen. Most of the code in this article (and probably most of the future articles too) is pseudocode, and exists more as an example than usable code.

So, to begin, we should define what a “game loop” is – or, at least how I interpret the term. The game loop describes the most central loop of an engine or game’s code. For example:

void RunLoop()
{
    while (gameShouldRun)
    {
        DoLogic();
        Draw();
    }
}

Obviously, this is a very basic example of a game loop, but the idea is there – as long as the game needs to run, this loop repeats, running the game logic and drawing the scene.

Now, there are a few different ways to implement a game loop. I don’t intend to go into detail on this subject, because it’s been covered far more effectively than I could cover it in several places (like here, arguably the most popular article on game loops and time step management), but I will briefly touch on some of the ways to do it, as well as their drawbacks.

Variable time steps

As we all know, it’s virtually impossible to guarantee a steady framerate. However, if you don’t accommodate for the changes in framerate, your game will behave drastically different between someone running at 30 frames per second, and someone running at 300 frames per second. One of the easiest ways to work around this is with a variable time step:

void RunLoop()
{
    while (gameShouldRun)
    {
        float timeSinceLastFrame = Time::GetDeltaTime();

        DoLogic(timeSinceLastFrame);

        Draw();
    }
}

Using the time since the last frame, we can make sure we step objects at the right speed. However, the major drawbacks to this approach are two-fold. First, this approach forces your logic and rendering to run at the same rate, which is definitely not necessary. Second, for large steps, your simulation could become unstable, especially physics. Neither of these are particularly desirable traits, so we’ll pass on this one.

Fixed time steps

So if variable time steps cause a bunch of problems, what if the steps didn’t vary?

void RunLoop()
{
    while (gameShouldRun)
    {
        float startTime = Time::GetTimeNow();
        DoLogic();
        Draw();

        float frameTime = Time::GetTimeNow() - startTime;
        sleep(FIXED_FRAME_TIME - frameTime); // sleep for the remaining time
    }
}

While this might solve some issues, it should be abundantly clear that this approach still suffers from issues. For one, it still forces your rendering and logic to happen at the same rate. The other problem arises if your frames take longer than the allotted time – suddenly your simulation is moving in slo-mo. Another major downside to this approach is wasted potential – why should you force 60 frames per second on someone whose system could push 200?

Combining the two

The big drawback the two previous approaches can’t overcome is the coupling of logic speed and render speed, so we want our loop to avoid this issue particularly. Specifically, we would like our logic to run at a fixed rate, and allow the rendering to run as quickly as it can. The general solution looks something like this:

void RunLoop()
{
    float lag = 0.0F;

    while (gameShouldRun)
    {
        float timeSinceLastFrame = Time::GetDeltaTime();

        lag += timeSinceLastFrame;

        while (lag > FIXED_FRAME_TIME)
        {
            DoFixedUpdate();
            lag -= FIXED_FRAME_TIME;
        }

        Draw(lag); // Use the lag to interpolate between fixed frames
    }
}

This code will only run the fixed-rate update loop as often as specified, while letting the renderer churn out frames as quickly as it can. We’ve successfully decoupled logic updates from render speed, but we’ve also created a new complication: interpolation. If the renderer decides to draw a frame between two fixed updates, the player could end up seeing the same frame twice – at which point, 120 frames per second looks just the same as 60. Interpolation helps this – the renderer would perform interpolation before drawing, in order to smooth out transitions between frames – but we’re now throwing potentially messy interpolation code into our renderer, which doesn’t sound particularly ideal.

This approach can also suffer from a phenomenon called the spiral of death, in which the time it takes to do a full frame is longer than your allotted time. The lag value continually grows, which forces more updates, which grows the lag variable… you get the point.

The Halogen loop

Now that we’ve established some of the ways you can write a game loop, I’d like to talk about how I decided to implement the core loop in Halogen, as well as some of my reasoning for doing so.

The core loop of Halogen is based on the last example given above, but draws much inspiration from Unity’s Monobehavior lifecycle as well (an excellent graphic on this can be found here). Essentially, we’re using both a fixed update and variable-rate update, the latter of which internally handles interpolation before drawing the frame. Here’s what this looks like, in pseudocode:

void RunLoop()
{
    float lag = 0.0F;

    while (gameShouldRun)
    {
        float timeSinceLastFrame = Time::GetDeltaTime();
        lag += timeSinceLastFrame;

        // Handle windows messages every drawn frame
        // This keeps input up to date for each variable update
        HandleWindowsMessages();

        while (lag > FIXED_FRAME_TIME)
        {
            DoFixedUpdate();
            lag -= FIXED_FRAME_TIME;
        }

        // Handles anything that doesn't rely on fixed framerate,
        // for example particle effects, camera rotation, etc.
        // Also handles interpolation
        DoVariableUpdate(timeSinceLastFrame);

        // The renderer uses interpolated values from the variable update
        // no more messy interpolation inside the renderer!
        Draw();
    }
}

The end result is exposing a fixed update function to the user, which can be used when a reliable time step is required (like physics code), but also exposing a variable update function that is called every time the scene is rendered. Users can even specify if they want their objects to use the built-in interpolation system or not, and use this variable update function to create their own interpolation or predictions. Another great thing about this is thread safety – since both of the update functions are called from the same thread, albeit at different rates, there’s no inherent risk like there might be if the fixed update function were in its own thread.

So, that’s it for my first entry to the blog. I’d love to hear feedback on the concept, as I feel very confident that this is a strong approach to a critical part of the engine. Coming next on the blog, I intend to talk about some of the memory management techniques I’ve chosen for Halogen.

Thanks for reading!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s