4
\$\begingroup\$

I've just "finished" my second attempt a core game loop, which is a heavily refactored version of my first attempt.

My perceived benefits are:

  • Using functors means that the actual loops themselves are independent from the actual systems them manage. You can wrap states up elsewhere, and the only assumption they need to make is they accept passed time in seconds.

  • Both of these functions are thread-safe assuming the functors are too.

  • These functions are mostly functionally pure (again assuming the functors are too).

  • Update rates, and framerate can be shifted on-the-fly (allow this is really only important for rendering.

  • Finally, the render loop, and the update loop are decouple in the sense neither rate is tied to the other.

Am I correct in making these assumptions?

It is a worthwhile idea making sure that the value of cAccumulated that's passed to Update is greater than zero to prevent the world slipping backwards, or is that taken care of by only stepping in increments of dStepSize. It seems petty but defensive.

//
//  Update the world in a series of steps and return left-over time.
template<typename UPDATEFUNCTOR>
CONST FLOAT Update(CONST FLOAT cAccumulated, CONST UINT cUpdateFrequency,
    UPDATEFUNCTOR &fnUpdate)
{
    extern CONST LARGE_INTEGER &cFrequencyRef;
    extern LARGE_INTEGER cInitialLogicTicks;

    CONST UINT cMaxLaggedUpdates = 5;
    CONST FLOAT dStepSize
        = 1.0E0F / static_cast<CONST FLOAT>(cUpdateFrequency);

    LARGE_INTEGER cCurrentTicks;
    FLOAT dElapsed, dRemaining;

    QueryPerformanceCounter(&cCurrentTicks);
    dElapsed = (cCurrentTicks - cInitialLogicTicks) / cFrequencyRef;

    //  Set the delta back to zero.
    CopyMemory(&cInitialLogicTicks, &cCurrentTicks, sizeof (LARGE_INTEGER));

    //  Prevent use of the debugger causing time-step issues.
    dElapsed = Clamp(dElapsed, 0.0E0F, cMaxLaggedUpdates * dStepSize);

    //  Pass available time off to the world.
    for (dRemaining = dElapsed + cAccumulated; dStepSize <= dRemaining;
        dRemaining -= dStepSize)
    {
        fnUpdate(dStepSize);
    }

    return dRemaining;
}

//
//  Attempt to render a frame and return a value indicating if it succeeded.
template<typename RENDERFUNCTOR>
CONST BOOL Render(CONST UINT cFrameFrequency, RENDERFUNCTOR &fnRender)
{
    extern CONST LARGE_INTEGER &cFrequencyRef, &cInitialLogicTicksRef;
    extern LARGE_INTEGER cInitialRenderingTicks;

    CONST FLOAT dFramePeriod
        = 1.0E0F / static_cast<CONST FLOAT>(cFrameFrequency);

    LARGE_INTEGER cCurrentTicks;
    FLOAT dElapsed, dInterpolation, dImmediatePeriod;
    UINT cImmediateFrequency;

    QueryPerformanceCounter(&cCurrentTicks);
    dElapsed = (cCurrentTicks - cInitialRenderingTicks) / cFrequencyRef;

    if (dFramePeriod <= dElapsed)
    {
        /* `dInterpolation` should be in the interval [0, dStepSize),
         * and is used to provide smooth motion.
         */
        dInterpolation = (cCurrentTicks - cInitialLogicTicksRef)
            / cFrequencyRef;

        /* !_TODO__ : There needs to be an implementation for smoothed values. */

        dImmediatePeriod = fnRender(dInterpolation) + dElapsed;
        cImmediateFrequency
            = static_cast<CONST UINT>(1.0E0F / dImmediatePeriod);

        CopyMemory(&cInitialRenderingTicks, &cCurrentTicks,
            sizeof (LARGE_INTEGER));

        return TRUE;
    }

    return FALSE;
}

LARGE_INTEGER cFrequency, cInitialRenderingTicks, cInitialLogicTicks;
CONST LARGE_INTEGER &cFrequencyRef = cFrequency,
    &cInitialRenderingTicksRef = cInitialRenderingTicks,
    &cInitialLogicTicksRef = cInitialLogicTicks;

This is then called as so:

int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    MSG msg;
    BOOL fIterateLoop;
    FLOAT cAccumulated;

    ZeroMemory(&cFrequency, sizeof (LARGE_INTEGER));
    QueryPerformanceFrequency(&cFrequency);

    QueryPerformanceCounter(&cInitialLogicTicks);
    CopyMemory(&cInitialRenderingTicks, &cInitialLogicTicks, sizeof (LARGE_INTEGER));

    cAccumulated = 0.0E0F;

    ZeroMemory(&msg, sizeof(MSG));

    fIterateLoop = TRUE;
    while (fIterateLoop)
    {
        for (/* */; PeekMessage(&msg, 0, 0, 0, PM_REMOVE);
            TranslateMessage(&msg), DispatchMessage(&msg))
        {
        }

        fIterateLoop = WM_QUIT == msg.message ? FALSE : TRUE;

        cAccumulated = Update(cAccumulated, /* Desired update frequency. */, /* Update functor */);
        Render(/* Desired FPS cap. */, /* Render functor */);
    }

    return static_cast<int>(msg.wParam);
}

The normal Win32 initialisation code has all been snipped out to focus on the core loop (which is what I'm asking about).

\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

Lets go through your stated benefits:

  1. game loop independant from actual updating / rendering. It's nice, but you add here that user supplied function should take time in seconds. You could just take std::function)> to say exacly that in the code, and as a bonus no more need to keep everything in header.

  2. Thread-safety. Functions update global variables with no locks, so they are not thread-safe. QueryPerfomanceCounter was not thread safe last time I looked at it (several years ago).

  3. Functional purity. Mostly pure means not pure.

  4. Update rates may shift on the fly. That can be good for render, but I doubt it is good for updating, espesially if you use some simulation (eg. phisics), different update times could lead to inaccurate and worse, not stable, results. Also update function is called for each delta period that fits in time from last update, but this easily will lead to hang if hardware is not capabale to do all the updates. You use policy to do only some maxLaggingUpdates, here is two issues. If updating stalls, you better not do any updates at all, it takes time and end user will be surprised to see some updated after lag. Secondly, how user of your loops is supposed to change policy or at least the constant?

  5. Frame rates of update and render decoupled. It is really good thing to have, but why not to go further with decoupling? Why render need time period passed to it? This implies it do some updating inside, but it is work of updater, isn't it?

Some random notes:

  • Consider to create Looper class, which have update and render functions, and functions to do some tweaks, such as providing lag policy. You can also move initialisation stuff from your main() to the class constructor.

  • Why do CopyMemory(ticks1, ticks2)? ticks1 = ticks2 tells more about intention, and overall is just clearer.

  • Seriosly you should consider to change your code style. These all-caps and prefixes clutter the code. Look at numerous available style guides, their reasoning and take what fits your needs.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.