Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411283 Posts in 69325 Topics- by 58380 Members - Latest Member: bob1029

March 29, 2024, 03:14:41 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsDeveloperTechnical (Moderator: ThemsAllTook)Academic Papers on Game Loops ?
Pages: [1] 2
Print
Author Topic: Academic Papers on Game Loops ?  (Read 2423 times)
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« on: September 05, 2018, 02:09:40 AM »

Actually doesn't have to be academic. I am interested in fixed timestep loops in particular. Here is the standard implementation:
https://gafferongames.com/post/fix_your_timestep/

If you know any sorts of fixed timestep loops which differ from the standard solution, feel free to share them here. If you know how Unity or other public engines implement fixed timestepping, would be interested to see it. If you know if the "GPU Gems"-series is addressing this topic somewhere, would be good to know too.

I am asking because I am about to write a paper on stable fixed timestep loops and need some references on related work (can be engine code as well). Without elaborating on what "stable" means, every solution I have seen so far is prone to frame pacing problems, it looks like this problem hasn't been attacked in a serious way yet (only bandaid solutions at best), not in a public spot at least. This has to be a bigger problem on pc than on consoles because sampled time values are likely noisier on pc (caused by the randomness of scheduler's timing), causing the loop to fall into "delay-then-catch-up" cycles (stuttering). This is possibly one reason why user complaints like this about games with locked framerate on pc are common:
http://www.tomshardware.co.uk/answers/id-2864044/console-runs-smoother-30fps.html

Digital Foundry is also stating that there are common problems with frame pacing on pc:

"Locking frame-rates at 30fps on the PC is a sketchy business that simply doesn't always work as one might expect, mostly because very few games actually have frame-rate limiters built-in. Of those that do, many do not pace the delivery of frames correctly, introducing judder and eliminating the point of having them in the first place. Once again, Need for Speed Rivals - this time in its PC incarnation - is a good example of this."

source: https://www.eurogamer.net/articles/digitalfoundry-2014-frame-rate-vs-frame-pacing

Now, the interesting question is, assuming performance issues are absent: Are stutters on pc often caused by the instability of the standard loop implementation (in case you are familiar with that issue)? Or are they predominantly caused by the unpredictability of gpu tasks, multithreading desyncs, stuff like that? Feel free to share your thoughts on that, too.

« Last Edit: September 14, 2018, 06:45:10 AM by J-Snake » Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
qMopey
Level 6
*


View Profile WWW
« Reply #1 on: September 12, 2018, 02:29:54 PM »

Personally I've always hated that article. It's weirdly explained and a bit over-engineered. I think it'd be fun to do a little write-up here; you might not be looking for this since you asked for links, but I'll write for my own amusement (feel free to ignore).

In general you accumulate a delta-time based on the *previous* frame's duration. Something like this:

Code: (variable timestep)
while (1)
{
    // dt is the delta-time (change in time) over the previous `do_game`
    float dt = get_timestep();

    do_game(dt);
}

This is the typical "variable time-step" strategy. The significant takeaway is that each game simulation tick, represented by do_call, will use the time from the previous frame as an estimate of what to do now. Each time do_game is called a little bit different of a value is probably given for dt, hence the "variable" name, since dt varies each game loop.

The next strategy is to make sure the game will not run faster than some frequency, say, 60fps.

Code: (non-variable time-step)
const float fps = 1.0f / 60.0f;
float accumulator = 0;

while (1)
{
    float dt = get_timestep();

    // limit the fps
    accumulator += dt;
    if (accumulator < fps)
        continue;
    accumulator = 0;

    do_game(dt);
}

The game will run in a very tiny infinite loop waiting until 60fps has elapsed. This solution can break depending on how get_timestep is implemented. If the loop runs so fast that it returns a super tiny number (maybe even zero), the small floats may not add up accurately. Typically people try to fix this sort of thing by using more precision, i.e. changing the accumulator to be a double instead of a float -- this is just a bandaid solution without addressing the real problem.

The real problem is this loop does nearly nothing except calculating a delta time. This can cause another problem besides just breaking dt as the game now does not render at any faster than 60fps, and does not look for user-input at any time other than on 1/60s time intervals.

The loop can be adjusted slightly so user-input and rendering can occur outside of the do_game function:

Code: (non-variable timestep, refactored for more frequent input/rendering)
const float fps = 1.0f / 60.0f;
float accumulator = 0;

while (1)
{
    float dt = get_timestep();

    // These two functions have been refactored out of the `do_game` function
    // and placed here.
    poll_input();
    render_game();

    // limit the fps
    accumulator += dt;
    if (accumulator < fps)
        continue;
    accumulator = 0;

    do_game(dt);
}

It can be very nice to gather up all the user-input at a very high frequency, that way players can play very quickly and perform very complicated for fast maneuvers more freely. For example, fast and responsive input can be very important in many action-style games.

Now a question rises: "What is the point of rendering beyond 60fps"? Most of the time games are interpolating graphical effects over time. These interpolations can still potentially animate many times and be fully rendered and displayed on the screen multiple times between each call to do_game. For those unfamiliar with interpolation, tweening, or animation, here is a nice video about the topic.





However, in order to perform these kinds of interpolations some kind of time-based float need to be passed into the render function.

Code: (non-variable time-step with interpolated rendering)
const float fps = 1.0f / 60.0f;
float accumulator = 0;

while (1)
{
    float dt = get_timestep();

    accumulator += dt;
    int iterations = 0;
    while (accumulator < fps)
    {
        ++iterations;
        accumulator -= fps;
    }

    float interpolant = accumulator;

    poll_input();
    do_game(dt);
    render_game(dt);
}

Pay special attention to the interpolant. It is the leftover value, a value between 0 and fps. The code above is adding dt to accumulator, so accumulator should be non-zero.

The interpolant is what is used to render between frames. This should make some sense, as it is *usually* assigned a value between 0 and fps. In a typical game loop with no lag or really bad performance, the game will run quite quickly and go to the next game loop in much less time than fps. In these common cases iterations will be 0, and interpolant will be used.

In cases where the is running slowly, iterations will be non-zero. This is to account for times when the previous frame was, for example, 3 * fps + "a little more but less than a frame". In this instance iterations would be set to 3, and interpolant would be a small value between 0 and 1 frames. For now iterations and interpolant are unused, but will be revisited later.

There is a problem with the current strategy. If the game loop takes longer than fps, it is probably because the game is running slowly due to some un-optimized code (or because perhaps there was a performance spike, like when the operating system does something weird... looking at you Windows Updates). The next game loop will run, but only once. This essentially makes the game "pause" and the entire game will feel the hiccup.

One strategy is to use the iterations variable in a loop like so:

Code: (non-variable time-step, naive)
const float fps = 1.0f / 60.0f;
float accumulator = 0;

while (1)
{
    float dt = get_timestep();

    accumulator += dt;
    int iterations = 0;
    while (accumulator < fps)
    {
        ++iterations;
        accumulator -= fps;
    }

    while (iterations > 1)
    {
        poll_input();
        do_game(fps);
        --iterations;
    }

    float interpolant = accumulator;

    poll_input();
    do_game(dt, interpolant);
    render_game(dt, interpolant);
}

Now the game loop can account for the occasional hiccup and simulate the game forward in the event iterations becomes non-zero. This can be quite nice to ensure the game logic itself keeps running forward through time. There lots of ways to tinker with this loop. For example, the iterations loop can have the input polling removed, as maybe it just isn't necessary for some games. Another strategy is to remove the iterations loop entirely, and do something like:

Code:
do_game(fps * iterations);

For some games this may be acceptable. The idea is to run the game an extra time, but with a very large time value. This can "jump" the game into the future to catch-up. For many cases this is not a good strategy, as sometimes big jumps can cause physics rigid bodies to tunnel, and many other kinds of nasty problems.

Another problem with the iteration loop, is if the previous game loop was really slow, the next one will probably be pretty slow too, as in the case of poorly optimized code or a slow machine. In this instance iterations will become larger and larger each game loop, forever trying to catch up the real-time. Very quickly this will devolve into a near-infinite loop, as iterations becomes ginormous.

A quick and easy fix is to clamp iterations below a small number, say, 5.

Code: (non-variable timestep, no death-spiral)
const float fps = 1.0f / 60.0f;
float accumulator = 0;

while (1)
{
    float dt = get_timestep();

    accumulator += dt;
    int iterations = 0;
    while (accumulator < fps)
    {
        ++iterations;
        accumulator -= fps;
    }

    // Prevent a death-spiral
    if (iterations > 5)
        iterations = 5;

    float interpolant = accumulator;

    while (iterations > 1)
    {
        poll_input();
        do_game(fps);
        --iterations;
    }

    poll_input();
    do_game(dt, interpolant);
    render_game(dt, interpolant);

    do_game(dt);
}

Note that another way to phrase "non-variable timestep" is "fixed time-step", fixed as in constant.

Physics simulations typically need to run at a constant timestep. This is to make sure physics will be deterministic given the same machine, and same inputs to the game. Physics determinism is important for many games, and without it things like instant replays, save states, and other features can be impossible. Variable timesteps, when fed to physics, is a source of non-determinism, since each variation in time will cause *different* variations in floating point error for every single floating point operation that happens within the physics. The only practical solutions are to A) use only integers in physics, B) use floating-point determinism, C) use lots of little hacks and interpolations to "cover up" the effects (this strategy is only valid for multi-player games where the physics on the server can be deterministic, and I won't talk about it any more here since it is quite complicated).

In this article we cover the most practical solution for 95% of games: B.

Simply *only* run physics updates on a fixed (constant) frequency:

Code: (fixed timestep, with physics)
const float fps = 1.0f / 60.0f;
float accumulator = 0;

while (1)
{
    float dt = get_timestep();

    accumulator += dt;
    int iterations = 0;
    while (accumulator < fps)
    {
        ++iterations;
        accumulator -= fps;
    }

    // Prevent a death-spiral
    if (iterations > 5)
        iterations = 5;

    float interpolant = accumulator;
    float game_time = fps * iterations + interpolant;

    while (iterations > 1)
    {
        do_physics();
        --iterations;
    }

    poll_input();
    render_game(dt, interpolant);
    do_game(game_time);
}

Again, some games can get away with removing poll_input and do_game from inside the iterations loop, and instead only simulate physics here. There are many ways to tweak this loop, and this loop is just a typical example. In this example the game can get away with being called just once per main-loop, and physics is by its lonesome self in the iteration loop. This is the most common strategy for all games, and I would consider it "default".

To summarize so far, we have decoupled the update frequencies of input, physics, rendering, and the game by refactoring each topic into a different function. These different functions can be called at different frequencies by placing them inside/outside/before/after certain loops. Most of the time we want graphics to draw as fast as possible, physics to run at a fixed frequency, and the game to run just once per loop.

There is one last gotcha.

Since the rendering is happening at a different frequency than physics, and probably faster, there will be a very disruptive "jumpy glitchiness" to all physically simulated things as they render. This happens because as physics runs in discrete time-chunks, the nice and smooth interpolated rendering does not. The result is that each time physics runs it goes ahead by fps in time, essentially jumping. It works by running at fixed (constant) simulation cycles.

As the game renders it will render these positions happily. These positions are going to jump at *different times* than when the rendering geometry is displayed on screen.

Here are some solutions: A) only render the game when physics runs (move all functions, poll_input, do_game, render_game, inside the iteration loop); B) interpolate between two physics states.

Most modern games are going to be doing option B. The idea is to store the current and *previous* positions for all physics objects. Then when time comes to render, interpolate between the current and previous positions using the interpolant value. This does take up at least 2x memory to store a bunch of extra transforms, but hey, memory is sort of cheap nowadays and people really like smoothly interpolated physics stuff Smiley

At the beginning of this post I stressed the importance of using a previous value for the current loop. In this case, it is best to interpolate between a previous value of physics, and the current one. This will always introduce a one-frame-lag for physics objects. Nobody will notice. It will look smooth. It is impossible to interpolate from the current physics positions to the next one, since the next one has not yet been calculated! The next physics positions can be estimated, but this will cause some drastic visual artifacts when physics objects take sharp turns or change directions abruptly; don't do it. You have been warned.

Note: I didn't test any of this code, and there are probably bugs in there.
« Last Edit: September 12, 2018, 02:59:52 PM by qMopey » Logged
qMopey
Level 6
*


View Profile WWW
« Reply #2 on: September 12, 2018, 02:52:39 PM »

AFAIK all the top game engines use something very close to the last code snippet I posted, including Unity/Unreal, etc.

Oddly enough Lumberyard is doing something more like "non-variable time-step with interpolated rendering". It's actually pretty bad strategy for such a large engine, but perfectly acceptable for specific games.
Logged
Crimsontide
Level 5
*****


View Profile
« Reply #3 on: September 12, 2018, 03:50:22 PM »

Why not just use a 64-bit integer instead of floats?  QueryPerformanceTimer(), std::chrono::high_resolution_clock, or something similar will get you sufficient precision.  Integers don't have precision problems.  For a 64bit integer, even at 1 ns precision, it would have to run for 213,504 years straight before you encountered issues.

As far as rendering interpolation...  If your physics engine can't keep up (assuming a fixed step size for physics) your game will crash (or slow down), async rendering won't save you.  So unless your game physics is costly, its usually quite easy for it to far exceed any reasonable frame rate (100 - 200 fps is not uncommon).  No interpolation needed, just render the next available physics step when ready to render.

Here's basically what I would use:

Code:
uint64_t ticks_per_physics_step = 100;    // nanoseconds, microseconds, query_performance_counter_ticks, whatever your system is using
uint64_t next_physics_step = get_time();

while (1) {
    uint64_t time = get_time();

    // process player input
    // IMHO this is more important than processing physics or rendering
    MSG msg;
    while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
        }

    // do physics/simulation
    if (time >= next_physics_step) {
        do_game_simulation_and_or_physics(next_physics_step);
        next_physics_step += ticks_per_physics_step;
        continue;
        }

    // rendering is lower priority
    if (ready_to_render()) {
        render_game();
        continue;
        }

    // nothing to do... Sleep() ?? SpinLoop??  Dance??
    Sleep(0);
    }
Logged
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #4 on: September 12, 2018, 05:25:22 PM »

Just a short note on timer precision:

There is a common misconception (there are many actually when it comes to loops, even amongst industry veterans) about the noise in sampled time values: The timer itself can be very precise, but the os-scheduler is not (on windows up to 1ms of noise). After a buffer swap you can never be sure when the process (and thus the loop) resumes its execution, so the time samples remain noised.
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
qMopey
Level 6
*


View Profile WWW
« Reply #5 on: September 12, 2018, 06:08:02 PM »

I read your post a few times and still don't know what the problem is. What is the problem?
Logged
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #6 on: September 12, 2018, 06:53:19 PM »

It is just a general note. You can even have a perfect timer, but the sampling of frametimes remains noised (up to 1ms noise on windows). Noised frametimes is a cause of inconsistent frame pacing when using the standard fixed timestep solution (or any close variants of it). Unfortunately, I don't have time to elaborate on it here, I want to do it in a paper. It will save redundant work.

Just to provide an idea on the quick:
Imagine the ideal update-sequence (for the configuration: updatesPerSecond = 60, framesPerSecond = 60) is 1-1-1-1-1...
Now in worst case you could get this instead: 0-2-0-2-0-2...
This is the "delay-then-catch-up"-problem, just to give it a poignant name on the quick.
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
Crimsontide
Level 5
*****


View Profile
« Reply #7 on: September 12, 2018, 07:18:33 PM »

While I do agree that due to the OS scheduler and other things (like variable speed CPUs) that time slices can be variable.  What I don't see is how the float/accumulator solution fixes anything.  In your game loop you assume that game simulation occurs at a faster rate than rendering:

Code:
while (iterations > 1)
    {
        do_physics();
        --iterations;
    }

In your case you hardcoded it to a maximum of 5 iterations/frame.  As long as your simulation is faster than your render the interpolation between simulation states seems needlessly complicated.

The only time rendering interpolation would be necessary would be in the case where the simulation runs slower than rendering.  I'm pretty sure I read somewhere cities skylines used that sort of technique to simulate up to 1 million cims in sort-of-realtime.
Logged
qMopey
Level 6
*


View Profile WWW
« Reply #8 on: September 12, 2018, 07:38:57 PM »

It is just a general note. You can even have a perfect timer, but the sampling of frametimes remains noised (up to 1ms noise on windows). Noised frametimes is a cause of inconsistent frame pacing when using the standard fixed timestep solution (or any close variants of it). Unfortunately, I don't have time to elaborate on it here, I want to do it in a paper. It will save redundant work.

Just to provide an idea on the quick:
Imagine the ideal update-sequence (for the configuration: updatesPerSecond = 60, framesPerSecond = 60) is 1-1-1-1-1...
Now in worst case you could get this instead: 0-2-0-2-0-2...
This is the "delay-then-catch-up"-problem, just to give it a poignant name on the quick.

I don't see how there's going be this kind of noise if the game is not sleeping. If it's just running a hot infinite loop, it should be able to record many small time slices that sum up to one frame, and issue a game tick at that point.

I don't see is how the float/accumulator solution fixes anything... As long as your simulation is faster than your render the interpolation between simulation states seems needlessly complicated.

It's not needlessly complicated, even if the renderer is running faster than other things (the speed of the renderer relative to other things irrelevant). The interpolant value needs to be expressed in order to run physics at a different frequency compared to the rendering frequency. Without this, physics and rendering *must* run at the same frequency.

The only time rendering interpolation would be necessary would be in the case where the simulation runs slower than rendering.

Interpolation is needed to render something moving at a discrete time frequency that differs from the rendering frequency, otherwise there will be a visual and jarring jumping motion.
Logged
Crimsontide
Level 5
*****


View Profile
« Reply #9 on: September 12, 2018, 08:04:13 PM »

If the render runs at a multiple of the simulation, they can be out of step.  For example, if the simulation was pegged at 200 fps, and the render 50 fps, there would be no jitter (that's the technical term Smiley https://en.wikipedia.org/wiki/Jitter).

If the simulation runs significantly faster than the render but not at a multiple, there will be jitter, but it will often be far too little to notice.  For example, if the simulation was at 200fps or more, there's no way anyone would be able to notice.

If you have a sufficiently complex simulation then interpolation might be warranted; but in the vast majority of cases simulation is pretty trivial, and its just easier to jack up the simulation frame rate.  Writing a proper interpolating render is not necessarily an easy task.
Logged
goob256
Level 2
**



View Profile WWW
« Reply #10 on: September 12, 2018, 09:21:44 PM »

In practice I haven't seen the slight error in OS timers cause a noticeable jitter. Some of the problems mentioned here come up often now though, with high frequency monitors (144 Hz etc), so if you're using a fixed time step you pretty much have no choice but to implement different render/logic rates if you want to run at full frequency on different screens.
Logged
goob256
Level 2
**



View Profile WWW
« Reply #11 on: September 12, 2018, 09:23:08 PM »

In practice I haven't seen the slight error in OS timers cause a noticeable jitter. Some of the problems mentioned here come up often now though, with high frequency monitors (144 Hz etc), so if you're using a fixed time step you pretty much have no choice but to implement different render/logic rates if you want to run at full frequency on different screens.

Actually not entirely true: on old non-multi tasking systems you can get smoother scrolling than the latest PC, but that seems to be unavoidable.
Logged
Schrompf
Level 9
****

C++ professional, game dev sparetime


View Profile WWW
« Reply #12 on: September 12, 2018, 10:04:50 PM »

Nice writeup, qMopey. I wanted to add that a variable update rate might be just as nice. Used it so far in all my games, it feels smooth but of course eats all of your computing power. I personally don't care about this, but I've seen people complain.

The only issue with a variable timestep is that at some point parts of your game mechanics break due to lack of precision. If you can estimate what your limit is, you can safely use variable time steps and just stay below that limit, and your game is super smooth in any situation. A common method to stay below the limit is simply VSync.

Anecdote: I once used a monitor with 75Hz refresh rate. Some games worked flawlessly, others jumped and paused like heck. If you're going for a fixed update rate, at least choose a rate that divides nicely with 60, 75, 90, 120, 144Hz. Or whatever other refresh rates might be out there.
Logged

Snake World, multiplayer worm eats stuff and grows DevLog
qMopey
Level 6
*


View Profile WWW
« Reply #13 on: September 13, 2018, 12:38:14 AM »

If the render runs at a multiple of the simulation, they can be out of step.  For example, if the simulation was pegged at 200 fps, and the render 50 fps, there would be no jitter (that's the technical term Smiley https://en.wikipedia.org/wiki/Jitter).

If the simulation runs significantly faster than the render but not at a multiple, there will be jitter, but it will often be far too little to notice.  For example, if the simulation was at 200fps or more, there's no way anyone would be able to notice.

Ah yes, multiples works.

But in my experience, it is not at all common for a naive setup of different frequencies to look OK without interpolation. So I don't agree with you here. Not that you are wrong, just that I don't agree and my experience is different.

Nice writeup, qMopey. I wanted to add that a variable update rate might be just as nice. Used it so far in all my games, it feels smooth but of course eats all of your computing power. I personally don't care about this, but I've seen people complain.

The only issue with a variable timestep is that at some point parts of your game mechanics break due to lack of precision. If you can estimate what your limit is, you can safely use variable time steps and just stay below that limit, and your game is super smooth in any situation. A common method to stay below the limit is simply VSync.

Thank for adding these points, they are both pretty good.

I think emphasis that the first option is a good one, is great. The first option *is* a great option. Personally I use this style in my own game.
Logged
goob256
Level 2
**



View Profile WWW
« Reply #14 on: September 13, 2018, 12:42:22 AM »

Just my preference but games that run full tilt over the refresh rate really bother me and if I find it's happening I usually stop playing. It's especially bad if you're running on battery power (laptop, phone, tablet...) From what I've seen a lot of people don't care though.
Logged
qMopey
Level 6
*


View Profile WWW
« Reply #15 on: September 13, 2018, 01:32:26 AM »

Oh yeah for battery powered devices it totally matters. I completely forgot about that.

One game I know of that did it quite well is Hearthstone. The default there seems to cap FPS quite low, presumably to save on battery power. There's an option called "high quality", and it just removes the cap. All the animations come out smooth and pristine.
Logged
J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #16 on: September 13, 2018, 03:49:38 AM »

It is just a general note. You can even have a perfect timer, but the sampling of frametimes remains noised (up to 1ms noise on windows). Noised frametimes is a cause of inconsistent frame pacing when using the standard fixed timestep solution (or any close variants of it). Unfortunately, I don't have time to elaborate on it here, I want to do it in a paper. It will save redundant work.

Just to provide an idea on the quick:
Imagine the ideal update-sequence (for the configuration: updatesPerSecond = 60, framesPerSecond = 60) is 1-1-1-1-1...
Now in worst case you could get this instead: 0-2-0-2-0-2...
This is the "delay-then-catch-up"-problem, just to give it a poignant name on the quick.

I don't see how there's going be this kind of noise if the game is not sleeping. If it's just running a hot infinite loop, it should be able to record many small time slices that sum up to one frame, and issue a game tick at that point.
We assume that vsync is enabled (no small time slices here).
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
ThemsAllTook
Administrator
Level 10
******



View Profile WWW
« Reply #17 on: September 13, 2018, 05:44:54 AM »

In practice I haven't seen the slight error in OS timers cause a noticeable jitter.

I have, on newer versions of OS X in particular. What I did to fix the 0-2-0-2-0-2 situation J-Snake described was to add a small tolerance value to my run loop, so that it would try to update the game exactly once per call as long as it was within 0.25 frames of where it thought it needed to be in either direction. When driving my run loop from a system timer set to 60 hz, in practice I was seeing something like 1-2-0-1-2-0, which the tolerance value smoothed out pretty much perfectly.
Logged

J-Snake
Level 10
*****


A fool with a tool is still a fool.


View Profile WWW
« Reply #18 on: September 13, 2018, 10:15:33 AM »

Did you test your solution for an extended time, like 1 hour?

From a theoretical perspective:

Your solution should work fine for a number of initial runs of a loop, if the accumulator is initially synchronized with the time sampling and the frequencies closely match (ups = fps = 60, for example). However, since the frequencies never match perfectly in reality, the accumulator will approach another over/underflow after a while. And the moment it starts to scratch on your tolerance area again, it will get prone to the same frame pacing instability. Just try to use slightly higher/lower ups (update frequency) than your monitor's refresh rate (by 1% or so), and you should find cases where you run into the "0-2..."-problem again.
Logged

Independent game developer with an elaborate focus on interesting gameplay, rewarding depth of play and technical quality.<br /><br />Trap Them: http://store.steampowered.com/app/375930
goob256
Level 2
**



View Profile WWW
« Reply #19 on: September 13, 2018, 11:11:31 AM »

I have, on newer versions of OS X in particular. What I did to fix the 0-2-0-2-0-2 situation J-Snake described was to add a small tolerance value to my run loop, so that it would try to update the game exactly once per call as long as it was within 0.25 frames of where it thought it needed to be in either direction. When driving my run loop from a system timer set to 60 hz, in practice I was seeing something like 1-2-0-1-2-0, which the tolerance value smoothed out pretty much perfectly.

Hm, I actually have a small tolerance in my latest game, too, though it was just a precaution, not something I noticed. I actually don't really know if my game plays well on anything but 60 Hz though.

EDIT: Looks like what my loop does is skip drawing for a frame if FPS is 2+ FPS below target. I'd like to test a 144 Hz screen with this sometime.
« Last Edit: September 13, 2018, 11:43:09 AM by goobliata » Logged
Pages: [1] 2
Print
Jump to:  

Theme orange-lt created by panic