Best way of sending updates at fixed intervals.

Started by
7 comments, last by hplus0603 11 years, 3 months ago

Assume my game is running at a rate of 60Hz (~16.66 ms) on both the server and client and updates from the server to the client is sent at 20Hz (50ms). Now since the game is running at 60Hz, there are two ways of calculating the interval for the client updates. I can either just do this:


if((currentTime - lastSendTime) >= 0.05) {
     SendUpdate();
     lastSendTime = currentTime;
}

And just check this every frame for each client. But to me it seems like a cleaner solution would be to specify the client send rate in "every n:th" frame instead, so if I want to send at 20Hz when running at a 60Hz simulation rate I would just do this:


if(frameNumber % 3 == 0) {
    SendUpdate();
}

It just feels a lot cleaner, you don't need to keep a separate timer or have to deal with small inaccuracies in the local time of the server, etc. It also becomes nicely synchronized with updates to the game state on the server, etc. The reason I'm asking is because in the few examples of "professional" networking code you can find on the web, I see *everyone* using a the first method of a separate timer for the send time for each client, so I'm thinking I must be missing something here?

Advertisement

For sure, if you can guarantee 60Hz, then frame hopping will be better, due to 'aliasing' associated with the first method (sometimes you will get perfect 1/3 updates, sometimes you will get 1 /4 updates doe to floating point drift).

Then you can also have options for 30Hz, 15Hz, 12Hz, 10Hz, 12Hz, ... (1/3, 1/4, 1/5, 1/6, ). A reason why '60' is a nice number to have.

Professionally, the first method is often used, with maybe some rounding error to compensate for round off errors and frame aliasing. Even sometimes the network tick runs in a separate thread.

Se second advantage, is that you can guarantee the frame timestep (so, each network tick will be exactly 20Hz, or considering to be exact), thus removing the need to implicitly indicate timing (you can measure time using the packet sqn, for example).

Sometimes, the problem of aliasing is not always such a big deal tbh. It depends how things work for you.

Everything is better with Metal.

papalazaru:

If the simulation runs on a fixed timestep (60Hz) - I would take that as a guarantee of 60Hz, it might jitter a *tiny* bit back and forth but at least the whole simulation will "jitter" instead of individual connections send intervals. And yes, the slight aliasing which will happen with the first method is what hit me when I implemented it - but like you say the aliasing could be countered by simply doing some slight rounding or something equivalent.

One thing which trips me up about using the second method of "frame hopping" (never heard that expression before) is this: What happens when I do get jitter on the server, what if I get HUGE jitter and I need to run 4 frames to "catch up" to real time. Then I will send two packets in quick succession... hm, but then again if that happens the client will just perceive it as network jitter anyway. The difference here I suppose is that timing it manually using (lastSend - currentTime) you would only send *one* packet for those four updates.

Here's an other method:


// all times are long given in ms 
const long INTERVAL = 1000/frequency_in_Hz;
if( (current_time / INTERVAL)>(last_time/INTERVAL))
{
 sendUpdate();
 last_time = current_time;
}

This ensures that you sends at most X, equally spaced, updates per second, but less if your performances drops for some reasons.

It is my advice that you should always run physics with fixed time steps. And, thus, if you use fixed time steps. you can also run networking at a fixed number of physics steps.

At the larger scale, if you get a big frame hitch, then yes, you may send to packets in quick succession. And, as you say, this will look to the other end as jitter, which is what it is.

enum Bool { True, False, FileNotFound };
Here's an other method:

// all times are long given in ms 
const long INTERVAL = 1000/frequency_in_Hz;
if( (current_time / INTERVAL)>(last_time/INTERVAL))
{
 sendUpdate();
 last_time = current_time;
}

This ensures that you sends at most X, equally spaced, updates per second, but less if your performances drops for some reasons.

Thats got 2 bugs. Should be more like this:


if(current_time>=last_time+INTERVAL)
{
  sendUpdate();
  last_time += INTERVAL; // to prevent the step being slightly larger
}

Thats got 2 bugs.[/quote]

No, it doesn't, assuming current_time and last_time are integral types. Given that INTERVAL is a long, and he counts in milliseconds, that's a reasonable assumption.

I don't like using milliseconds as integers for timing much, as they are kind-of imprecise, but it actually works alright. The only bug in the code posted above is when the system clock wraps 2 billion milliseconds, which happens a little over three weeks after boot. (And then again nine weeks after boot.)

enum Bool { True, False, FileNotFound };

I was assuming current_time and last_time were real numbers like in the thread starting post; that would mean its nearly always updating.

If they are longs its still not right, for example wanting 60 updates per second 1000/60=16.666666... not 16 and 1000/16=62.5 updates per second.

Depending on 2 divisions to round down appropriately seems not the right way to do this as when the starting time is not exactly divisible by the INTERVAL everything is delayed about the size of remainder forever.

Also there are forgotten updates when the time ticks further than an exact multiple of the INTERVAL sometimes and you just set last_time=current_time instead of adding:

for example wanting 100 updates -> INTERVAL==10

current_time==0 -> no update

current_time==9 -> no update

current_time==21 -> 1 update -> oops should have been 2

I guess you are just assuming larger error margins are ok when I was already counting them as wrong.

I was assuming current_time and last_time were real numbers like in the thread starting post

But if you read the code, that's clearly not what it's assuming.
If they are longs its still not right

Yes, it does, because it does the division and compares every step.
Also there are forgotten updates when the time ticks further than an exact multiple of the INTERVAL sometimes and you just set last_time=current_time instead of adding

But that's not what the code is doing. It adds for each step, and if time elapsed more than one step, it will step faster until it catches up.

I'm all for pointing out flaws in code snippets, so that other readers can be aware. However, so far, I'm afraid you're batting pretty poorly :-)
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement