📜 ⬆️ ⬇️

How to make the game run at 60fps

Imagine the task: you have a game, and you need it to work at 60 fps on a 60 Hz monitor. Your computer is fast enough for rendering and updating to take an insignificant amount of time, so you turn on vsync and write this game cycle:

while(running) { update(); render(); display(); } 

Very simple! Now the game works with 60fps and everything goes like clockwork. Is done. Thank you for reading this post.


Well, obviously, that is not so good. What if someone has a weak computer that cannot render the game with enough speed to ensure 60fps? What if someone bought one of those cool new 144 Hz monitors? What if it is disabled in the vsync driver settings?

You might think: I need somewhere to measure time and provide an update with the correct frequency. It is quite simple to do this - it is enough to accumulate time in each cycle and perform an update every time it exceeds the threshold of 1/60 second.
')
 while(running) { deltaTime = CurrentTime()-OldTime; oldTime = CurrentTime(); accumulator += deltaTime; while(accumulator > 1.0/60.0){ update(); accumulator -= 1.0/60.0; } render(); display(); } 

Done, nowhere easier. In fact, there are a lot of games in which the code essentially looks that way. But it's not right. This is suitable for adjusting timings, but leads to problems with stuttering and other mismatches. Such a problem is very often encountered: frames are displayed not exactly 1/60 of a second; even when vsync is turned on, there is always a slight noise in the time of their display (and exactly the OS timer). Therefore, there will be situations when you render a frame, and the game believes that the time for the re-update has not come yet (because the battery is late for a tiny share), so it simply repeats the same frame again, but now the game is late for the frame, therefore it performs a double update. Here and twitching!

Googling, you can find a few ready-made solutions to eliminate this twitching. For example, a game can use a variable, rather than a constant time step, and just completely abandon the batteries in the timing code. Or you can implement a constant time step with the interpolating renderer, described in Glenn Fieder’s rather famous article “ Fix Your Timestep ”. Or you can remake the timer code so that it is a bit more flexible, as described in the post " Frame Timing Issues " by Slick Entertainment (unfortunately, this blog is no longer there).



Fuzzy timings


The Slick Entertainment method with “fuzzy timings” in my engine was the easiest to implement, because it did not require changes in the logic of the game and rendering. So in The End is Nigh, I used it. It was enough just to insert it into the engine. In fact, it simply allows the game to update "a little earlier," to avoid problems with timing mismatch. If vsync is turned on in the game, it simply allows you to use vsync as the main timer, and provides a smooth picture.

This is how the update code looks now (the game “can work” at 62 fps, but it still processes each time step as if it works at 60fps. I don’t quite understand why limit it so that the battery values ​​do not fall below 0, but without This code does not work). You can interpret it like this: “the game is updated with a fixed step, if rendered in the interval from 60fps to 62fps”:

 while(accumulator > 1.0/62.0){ update(); accumulator -= 1.0/60.0; if(accumulator < 0) accumulator = 0; } 

If vsync is turned on, then it essentially allows the game to work with a fixed pitch, which coincides with the monitor refresh rate, and provides a smooth picture. The main problem here is that when vsync is disabled, the game will run a little faster, but the difference is so insignificant that no one will notice it.

Speedrunners. Speedrunners will notice. Soon after the release of the game, they noticed that some people in the speed-run record lists had a worse time, but by calculation it turned out to be better than others. And the immediate cause of this was the lack of timings and disabling vsync in the game (or 144 Hz monitors). Therefore, it became obvious that you need to turn off this fuzziness when disabling vsync.

Oh, but we can't check whether vsync is disabled. In the OS, there are no calls for this, and although we can request from the application to enable or disable vsync, in fact it depends entirely on the OS and the graphics driver. The only thing you can do is to render a bunch of frames, try to measure the time it takes to complete this task, and then compare whether they take approximately the same time. That's what I did for The End is Nigh . If the game does not include vsync with a frequency of 60 Hz, then it rolls back to the original frame timer with “strict 60 fps”. In addition, I added a parameter to the configuration file that forces the game not to use fuzziness (mainly for speedrunners who need exact time) and added an accurate handler for the in-game timer for them, which allows using autosplitter (this is a script that works with the atomic time timer).

Some users still complained about the occasional jerking of individual frames, but they seemed so rare that they could be explained by OS events or other external causes. Nothing really scary. Right?

Looking through my recent timer code, I noticed something strange. The battery shifted, each frame took a little longer than 1/60 of a second, so the game periodically thought that it was late for the frame and performed a double update. It turned out that my monitor runs at 59.94 Hz, not 60 Hz. This meant that every 1000 frames he had to perform a double update in order to “catch up”. However, it is very easy to fix - it is enough to change the interval of allowable frame rates (not from 60 to 62, but from 59 to 61).

 while(accumulator > 1.0/61.0){ update(); accumulator -= 1.0/59.0; if(accumulator < 0) accumulator = 0; } 

The problem described above with disabled vsync and high-frequency monitors still persists, and the same solution applies (rollback to the strict timer if the monitor is not synchronized vsync to 60).

But how do you know if this is the right solution? How to make sure that it will work correctly on all combinations of computers with different types of monitors, with vsync disabled and enabled, and so on? It is very difficult to monitor all these timer problems in the head, and to understand what causes out of sync, strange cycles and the like.

Monitor simulator


Trying to come up with a reliable solution to the “problem of the 59.94 Hertz monitor”, I realized that I could not simply carry out tests and tests, hoping to find a reliable solution. I needed a convenient way to test different attempts at writing a quality timer and a simple way to check if it caused a twitch or a time shift in different monitor configurations.

Monitor Simulator appears on the scene. This is a “dirty and fast” code written by me, simulating “monitor operation”, and in fact outputting to me a bunch of numbers that give an idea of ​​the stability of each timer being tested.

For example, for the simplest timer, the following values ​​are displayed from the beginning of the article:

20211012021011202111020211102012012102012[...]
TOTAL UPDATES: 10001
TOTAL VSYNCS: 10002
TOTAL DOUBLE UPDATES: 2535
TOTAL SKIPPED RENDERS: 0
GAME TIME: 166.683
SYSTEM TIME: 166.7


First, the code displays for each vsync emulated number of the number of "updates" of the game cycle after the previous vsync. Any values ​​other than solid 1 result in a jerky picture. At the end of the code displays the accumulated statistics.

When using a “fuzzy timer” (with an interval of 60–62fps) on a 59.94 Hertz monitor, the code displays the following:

111111111111111111111111111111111111111111111[...]
TOTAL UPDATES: 10000
TOTAL VSYNCS: 9991
TOTAL DOUBLE UPDATES: 10
TOTAL SKIPPED RENDERS: 0
GAME TIME: 166.667
SYSTEM TIME: 166.683


The frame jerking occurs very rarely, so it can be difficult to notice with so many 1. But the output statistics clearly show that the game has performed several double updates here, which leads to jerking. In the revised version (with an interval of 59–61 fps), there are 0 missing or double updates.

You can also disable vsync. The remaining statistics become unimportant, but it clearly shows me the value of the “time shift” (the system time shift relative to where the game time should be).

GAME TIME: 166.667
SYSTEM TIME: 169.102


That is why, with vsync disabled, you need to switch to a strict timer, otherwise these discrepancies accumulate over time.

If I assign the rendering time a value of .02 (i.e., for rendering, I need “more than a frame”), then again I get a twitch. Ideally, the game pattern should look like 202020202020, but it is a bit uneven.

In this situation, this timer behaves a little better than the previous one, but it becomes more and more complicated and harder to figure out how and why it works. But I can just shove the tests into this simulator and check how they behave, and you can understand the reasons later. Samples and errors, baby!

 while(accumulator >= 1.0/61.0){ simulate_update(); accumulator -= 1.0/60.0; if(accumulator < 1.0/59.0–1.0/60.0) accumulator = 0; } 

You can download a monitor simulator and independently check various methods for counting timings. Email me if you find something better.

I am not 100% satisfied with my decision (it still requires a “vsync recognition” hack and there may be some twitching when out of sync), but I think it is almost as good as trying to implement a game cycle with a fixed pitch. Part of this problem arises because it is very difficult to determine the parameters of what is considered “acceptable” here. The main difficulty lies in the trade-off between the time shift and double / missing frames. If you run a 60 Hz game on a 50 Hz PAL monitor ... what would be the right decision? Do you want wild jerking, or noticeably slower game play? Both options seem bad.

Separated rendering


In the previous methods I described what I call “fixed-step rendering” (lockstep rendering). The game updates its state, then renders, and when rendering, it always displays the most recent state of the game. Rendering and updating are connected together.

But you can separate them. This is exactly what the method described in the post " Fix Your Timestep " does . I will not repeat, you definitely should re-read this post. This is (as I understand it) the “industry standard” used in AAA games and engines such as Unity and Unreal (however, in intense active 2D games, they usually prefer to use a fixed pitch (lockstep), because sometimes the accuracy that this method).

But if we briefly describe Glenn's post, then it simply describes the update method with a fixed frame rate, but the rendering interpolates between the “current” and “previous” game state, and the current battery value is used as the interpolation value. With this method, you can render with any frame rate and update the game with any frequency, and the picture will always be smooth. No twitching, works universally.

 while(running){ computeDeltaTimeSomehow(); accumulator += deltaTime; while(accumulator >= 1.0/60.0){ previous_state = current_state; current_state = update(); accumulator -= 1.0/60.0; } render_interpolated_somehow(previous_state, current_state, accumulator/(1.0/60.0)); display(); } 

So, elementary. Problem solved.

Now you just need to do so that the game could render the interpolated states ... but wait, in fact, this is not at all easy. In Glenn's post, it is simply assumed that this can be done. It is easy enough to cache the previous position of the game object and interpolate its movements, but the state of the game includes much more than anything else. It is necessary to take into account in it the state of animation, the creation and destruction of objects, and a whole lot more.

Plus, in the logic of the game, it is necessary to take into account whether the object is teleported or should be smoothly moved so that the interpolator does not make false assumptions about the path made by the game object to its current position. This chaos can occur with turns, especially if in one frame the rotation of an object can change more than 180 degrees. And how to handle the created and destroyed objects?

At the moment I'm just working on this task in my engine. In fact, I just interpolate the movements, and leave everything else as it is. You will not notice jerking if the object does not move smoothly, so skipping animation frames and desynchronizing the creation / destruction of an object down to one frame will not become a problem if everything else is done smoothly.

However, it is strange that, in essence, this method renders the game in a state that is late by 1 state of the game from where the simulation is now located. This is imperceptible, but it can connect with other sources of delays, for example, input delay and monitor refresh rate, so those who need the most responsive gameplay (I'm talking about you, speedrunners) are likely to prefer to use lockstep in the game.

In my engine, I just give a choice. If you have a 60 Hz monitor and a fast computer, then it is best to use lockstep with vsync enabled. If the monitor has a non-standard refresh rate, or your weak computer cannot constantly render 60 frames per second, then turn on frame interpolation. I want to call this option “unlock framerate” (“unlock frame rate”), but people might think that it simply means “turn on this option if you have a good computer”. However, this problem can be solved later.

In fact, there is a method to work around this problem.

Updates with variable time steps


Many people asked me why not just update the game with a variable time step, and theorists often say: “if the game is written RIGHT, then you can just update it with an arbitrary time step”.

 while(running) { deltaTime = CurrentTime()-OldTime; oldTime = CurrentTime(); update(deltaTime); render(); display(); } 

No oddities with timings. No strange interpolation rendering. It's simple, everything works.

So, elementary. Problem solved. And now forever! The best result is impossible to achieve!

Now it is enough just to make the game logic work with an arbitrary time step. It's simple, just replace all the code:

 position += speed; 

on this:

 position += speed * deltaTime; 

and replace the following code:

 speed += acceleration; position += speed; 

on this:

 speed += acceleration * deltaTime; position += speed * deltaTime; 

and replace the following code:

 speed += acceleration; speed *= friction; position += speed; 

on this:

 Vec3D p0 = position; Vec3D v0 = velocity; Vec3D a = acceleration*(1.0/60.0); double f = friction; double n = dt*60; double fN = pow(friction, n); position = p0 + ((f*(a*(f*fN-f*(n+1)+n)+(f-1)*v0*(fN-1)))/((f-1)*(f-1)))*(1.0/60.0); velocity = v0*fN+a*(f*(fN-1)/(f-1)); 

... well, wait a minute

Where does all this come from?

The last part is literally copied from the auxiliary code of my engine, which performs “really correct, independent of frame rate movement with friction limiting speed”. There is a little rubbish in it (these multiplications and divisions by 60). But this is the “correct” version of the code with a variable time step for the previous fragment. I calculated it for more than an hour with the help of Wolfram Alpha .

Now they may ask me why not do this:

 speed += acceleration * deltaTime; speed *= pow(friction, deltaTime); position += speed * deltaTime; 

And although it seems to work, in fact, so wrong. You can check it yourself. Perform two updates with deltaTime = 1, then perform one update with deltaTime = 2, and the results will be different. Usually we want the game to work in concert, so such discrepancies are not welcome. This is probably a fairly good solution if you know for sure that deltaTime is always approximately equal to one value, but then you need to write code that ensures updates are performed at some constant frequency and ... yes. True, now we are trying to do everything "CORRECT".

If such a tiny piece of code unfolds into monstrous mathematical calculations, then imagine more complex patterns of motion, in which many interacting objects participate, and so on. Now you can clearly see that the "correct" solution is unrealizable. The maximum that we can achieve is a “rough approximation”. Let's forget about it for now, and let's say that we really have a “really correct” version of the motion functions. Great, right?

Actually, no. Here is a real example of the problem that I had with this in Bombernauts . A player can jump about 1 tile, and the game takes place in a grid of blocks into 1 tile. To land on the block, the character’s feet must rise above the top surface of the block.


But since collision detection is performed here in discrete steps, if the game works with a low frame rate, sometimes the legs will not reach the surface of the tile, even though they follow the same motion curve, and instead of lifting the player will slide off the wall.


Obviously, this problem is solved. But it illustrates the kinds of problems that we encounter when trying to properly implement the work of the game cycle with a variable time step. We lose coherence and determinism, so we have to get rid of the replay functions of the game by recording the player’s input, deterministic multiplayer and the like. For reflex-based fast 2D games, consistency is extremely important (and hello to speedrunners again).

If you try to adjust the time steps so that they are neither too large nor too small, then lose the main advantage gained from the variable time step, and you can safely use the other two methods described here. The game will not be worth the candle. Too much extra effort will be invested in the game logic (implementation of the correct mathematics of motion), and it will take too many victims in the field of determinacy and consistency. I would use this method only for a musical rhythm game (in which the equations of motion are simple and maximum responsiveness and smoothness are required). In all other cases, I will choose a fixed update.



Conclusion


Now you know how to make the game run at a constant frequency of 60fps. This is trivially simple, and no one else should have problems with it. There are no other problems complicating this task.

Source: https://habr.com/ru/post/442528/


All Articles