Advertisement

Dewitters Game Loop - Understanding

Started by July 04, 2017 07:03 PM
8 comments, last by Rutin 7 years, 4 months ago

Hello everyone,

I'm not sure if this is the right area to place this in as I'm not new to C++ or game design, however in the past I had the framework handle my FPS, but now I'm doing it myself, and would be new to implementation of a fixed game speed, with rendering FPS as fast as possible. I have experience in using Variable Time-steps, but I'm not a fan of them. I've also set up a fixed FPS with logic and draws under the same hood, as well as a grid system for movement prior while letting the framework handle time. If this is in the wrong area please feel free to move it.


I've been spending sometime on researching game loops but wanted to be sure I understand what makes everything tick. Personally, it's not enough for me to have something work unless I understand why it works the way it does.

I'm using Dewitters Game Loop - Constant Game Speed independent of Variable FPS
http://www.koonsolo.com/news/dewitters-gameloop/

My current code is below and I wanted to make sure I understand what is going on.


 // Timer for 30 INPUT LOGIC UPDATES PER SECOND
        const float ticksPerSecond = 30.f; // Updates per second
        const float skipTicks = 1000.f / ticksPerSecond; // This would be 33.33333333333333
        const int maxFrameSkip = 5; // Force draw if looping more than 5 times

        sf::Clock mainClock;
        double nextTick = mainClock.restart().asMilliseconds(); // Set nextTick to = current time elapsed in Milliseconds (reset first)

        int loops = 0;
        float interpolation = 0.f;

        while (mainWindow.isOpen()) {

                // Event Processing
                sf::Event event;

                while (mainWindow.pollEvent(event)) {
                        // Closing Window
                        if (event.type == sf::Event::Closed) {
                                mainWindow.close();
                        }
                }

                loops = 0;
                while (mainClock.getElapsedTime().asMilliseconds() > nextTick && loops < maxFrameSkip) {
                        
                        // Updates - LOGIC
                        Input()
                        Logic();

                        nextTick += skipTicks;
                        loops++;
                }

                // Clear Window
                mainWindow.clear();

                // Draw to Window
                mainWindow.draw(SOMETHING);

                // Display
                mainWindow.display();

                std::cout << "Draw - loops: " << loops << std::endl;

                // Just to see if updates are at 30 FPS
                std::cout << "UPDATE FPS: " << (mainClock.getElapsedTime().asMilliseconds() / nextTick) * 30 << std::endl;
        }

My understanding is that I'm using ticksPerSecond to dictate that I only want my logic and updates to cycle through a maximum of 30 times per second.

skipTicks = 1000 / 30; would be the amount of time per update in milliseconds.

I then have maxFrameSkip = 5 so if loop exceeds this amount it will not loop through logic, but just render and reset the counter.

mainClock is for tracking the amount of time, and nextTick will just equal mainClock in milliseconds.

loops is just for tracking if we hit the 5+, and interpolation isn't a problem yet as I haven't gone that far yet.

The main thing I need to understand is the loop, and how it works.

My loop starts by setting the loop counter to 0, and then checking to see if the elapsed time is greater than nextTick, as well as seeing if loops is less than our maxFrameSkip variable.

I'm assuming it's supposed to work like this. The loop will make sure to hit once per every 0.03333 seconds, and render as fast as possible outside of this. If the loop starts and the game time is 20 ms, and our nextTick is 33.333 ms it will draw until the timer hits a number greater than 33.3333 ms. Let's suppose the game timer hits 35 ms it will loop through making nextTick 66.6666 ms, then render as fast as possible until the game timer is greater than nextTick, ect...

If the loop takes too long it will make the game timer much higher than nextTick which could cause the loop to cycle through several times and not draw, this is why we have the loop counter and maxFrameSkip, so if it hits a number greater than 5 it forces the draw while trying to catch up. I'm confused on what would happen if the game was hanging several times, couldn't it create an impossible scenario that wouldn't be able to catch up? Wouldn't it be a good idea to track if we hit our loop at 5 counts x amount of times, to make nextTick = our game time + skipTick to reset?

Also, is this a correct way to messure the update time for the loop?
 


std::cout << "UPDATE FPS: " << (mainClock.getElapsedTime().asMilliseconds() / nextTick) * 30 << std::endl;


Is my game loop correct in keeping event input outside of the loop, and only having real-time input (moving game toons around) + logic within? Then drawing outside as the final step?

I wanted to make sure I have a good understanding of what is going on here before I move forward. Thank you!

 

EDIT: I set up interpolation and it's moving very smooth. Thanks for all the help!

Programmer and 3D Artist

That’s more or less the gist of the game loop.  I recommend also supplementing your reading with Gaffer’s Fix Your Timestep.  It is a similar implementation and may answer your other questions.

Advertisement
1 minute ago, fastcall22 said:

That’s more or less the gist of the game loop.  I recommend reading Gaffer’s Fix Your Timestep

Thank you for the quick reply! I'll be sure to read up on Fix Your Timestep! I've seen this referenced a lot online as the Gold Standard.

The only issue I can see with Dewitters Game Loop would be finding your game in a scenario that makes the Game Time much higher than the Next Tick, making it impossible to catch up, and forcing the game to cycle 5 times for logic/updates/input, then drawing once, and repeating until it can if at all catch up. I assume the programmer would need to program in a way to say "Hey, we cannot catch up, just reset and continue as normal." I know if this happened a lot it would most likely mean something isn't optimized properly in the logic.

I really appreciate the link, thanks again!

Programmer and 3D Artist

13 minutes ago, Rutin said:

"Hey, we cannot catch up, just reset and continue as normal." I know if this happened a lot it would most likely mean something isn't optimized properly in the logic.

Gaffer addresses this by clamping the elapsed time per loop to a certain number of updates.  There are other ways this can happen as well, such as pausing the game while you’re in the debugger.  :^)

1 hour ago, fastcall22 said:

Gaffer addresses this by clamping the elapsed time per loop to a certain number of updates.  There are other ways this can happen as well, such as pausing the game while you’re in the debugger.  :^)

I didn't think about that! Good to know. I noticed when I was using the debugger the loop count would go 1, 2, 3, 4, 5 and reset, just not in real time. I understand why now, thanks!

When handing input updates in the past I did this as part of the Time Step, is this still the same? Other than window events, everything I have is within the 30 ticks per second. This would include keyboard input when moving a sprite, or should input be taken at anytime outside of the loop, and stored until the loop is able to cycle through input updates to apply new positions for the sprite? Wouldn't outside events allow faster movement depending on the input speed of the keyboard or settings within Windows as the event can be called as fast as the system allows for a key stroke?

I've only heard about input delay issues when doing this within a time step, as opposed to storing all updates, then applying them once the cycle happens.

Programmer and 3D Artist

It's worth not overthinking the issue of input latency - what matters is the round trip between the player seeing a stimulus and the player seeing the game reflect their input response to that stimulus, and since that inevitably involves primarily display latency, where the input happens along that timeline is less important than you might think. Normal human reaction time is well over 100 milliseconds, i.e. 3 to 6 visual frames for most games, so you're unlikely to see any significant difference - especially with parameters like the ones above, where you're typically performing either zero or one update per visual frame! But if you want to poll for input inside every update instead of before performing the update loop, it can't hurt.

Advertisement
1 minute ago, Kylotan said:

It's worth not overthinking the issue of input latency - what matters is the round trip between the player seeing a stimulus and the player seeing the game reflect their input response to that stimulus, and since that inevitably involves primarily display latency, where the input happens along that timeline is less important than you might think. Normal human reaction time is well over 100 milliseconds, i.e. 3 to 6 visual frames for most games, so you're unlikely to see any significant difference - especially with parameters like the ones above, where you're typically performing either zero or one update per visual frame! But if you want to poll for input inside every update instead of before performing the update loop, it can't hurt.

Thank you Kylotan. I appreciate the reply.

Programmer and 3D Artist

You do want to think about the ABA problem if you're polling. That's not really what would be described as a latency problem, but the slower your simulation rate the more relevant it becomes. If you're just using the analog stick to move something around then it's not a big deal because the input pattern is the stick being held in some direction for a period of time, but if your game deals with rapid button presses like Street Fighter or Parappa then you definitely want to collect input messages and then process them during the update. That issue will be exacerbated at 30hz compared to 60.


One approach I've used to deal with this is to have my input class set up such that each registered key has several methods for checking its state. Messages from the OS accumulate in a buffer until the tick begins, at which point the input object is updated. The update sets the state representation of each registered key and they may then be queried using:


triggered() //this key went from up to down since the last poll
pressed() //this key is down right now (during the poll)
released() //this key went from down to up since the last poll

note that a key could be triggered, pressed, and released at the same time if the player releases the key after the previous poll and then pushes it back down again before this poll. It's also possible for the player to trigger and release the key between polls, in which case it would not be "pressed", but would be triggered and released.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
2 hours ago, Khatharr said:

You do want to think about the ABA problem if you're polling. That's not really what would be described as a latency problem, but the slower your simulation rate the more relevant it becomes. If you're just using the analog stick to move something around then it's not a big deal because the input pattern is the stick being held in some direction for a period of time, but if your game deals with rapid button presses like Street Fighter or Parappa then you definitely want to collect input messages and then process them during the update. That issue will be exacerbated at 30hz compared to 60.


One approach I've used to deal with this is to have my input class set up such that each registered key has several methods for checking its state. Messages from the OS accumulate in a buffer until the tick begins, at which point the input object is updated. The update sets the state representation of each registered key and they may then be queried using:



triggered() //this key went from up to down since the last poll
pressed() //this key is down right now (during the poll)
released() //this key went from down to up since the last poll

note that a key could be triggered, pressed, and released at the same time if the player releases the key after the previous poll and then pushes it back down again before this poll. It's also possible for the player to trigger and release the key between polls, in which case it would not be "pressed", but would be triggered and released.

Thanks! I need to keep this in mind because I'm working on a networking game which would need to store key presses.

Programmer and 3D Artist

This topic is closed to new replies.

Advertisement