Timesteps, tick rates, and their logic

Started by
4 comments, last by AnotherFalseProphet 11 years, 3 months ago

I'm trying to grasp the concept of timesteps and tick rates. I've read a couple articles but they talk about things like Runge Kutta integration and I honestly have no idea what to make of that.

Are there any examples with plain ole' C and SDL?

Advertisement
Have you read this article titled "Fixed your timestep!"? It uses c++ for examples, but most of it can translate to c pretty easily.

I have. But, for a beginner, it's not easy to follow.

I'm fairly good with C, and I can understand the source that the article provides, but I don't understand its practical use or logic.

Runge kutta and euler are methods to approximate of the change in a function over a given time span, and they use different forms of approximation. Euler is very straight forward and just calculates the change over the time span you are interested in and aplies this to the previously calculated values. As you can see this will verge away from the actual value fairly quickly. Runge Kutta is a method of doing this that increases the accuracy of this approximation. The bigger the time span the bigger the values you calculate and thus the bigger the steps you are making in your simulation, which leads to bigger errors. This is why you simulate with a small timestep say 0.01 seconds and then run your simulation for however long it took to update from the last frame. This way the errors in your simulation are a known and you can take these into account when doing collision response or what ever you were trying to approximate. Also it gives you a fixed sample rate on the function instead of a variable one which is what you get if you just pass in the actual elapsed time since the last simulation.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

I actually ended up working out fixed timesteps on my own due to issues with precise timing that I encountered, though not entirely identical to the approach described in that article, it's still very similar. I can explain some of the reasoning, maybe that will help.

The idea is to achieve frame-rate independent updates.

The first part I hope is obvious - updating (or i guess what the article calls as Integrate(..) ) is usually expensive. I know it is for me, as I do collision in it. That means that it's a bad idea to call your Update method for every frame you render.

The other side of this, even if your Update method is not that expensive, rendering times can be incredibly quick - which means if you're passing in a dTime (the time between two updates) into your update method, these small numbers may end up causing issues with your math. I've seen this happen surprisingly often, since even if all you math is done with doubles (as opposed to floats), you'll still find the inevitable lack of precision issue.

In pseudo-code this looks like:


while (gameIsRunning)
{
  Update(timeSinceLastUpdate);
  Draw()
}

The next thing I did, and used for a long while, was to simply put in a check to see if more than some time (1/60th of a second in my case) has passed since the last update. If more than 1/60th had passed, I called update with the amount of time that had passed since the last update. This worked fine for me for a while. The main situations where this seemed to work fine was when I cared less about precision and constant reproducibility.

Basically


while (gameIsRunning)
{
  if (timeSinceLastUpdate > 1/60.0)
    Update(timeSinceLastUpdate);
  Draw();
}

The other major problem with this approach is that if, say, you have some hiccup on your pc, which causes your framerate to drop to like 2fps for a brief second. By the above method, this would cause at least one call to be made to Update where the dTime is something like 0.5 seconds (2 frames per second means that 0.5 seconds pass between the two Update calls).

When I started working on a project that dealt with smaller distances, this ended up causing issues for me. A hiccup could sometimes cause the player to teleport through an object that would normally have stopped him. This is because a dTime of 0.5 seconds caused the player to 'move' something like 2 meteres in a single update frame, which meant that (due to the way my collision was setup) the obstacle in his way was never even collided with. This gets even worse if your objects in game move at higher speeds, causing them to jump even larger distances in case of a framerate slowdown.

So the solution to this is to always update (or integrate, whatever you call it) by a fixed timestep - not allowing huge dTime values to be passed into your update function since this could cause issues. In my terms, this simply meant this:


while (gameIsRunning)
{
  if (timeSinceLastUpdate > 1/60.0)
    Update( 1/60.0 );
  Draw();
}

This works fine, but has one issue. (well more than one, but one major) - if your timeSinceLastUpdate ends up being like 0.5 seconds, you'll still only end up updating your world as though only 1/60 of a second has passed. This basically translates to mean that if your frame rate drops below 60fps (because we used 1/60), your game will appear to run slower (as in, the movement would feel smaller).

I'm starting to feel like i'm just repeating that article here. But the solution is to use a while loop, as described there, like this:


while (gameIsRunning)
{
  while (timeSinceLastUpdate > 1/60.0)
  {
    Update( 1/60.0 );
    timeSinceLastUpdate -= 1/60.0;
  }
  Draw();
}

This also has issues - namely the spiraling death issue as he calls it where you can end up calling your Update too much if your timeSinceLastUpdate is too large, which is why he limits the maximum timeSinceLastUpdate to be something like 0.25 seconds - which means that you won't call your Update more than fifteen (in this case) times per frame. This does mean some slowdowns as well, if your framerate is really low, but this is an acceptable tradeoff.

The last thing he handles is that even if you do the above with the while loop, you'll inevitably have some non-zero value left in timeSinceLastUpdate that's smaller than 1/60. Yet this is still a time that has passed that should be accounted for by your update/integration. That's the last bit he mentions - interpolating between physics states. This isn't a bad idea, though I don't use this since I also ran into issues with double precision when trying something similar. I basically end up updating my game in a way that it thinks its running slightly slower than it actually is running - but on the other hand, i have consistent physics where things are a bit more predictable.

Now I'm really starting to repeat the article. :)

Bottom line is this: You always want to update/integrate at the right time - if you update too often, you run into performance and precision issues. If you update too rarely you can run into teleportation and improper physics calculations issues.

Feel free to ask questions if what I wrote wasn't entirely clear.

Great post! Thank you! Very helpful for me.

This topic is closed to new replies.

Advertisement