Advertisement

Running both server and client on one PC causes lag despite ping < 1 ms.

Started by October 09, 2016 12:53 PM
26 comments, last by Heelp 8 years, 1 month ago

samoth, thanks for clearing this up for me. Basically, you are right, IMMEDIATE_PRIORITY sends things right away and doesn't wait for 10ms, it doesn't wait at all. And UNRELIABLE_SEQUENCED is the best one for LAN play, because it doesn't care about lost packets, so if I receive packets in order 5,1,2,6, I just get 5 and 6.

So you are right, I haven't read that.

EDIT:

We still don't know how your apps decide when and how often to call the receive function and that will affect things too.

I was wondering why are you talking about that receiving so much. And I spotted my mistake. Actually this is why my timestamping was totally wrong. Because timestamps actually tell when you RECEIVED the packet, not when it was READY to be received.

And I do all the sending and receiving only when I update the simulation, which is every 50 ms. That's why the timestamping was either 25ms, 24ms, 26ms, 27ms, or 48ms, 2ms, 1ms, 47ms, 48ms, because for example if I start the server app at time = 0 ms, and the client app at time = 25ms, and they all do sending and receiving every 50 ms, this means that even if the packet needs 5ms to arrive, I will still wait another 20 ms until time = 25ms because I haven't called packet->receive()..... :angry: :angry: :angry: :angry: :angry:


    while( myApp.myInputManager.isGameRunning() )
    {
        double current = double( SDL_GetTicks() );
        double elapsed = current - previous;
        previous = current;
        lag += elapsed;

        myApp.myInputManager.listenForAllEvents();

        while( lag >= MS_PER_UPDATE )
        {
            myApp.currentState->doServerNetworking(); //Here is where sending and receiving happens. I need to do it outside the loop.
            myApp.currentState->updateLogic( myApp.myInputManager );
            lag -= MS_PER_UPDATE;
        }

        myApp.currentState->renderScene( lag / MS_PER_UPDATE );
    }

It's really easy to just move the networking out of the inner while-loop, but then even if the server receives the client position earlier, it still has to wait until the simulation is updated, otherwise it doesn't know what position to send back.

EDIT 12093219: Guys, the packets are being sent and received instantly,( 2-3 ms). I just tried at 25 ms and it's working. The problem is that it works 1 out of 10 times. I need to time perfectly when I press 'enter' and start the client app, this way the client and server update loops happen at different times and it works.

But can you propose some other way of synchronizing, other than restarting the client app until it stops to lag?

Uh, wait, wait, wait... This is not at all how you do it correctly.

Try rather something like:


while(running)
{
    while((packet = peer.Receive()) != 0)
        handle(packet);

    if(have_user_command())
        send_command();

    render();
}

Never receive something based on some timer, in particular not a low-resolution timer from which you add up small increments expecting them to be accurate. They are not, but even if they were accurate, you do not want to delay. Why would you?

Whenever there is something to be received (technically it has already been received, you're just pulling it from the queue), by all means, do receive it. Now, not later. You have nothing to gain, and everything to lose from waiting.

Normally, one calls a function like select or poll for that matter, or uses epoll / completion port / kqueue, or whatever (on a server anyway, the client probably has only one socket anyway, so you can as well just read it, as long as you're setting the nonblocking flags... saves one system call).

Now, RakNet doesn't expose that functionality (it probably uses one of the above functions internally to communicate with the network stack, though). Instead, you can call Receive(), which either returns a packet or a null pointer. That's OK, this is actually just what you want.

As long as there is at least one packet available, you will get it (usually your game loop runs much faster, so there will be either zero packets, or exactly one, but sometimes there might be more). Once you get back a null pointer, you know there is nothing interesting available, therefore, there is no point in wasting CPU time polling any further ---> go on.

The same goes for sending. If you have something to send (e.g. user pressed a button) there is usually no good reason to hold it back for some milliseconds. That's the opposite of "low latency". Do it right away, not after some timer is above some threshold. RakNet will do the actual send asynchronously anyway.

Advertisement

samoth, yes, but when the server receives a packet early, it still needs to wait for the inner while-loop to update the logic, because it has to update all clients' positions in order to send back the new one, right? And then when the server sends back the new position to the client, even if the client receives the position earlier, he still needs to wait for his update loop in order to execute it.

I know there's a hole in my logic somewhere, but I can't come up with the solution.

Your problem is that you're treating "reading something from the network" and "running game logic based on that incoming data" as the same operation. The first bit, you want to do as quickly as possible, as often as possible. The second part, you perform at a fixed rate alongside the rest of your logic. You probably need a queue of messages in your application where they wait after being pulled off the network, and before being processed in your game logic.

But as you realised above, if you only send responses back as part of your game logic step, and your game logic happens every 50ms, then your typical response latency will be anything from 0ms to 50ms. That's essentially unavoidable and is a big part of why you can't skip client prediction even for a LAN game.


Guys, the packets are being sent and received instantly,( 2-3 ms). I just tried at 25 ms and it's working. The problem is that it works 1 out of 10 times. I need to time perfectly when I press 'enter' and start the client app, this way the client and server update loops happen at different times and it works.

You're going to have to explain what this all means, because it doesn't make sense to me.

a) What do you mean 'it works', and what are you seeing the other 9/10 times?

b) Why do you think it matters how you time the app startup?

To be clear, you need to give us the usual 3 pieces of key debugging information:

1) What you are doing;

2) What you expected to happen;

3) What you actually observed (and how it differs from what you expected).

To be clear, you need to give us the usual 3 pieces of key debugging information: 1) What you are doing; 2) What you expected to happen; 3) What you actually observed (and how it differs from what you expected).

Ok, I press enter and start the server.exe at time == 0ms( not server time, just real time, out of computers).

Then I press enter and start the client.exe at time == 25ms. This means that the server will update its logic at time == 0ms, 50ms, 100ms, 150ms and so on. And the client updates at 25ms 75ms, 125ms and on and on.

Now, in this case, there is no lag at all at the client program. I can play perfectly fine. printing timestamps on cmd shows 25ms,24ms,26ms,26ms,23ms,25ms, and so on.

But if I had started the client.exe not at 25ms, but at 99ms after the big bang, then the client's first update happens at 99ms, and the server's 3rd update happens only 1ms later, at 100ms.

Now that the server and client's inner while-loops execute in a really small time span, it will lag from time to time. By 'lag' I mean that the client, instead of receiving one packet per update, may receive 0 packets in a given update, and then 2 packets at the next update, then 1,1, 2, 0, 1, 1, 1, 1, 0, 2 and so on. This causes lag.

Why the hell does this happen, you ask? I haven't the slightest idea....it's magic.

Nevermind, printing timestamps on cmd shows 48ms, 1ms, 49ms, 0ms, and so on and so on. Yes, sometimes it's indeed 0ms on timestamps. This means that actually the packets are sometimes sent and received in less than 1ms!!( thanks to IMMEDIATE_PRIORITY flag ).

But it lags. That's why it depends on what time I start the client...... Go figure it. :wacko:

EDIT: I came up with a very funny way to fix it. I will tune the client's timer with F1( decrease time) and F2(increase time), this way when a client starts his app, he can fine-tune it until his update loop is far away in time from the server's update loop. hahahaha :lol:.

Well, sure, this is exactly what we said above. It's the equivalent of putting the mail in the mailbox 5 minutes before the postman is coming to collect it, compared to putting the mail in the box 5 minutes after he came to collect it. There's only 10 minutes difference in posting time but the item posted 10 minutes later actually arrives a whole day later.

As for why sometimes you get 2 messages per update and sometimes 0, that's because sometimes there is a bit of variance in performance - maybe something else was using the network, maybe another program is running, maybe the operating system had some work to do, etc. At the millisecond level you have to expect some variation, and therefore you mustn't expect to always get one message per client in each tiny time-slot. Delivery time will always vary and you have to write your program to deal with that.

Advertisement

I need to time perfectly when I press 'enter' and start the client app


Your "what time is it" (read the clock) function should include an "adjustment" parameter.
The clock you return to the game should be SystemClock() + Offset.
Then, your server should send "the time is now X" data in each packet it sends to the clients.
The clients can then compare the timestamp they get from the server, to the timestamp they have locally, and adjust the Offset to make them line up.
enum Bool { True, False, FileNotFound };
Your "what time is it" (read the clock) function should include an "adjustment" parameter. The clock you return to the game should be SystemClock() + Offset. Then, your server should send "the time is now X" data in each packet it sends to the clients. The clients can then compare the timestamp they get from the server, to the timestamp they have locally, and adjust the Offset to make them line up.

hplus, thanks for the solution. Works like a charm. :cool:

For now I adjust every client's timer only once, when accepting connection. Seems enough for now. I hope there aren't any floating point errors that can accumulate and mess up my timers with time.

Guys, thanks to all for the big help, I kind of misleaded you because I couldn't explain the problem properly, but I'm glad it works now.

I hope there aren't any floating point errors that can accumulate and mess up my timers with time.


If you use "float" then, yes, after hours of uptime, you may accumulate drift.
If you use "double," it will happen after years of uptime, which probalby is an OK trade-off.
An alternative is to count time in quanta (either "microseconds" or "simulation ticks" or whatever) and use an int32 or int64.

Separately: It's more likely that the rate of time progress on your PC will be slightly different from the rate of time progress on the server.
Thus, it is good to include timing information in each packet, and if you find that the PC is "too different" from the server, adjust the PC.
(This also happens when network conditions change and latency increases/decreases.)
Typically, you will never allow the PC to be ahead of the server, so if it is, immediately adjust the clock backward to match.
If the PC is too far behind the server, adjust the clock forwards by 1/10th of the delta. That way, it will be reasonably smooth when catching up, without making a large jump.
enum Bool { True, False, FileNotFound };
I hope there aren't any floating point errors that can accumulate and mess up my timers with time

No worries there. Floating point is perfectly accurate, with no precision issues -- none that matter to you, anyway. (Read as: You have different things to worry about)

As stated earlier, using SDL's tick count function is not the most precise or predictable/reliable thing in the world. Depending on how SDL was configured, you get one of these three:

  • the result of GetTickCount
  • the result of timeGetTime where timeBeginPeriod(1) has been called at initialization time
  • the result of QueryPerformanceCounter

GetTickCount will always run at 15.6ms precision, regardless of what scheduler granularity you set, timeGetTime will run at 1ms precision, but this costs you about 5% overall in performance due to 15x more frequent context switches and scheduler runs. QueryPerformanceCounter will, in theory, be much more accurate, but in practice SDL internally multiplies and truncates the value so it has a 1ms resolution (it is admittedly much more close to 1ms precision than timeGetTime, though).

Therefore, whatever small time deltas you add up, you will not have a precise (or usable) result. Floating point is the least of your concerns here. Adding up timers that way is kinda bound to fail, simply because 10ms and 10.75ms are exactly the same, and depending on what timer you have, 10ms and 15ms are exactly the same, too.

Note that under POSIX, SDL uses gettimeofday, which is somewhat better and more reliable. Microsecond resolution and precision on reasonably recent mainstream hardware.

This topic is closed to new replies.

Advertisement