lørdag 5. juli 2014

[ SDL2 - Part 9 ] No more delays!

Getting rid of SDL_Delay


Up until now, we've been regulating movement by just setting a delay of 16 milliseconds at the end of each frame like so : SDL_Delay( 16 )


This is bad because sometimes a frame takes a bit longer. In our code, there is so little going on, so the frames usually take less than on MS. So we've added a delay of 16 ms. But things may happen, and suddenly a frame takes 5 ms. What happens then? Well, your animations will jump. And if this happen regularly, your animation will be choppy. We don't want choppy animations, we want smooth animations.How do we achieve this?

Method 1 - our current solution


We've set an aim for 60 frames per second. Since there is 1000 millisconds ( ms ) in a second, each frame should take 1000 ms / 60 frames = 16 ms.

Since our code is very light, we've just assumed that each frame takes less than 1 ms, so we've added a delay.

Problem : what if one or more frames uses more than 1 ms to run? Our animations will get choppy and we don't want that

Method 2 - using delta


Another, better way we could limit framrate is to calculate delta time and use that to set the delay, of. Delta time is the time between each frame ( or update. ) If a frame takes 3 ms, delta will be 3 ms. In method 1 we had a 16 ms delay and assumed the frame took 0 ms to run. But if the frame took 3 ms, we "loose" 3 ms of the delay time and end up with a delay tie of 16ms - 3ms = 13 ms.

This is a valid solution, and your game will run relatively smoothly in 60 fps.

Method 3 - using delta, no delay


Method 1 and 2 has worked by limiting frame rate. But why limit it? We can just skip the delay altogether and just use the delta time to adjust animation. Most modern games works this way. Since we adjust movement with delta time, it doesn't matter if we run in 60 fps or 6000 fps. For smooth animation, this method is superior to the ones above. Waiting 16 ms between frame can make objects jump a tiny bit. Better to eliminate this delay at all.

It's also very simple, just multiply the movement with the delta time.

Implementation


Implementing frame independent movement is quite simple, but there are a few things you need to be aware of in order to get it right.

Delta time


Probably the most important point. In order to get animations right, the delta tie needs to be accurat. Very accurate, preferably with microsecond ( µs ), or nanoseconds ( ns )

There are various ways to do this.
  • The C way</li>
    • - Confusing ( functions takes a struct and fills it with time / date )
    • - Not always accurate
    • - Hard to use correctly
    • - Platform dependent ( no one way to do it in Linux and Winows ) 
    • + No library needed,
    • + Doesn't need C++11 support
  • Boost
    • - You'd need to download, and link with boost
    • - Adds and a dependency, just for time
    • + Cross-platform
    • + Doesn't need C++11 support
  • C++11
    • - Syntax can be hard to understnad
    • + Does everything you need easily
    • + Cross platform
    • + Well documented
Both boost and C++11 are good alternatives. In this tutorial, we'll cover the C++11 way. The main reason for this is that it means we don't have to install boost. And the C++11 way is well documented, though lacking of examples.

std::chrono


Chrono is the C++11 way for everything related to time and timing. It's quite large and complex, but I'll try to give a short explanation of how we get the delta tie using chrono. std::chrono::time_point is the basic C++11 time structure. It contains std::chrno::duration, which contains information about a point in time. More specific it contains a tick variable that contains the number of time units since 1.1.1970, and a std::ration which says what unit of time the ticks are in ( secnds, monutes, milliseconds, etc... )

This might be kinda confusing, but all you need to remember is that std::chrono::time_point represent a point in time. So let's implement our delta function. First of all, we're gonna make things a lot easier for ourselves by adding using namespace std::chrono;. This is not necessary, but it means we don't have to add std::chrono:: in front of everything.

So now we need to get the point in time, this can be done using high_resolution_clock::now(); which returns a std::chrono::time_point

time_point timeCurrent = high_resolution_clock::now();

This will be done at the beginning of our GetDelta() function. We also need a variable to store the previous point in time ( the last tie GetDelta() goy called. We'll set this at the end of our GetDelta() like this ( timePrev is just a time_point )

timePrev = timeCurrent;

Now that we have our two time_points, we can calculate the time between them ( the delta time. ) time_point supports arithmetic operations such as - and + so we can easily find the time difference between them by doing time_point timeDiff = ( timeCurrent - timePrev ).

auto timeDiff = timeCurrent - timePrev;

So now we have a third time_point that contains the difference between the two frames. This is essentially the delta time represented as a time_point. But there is anther problem : we have no idea what time unit the duration object uses. So to be certain we get the time as nanoseconds, we need to cast the duration inside the time_point to be using nanoseconds. To do this, we use a duration_cast<> we can simplify it a little to calculate timeDiff and cast it in the same line :

auto timeDiff = duration_cast< nanoseconds >( timeCurrent - timePrev );

To get the actual number of time unites, we use the count() member function :

double delta = duration_cast< nanoseconds >( timeDiff ).count();

And finally we have the delta. But since it's in nanoseconds, we need to convert it into seconds :
delta /= 1000000000;

There! It's a bit complicated with all the different variables types, but we now have a function for getting delta time. And it will work on any platform as long as it supports C++11, not extra libraries needed. Here is the finished function :


Using the delta time


Now that the worst part is over, it's time for some minor details on how to implement frame independent movement.

First of all, the animated objects needs to have speed. And for better precision, we use doubles. Let's make a simple struct for holding the speed. Our speed will have values between -1.0 and 1.0.
struct Speed
{
     double x;
     double y;
};
We also need an Update( double delta ) functions that's used to update the position of the object.

rect.x += speed.x * delta;
rect.y += speed.y * delta;

There's a few things wrong with this, though. First of all, delta values will be really small, and our speed is between-1.0, and 1.0 so the resulting movement will be really small. We can solve this by multiplying with a constant.

rect.x += speed.x * delta * 300;
rect.y += speed.y * delta * 300;

But there is a second issue here : both speed and delta are likely to do decimal numbers. So the result of the multiplication will almost always be a decimal number as well. But we just put it into an int and this means we'll loose the decimal precision. So if the result is 0.8, we'll end up adding 0 to x or y. And if the same things happen in the next frame, we've "lost" 1.6 worth of movement. A while pixel! It might seem like a small thing, but it would make the movement really choppy. So instead we introduce two new member variables to our Texture struct to hold the result of the multiplication. So here's our final Update function:

double x;
double y;

// .......

void Update( double delta )
{
     x += speed.x * delta * 100;
     y += speed.y * delta * 100;
  
     rect.x = x;
     rect.y = y;
}

Conclusion


That concludes the tutorial on delta timers. I've made a few updates, put things into different classes and other minor improvements. The code is too big to include in this blog post, but you can find a full zip of it here.

 I've added a new .cpp file, so you need to add that to the compilation string if you're using clang or gcc

clang++ main.cpp Texture.cpp -std=c++11 -lSDL2 -lSDL2_image -o Game

As always, feel free to post a comment or message me if you have a question.

1 kommentar: