Make server movement independent from package rate / latency fluctuation

Started by
16 comments, last by Geri 1 year, 2 months ago

Hi, I've faced an issue while working on a multiplayer client-server model that I can't solve, the simplified scheme is as follows:

Client produces and sends input
Server receives input, moves, and returns state to client
Client updates its state

This, as always, works fine on local network, but as soon as the latency starts going up and down, or the packet rate changes, the movement stops being smooth, due to the fact that now packets aren't arriving perfectly spaced at 30hz to give and example, so server might get a packet now, move, and then not get packets for 123ms, and then get a packet again and move, this will translate to choppiness on the client side. One solution I thought about was creating a buffer on the server side but this will only add more latency (I already have a buffer client side for entity interpolation), how is this problem usually solved?

None

Advertisement

You need “client-side prediction".

The idea is to let the client simulate the game world for some time, but when the client receives the message from the server's update, it replaces its state with what the server sent.

Each side, at each timestep:

  • Client records all inputs to a buffer B: B.append Inputs[t_client]
  • The client *may send the input sequence to the server (what was inputted since the last sent update to the server):
    • send_to_server ( client_msg_time = t_client, inputs = B )
    • *May send, as it's not necessary to synchronize states each frame; you can do it once a few several timesteps

When the server receives this message:

  • The server now will be at moment t_server (client_msg_time < t_server ).
  • He has to ‘backtrack’ the state to the moment client_msg_time, set input sequence to inputs, and then simulate the world to the ‘current’ server's time (t_server)
    • Since the server has authority, all impossible inputs or state discrepancies are discarded
  • The server responds with the new state (or just updates):
    • send_to_client ( server_msg_time = t_server, state = server_state)

When the client receives the server's message (in the future)

  • Client will be at a timestep t_client( client_msg_time < server_msg_time < t_client)
  • Again, he has to ‘backtrack’ the state to the moment t_server, (which happened ‘after’ the previous message to the server but before the ‘current’ moment, t)
  • and again simulate the input sequence to the moment t_client
    • Again, as the server has authority, all discrepancies between the server and client are overwritten by the received server state

The scheme is quite complex. and ‘faithful’ implementation requires identical implementation for both client and server, and to properly ‘merge’ states of server and clients. There are a lot of small tricks to improve the user experience; for example:

  • Instead of setting client object states to the received server values, you may interpolate between client position and server position for a few frames (Rocket League uses this AFAIR)
  • If there is some complex server-side logic that cannot be replicated on the client, and latency requirements are constricting, it makes sense to estimate ping, and instead of sending each other ‘current’ state, one sends the ‘future’ state (t_server+ping). This doesn't save from latency spikes, though.

But, realistically, in most cases, you can get away with approximating what the server will respond to on the client.

Feel free to ask questions! I can draw a diagram; it might be hard to understand the algorithm from this description.

None

@razcodes Client side prediction is important, but it sounds like an even more fundamental bit needs to change: The client/server operations cannot be viewed as “requests” and cannot be synchronous.

The game loop (both client, and server) should be something like:

while (game is running) {
  nowtime = now()
  virtualtime = nowtime - starttime
  if (has game controls) {
    decode control events
  }
  if (is client) {
    queue control events to server
    dequeue object events from server
  }
  if (is server) {
    dequeue control events from clients
  }
  while (virtualtime - prev game time >= tick size) {
      simulate one physics frame
      prev game time += tick size
  }
  if (has display) {
    render one frame
  }
  if (is server) {
    queue object updates to clients
  }
}

Note that the client just keeps reading and simulating; it doesn't “wait” for anything. Similarly on the server. Those are both processes that just keep running their loops, and where “input” happens to arrive from a server, or from a gamepad, or whatnot.

Client prediction, or server prediction, or lock-step simulation, are all slight variations on this basic theme.

enum Bool { True, False, FileNotFound };

@rampeer I already have Client Side Prediction! and Entity Interpolation. Works just fine, the local players feels smooth as butter no matter the latency, the problem would be the "others clients" (not the local player). Your reply was extremely detailed tho and I will definitively improve my implementation, thank you!

If a client has jitter, the server will do something like "move, stop, move move move, stop, stop, move" instead of a continuous move and that will be visible when the server returns a game snapshot.

I recorded a video so you can see what I'm talking about, the windows are: Client - Server - Client and I'm moving the client on the right window. As you will see, the local movement is smooth even tho I'm simulating 100ms with 20ms jitter (thanks to CSP), but the server view/other clients view is jittery.. Also this is not due to lack of entity interpolation, it is implemented and working fine, the problem is that the actual packets received from the server include this “jitter”… I believe this has more to do with the way I'm handling inputs and game updates as @hplus0603 says.

The way i'm handling inputs right now, the client send inputs every 30hz, and they get stored in an array on the server side, each tick the server will process all pending inputs in a for loop, and return the game snapshot… The problem here is that if the client has jitter, sometimes the server doesn't have any inputs to move, and all those inputs will arrive the next tick, producing double the movement. Are there any good resources on how to handle input for multiplayer games?

None

Video was a good illustration of the issue. I have a hunch that you just have a bug; like entity velocity being received by the server (center, so movement is smooth) from the first client (right one), but not retranslated to the second (left one)

None

Well, there is no velocity, the inputs just say “move X units in this direction” hahaha. I needed to simplify the code in order to solve the problem, the client on the left just gets a game snapshot that says “client A at (X,Y), client B at (X,Y)”

None

Huh, send velocities then! If you only use snapshots (=just coordinates), the movement will be jittery, and there is nothing you can do about it. Latency randomness means these static coordinates arrive to the second client at random intervals; with no mechanism to fill in ‘gaps’ when no data is received, you are doomed to get the jitter. BTW, just rewatched your video at 0.25 speed - at server has some jitter too. It's not as bad, but there is some stutter; it's just gets amplified when relayed to the second client.

Or, if you want a ‘dirt-cheap’ solution: at each client (for each externally controlled entity, i.e. ‘non-local’ player), keep track of two messages: last received from the server (time f(t1) and coordinates (X1,Y1,Z1)); and message before that (t0, X0, Y0, Z0). So, ‘last’ and ‘before last’ messages. At each frame, interpolate each coordinate of each entity: X(t) = X1 + (X1-X0) * (t-t1) / (t1-t0). It essentially ‘recovers’ velocities of each coordinates from a pair of messages; so no need to explicitly send them.

You cannot get smooth movement without some sort of interpolation, that runs each frame and somehow compensates that lack of the server messages. Lag is random; you cannot expect to receive evenly-spaced packets at 30HZ when server sents them at that rate. The packets will arrive when they wish to (I guess you expected them to arrive steadily). They may even arrive out-of order! (if you use UDP)

None

Neither interpolation nor packets out of order are the problem, I am interpolating between states, just not using the velocity. Look at this example:

Tick 0: Server receives movement package. Player at Z=1

Tick 1: Server receives movement package. Player at Z=2

Tick 2: Server doesn't get anything Z=2

Tick 3: Server gets 2 movement packages. Player at Z=4

Every tick the server sent back a state, even if you are interpolating, even if you are sending speeds, the player will experience a jitter on tick 2, not because it didn't have information for the interpolation, because it HAD the information, and it said that the player was still, when in reality it should have moved.

Again, I believe this is a problem on handling input, as I have greatly improved the jitter by buffering client inputs server side, this way every tick I have information to apply, but still I don't fully understand the proper way of doing it.

None

I am interpolating between states

Ah. You are right; interpolation won't solve the problem. Above, I meant extrapolation (as you are projecting data beyound anchor points), which is a slighly different concept.

In the case you provided, a party that did not receive expected packet (Tick #2) can guess its contents based on data from previous ticks. Using some clever tricks, velocity data, or data from several previous frames; it doesn't matter; an educated guess has to be made. Otherwise, the player will experience stutter at Tick #2, as the object will suddenly stop moving at Tick #1, and then jump two distance units at Tick #3. No input processing can save it: the second client didn't receive first client's inputs, so nothing to process.

(by the way, “input buffering”, if it's “keeping button pressed state the same when there is no new packets”, then it's conceptually, similar to sending velocity. That's why it helped: it gave machine some information on how to behave during this inter-message frames.
Also, are you sending one player's inputs to another player?..)

razcodes said:
it HAD the information, and it said that the player was still

… but if you had sent the velocity (or buffered inputs), then the system would know that the player wasn't still, isn't it?

I don't quite get why you tie the issue to the input processing. If there is lag, then remote machine will receive our data (inputs) at irregular intervals. Sometimes, the data won't arrive in time, and it our inputs won't be available for the remote client at the instant it needed. The information is not available, physically. Some compensatory guesswork must be made.

For me, it seems like a plausible root cause, and a solution for it. You seem to disagree - well, yeah, I must be missing something. I do not understand the problem, and I percieve some statements as contradicting each other / just a bit strange (why server has jitter too, as I mentioned above?.. why do you have this problem at all with client-side prediction, it's designed to solve this exact problem?..).

I guess, a description of how existing mechanisms work, would be helpful ?

None

I'm sorry, I will try my best to explain the current logic of the implementation I have, with all the details, so you can tell me if I'm doing anything wrong.

Client logic, main loop:

// Called every 30hz
void OnUpdate()
{
	var input = GetCurrentInput(); // Returns a struct with things like: 'w pressed', 'space pressed' etc
	SendInput(input, _seqNumber); // Sends the input to the server
	MoveLocally(input); // Moves the player locally, for Client Side Prediction
	_seqNumber++;
}

Client logic, when a package comes back from the server:

void OnPackageReceived(Snapshot snapshot) 
{
	_snapshotInterpolator.PushSnapshot(snapshot); // This will interpolate the entities in the world, except for the local player
	Reconciliate(snapshot); // Will check if the game snapshot returned and the predicted state match
}

This is all there is for the client, I know that the Client Side Prediction and Entity Interpolation work just fine.

Server logic, main loop:

void OnUpdate() // Called every 30hz
{
	// Move all players and clear the queue
	foreach (Input inputReceived in _inputsQueue)
	{
		var player = GetPlayer(inputReceived.PlayerId);
		player.Move(inputReceived);
		player.LastInputProcessed = inputReceived.SeqNumber;
	}
	
	_inputsQueue.Clear();
	
	// Now that all players have been moved, return the current game state to clients
	var snapshot = new Snapshot
	{
		Time = TicksMilliseconds(),
		PlayerStates = GetPlayerStates() // Returns an array of players and their positions, as well as the last seqstamp processed
	};
	
	SerializeAndBroadcast(snapshot);
}

Server logic, on input received:

void OnInputReceived(Input input)
{
	_inputsQueue.Enqueue(input);
}

That is all the logic, I believe the problem is in the fact that on the server loop, if a packets gets delayed, the loop will not update the player position, but it will still send a snapshot, saying that the client didn't move on that tick, when that snapshot gets back to the client, the client will interpolate correctly to it, and stop, (because that is actually what the client can see) but on the next server tick, the player will move again, since the delayed packet finally got to the server, and in that snapshot you will see a movement again. So from a clients perspective, the entities will go on for a few ticks, then stop, then go on again, then stop, etc, etc. But again, I believe this is not an interpolation problem, since the actual snapshots received by the client tell that the entity has stopped, it is supposed to stop. There is no velocity involved yet in the game and I don't want to include it, I want to keep this as simple as possible.

Also, the local player feels just fine! Thanks to CSP, no problem there.

One way I managed to mitigate the problem, (actually it solved it, but I know I'm not implementing it right) is to have a buffer on the server, every time an input gets to the server the server stores it in a buffer and waits till it was 2 / 3, this way, when the main loop ticks, the foreach that applies movements will always have some input to work with, and the jittering disappears, still this adds latency and I'm not really sure how to do it right. Thank you very much for helping again!

None

This topic is closed to new replies.

Advertisement