Error Prediction Ball

Hello Everyone,

We are making a multiplayer pong game. And we have stabilised our game with an old version of Netcode (0.2).
We decide to migrate to Netcode 0.51 and we got some regression on the ball synchronisation…

Because of the simplicity of collisions (Ball collide with wall and players), we implement our own system of collision.
In order to have smooth collision between ball and players. Each player needs to predict the ball.

We succeed to do it with the following configuration…

Here is our settings in Netcode (0.2)


We tried to configured the same way with Netcode 0.51 but he keep having jittering collision between ball and player.

Here is our new configuration :

How should we configure the ball synchronisation to have smooth collision between Player and ball, in order to be predicted by all the players ?

Thanks in advance,
Ali

please help ! @CMarastoni @timjohansson

The setup you posted would make the ball predicted by all clients. It would probably be better in this case to set “Supported Ghost Modes” to predicted instead of default ghost mode, and OwnerPredictedSendType on the GhostComponent attribute does not have any effect since the ghost is not owner predicted so the server doesn’t know if the client is predicting or interpolating it (you can change the default ghost mode when receiving a ghost on the client).
Neither of those would change the functionality though, so it will not fix your problem.

The only thing that sounds a bit off with the ghost setup is that you would need to predict the other players pad in order to avoid prediction errors when the ball hits the other player pad - but that was not possible in 0.2 so should not be a regression.

This sounds more like a problem in the prediction system - for prediction errors like this the most common issue is some data used in the prediction logic not being rolled back, so you should double check which data gets ghosted in the old vs new version and that you are not relying on some data which is not ghosted. I think there is also some optimizations which makes it more important to correctly check “shouldPredict” in the prediction systems so double check that you are doing that correctly too.

There is a prediction error display in NetDebug (Multiplayer > NetDbg in the menu) which might help point you to which component is mis-predicted.

Thank you for your help !

I followed your recommendation and i am feeling that in Netcode 0.51. There is a kind of regression regarding the Prediction System, the ball is kind of lagging. It is behaving correctly but always with little delay that is parasiting the user experience.

I am feeling that the Client Tick and the Server Tick is not well synchronized…
Do you have any clue ? or thing i should investigate or do, to fix that ?

For my understanding, the client tick is always in front of the server tick. So if the client is processing the current tick “t” then keep going to process “t+1”. While the server is processing the tick “t” and send the result to the client.

When the client received server’s snapshot, the client will rollback the tick “t” received and play the simulation until the last client tick processed to verified that prediction is correct.

When i test locally, Server and Client on the same computer, and see the logs… I saw server processing ticks always before the client.
For my understanding, it is not the correct behaviour… Please correct if i am wrong.

Plus, when i use the NetDbg, i see any errors in prediction…

Please Help @timjohansson and @CMarastoni

the client is always ahead of the server of about 2 ticks (in 0.51). The client connection also use socket, so there is really no guaratee when the OS is going to deliver the next packet.
In 1.0-exp the client is again ahead of the server BUT if the simulator is disabled, the IPC connection is used instead and the client always run the next tick (plus an extra partial partial tick).

The server should run before the client because this is the actually correct loop. The server send the snapshot, that ideally is used by the client to simulate/predict the next frame for the current tick (that is in sync) and it will then the next frame receive what the server computed, rollback (1tick) and go.

So you have no idea why i have this lag ? (everything is predict correctly but we have a little delay before it is applied…)

I’m not saying I don’t have an idea. What I’m saying is that we do have predicted entities as well and the input is correctly applied without lagging.
The input of course must be polled and added to buffer inside the GhostInputSystem group (otherwise we cannot guarantee absence of lagging).
You said everything is predicted correctly. Well, if the is the case I’m expecting the position of the object is correctly reflected (as you said) on the LocalToWorld matrix. And that is what it is show on the screen.
There can be different reason for any problem we see, but looks like to me the visualisation is lagging behind from what you say,

First thank you for your time,
But i have a hard time to understand what you are saying :
Literally you said that “Client is ahead of the server” and then said the “Server should run before the client”…

Can you please clarify this point what is the proper workflow if everything is IN SYNC ?
Which workflow don’t and so reflect bad use of the prediction system ?

The client will re-run multiple ticks every frame, so the expected output if you log current tick (UnityEngine.Debug.Log($"{(World.IsServer()?"Server":"Client")} Predict tick {GetSingleton<NetworkTime>().ServerTick.TickIndexForValidTick}")) in the prediction system would be something like this (tick numbers are made up and not the exact number of client ticks / server tick you are likely to see, sometimes client will only run once with a high tick index, sometimes multiple client frames will start or end with the same tick index as previous client frame, server will not run every frame etc)

Server Predict tick 10
Client Predict tick 9
Client Predict tick 10
Client Predict tick 11
Server Predict tick 11
Client Predict tick 10
Client Predict tick 11
Client Predict tick 12

So the client will generally start with a lower tick that the server and end with a higher tick than the server.

Let me give you an example where we have a weird problem :

We are making a multiplayer pong.
Expected Behaviour : Every 2 minutes we want the ball to be repositionne in the middle of the Arena (0,0,0) whatever his position. (without any blinking effect)
Current Behaviour : Every 2 minutes, the ball is blinking in the middle of the Arena before being definitely repositioned in (0,0,0).

When i check the logs with the tick : The server is moving the ball in the middle of the arena in a tick 100 while the Client is doing the same operation in tick 103. which results in this blinking state before the server force the final position.

My current implementation :
In my pong game i have a “LauncherBallSystem” that is in ServerSimulationGroup which waits 2 minutes before incrementing a variable “spawning_counter”.
The ball is a GhostObject in a Predicted State (because we want each client to be able to predict it).

Inside GhostPredictionSimulationGroup, we have a SpawnerSystem that check the variable “spawning_counter”.
If it is increased, the Ball is repositionne in the middle of the arena…

The variable “spawning_counter” is a GhostField that belongs to the Ball (GhostObject).

My thoughts : is that the server and the client should run (= reposition the ball) at the same tick the SpawnerSystem (which belongs to GhostPredictionSimulationGroup) to avoid any gap…

  1. What do you suggest to fix this blinking effect ?
  2. Did i miss understand something in how the tick should work ?

Thanks in advance

@timjohansson @CMarastoni

Ok thank you for your clarification ! so how do you fix a situation where you want to run only one time ? (can you please check my reply just above this one…)

This is what I would expect. The timer which updates the spawning_counter is server only, so the client will not know that the value changed until a few frames after the server - when it receives the real value from the server.
You could make the LauncherBallSystem also run in the prediction loop and make sure the timer is synchronized through a ghost (create a LauncherTimeSingleton with a [GhostField] float remainingTime or something like that.

How are you checking that the counter is increased? If you do something like if (spawning_counter > local_value) where local_value is not a ghost field you will get something like the behavior you describe. You would update the local_value the first time the prediction loop run. Next frame everything in the ball would be rolled back, but the local_value would not be rolled back so the check would not be hit after a rollback and the ball would move back to its old position.
In order to fix it, make sure whatever you use to check if the value increased is also rolled back (easiest by making it a ghost field).

Another thing to consider in this case is if you really need to predict the move back to center or if having specifically the teleport server authoritative. If you only perform the move back to 0,0 on the server the only difference will be that client will have a slight delay on resetting it when the timer hits - if that is ok you can save a lot of complexity

Thank you for your precise explication.

So what you said basically it is that you cannot have a part of a logic in Prediction Group that depends on another part of logic in Server side (which i did and explained above) because the rollback will be compromise…

Either you put everything inside prediction loop and sync all conditions variables by using GhostField (like the local counter) or either put all the logic in Server side and accepte the slight delay that will generate…

Both of those solutions will not trigger “the blinking effect” that i was describing before.

Am I right ?

@timjohansson , now i have another question to help me to understand better :

Here is another problem where i have a weird behaviour :

We are making a multiplayer pong game.
Expected Behaviour : The player can trigger a shockwave, which push back all the balls, smoothly.
Current Behaviour : The player triggers a shockwave, the ball is either doing a small Z movement before taking the right direction or do a teleport to the right direction… Either way the ball does not have a smooth transition.

My current implementation :
When the player is pressing the shockwave button, it does 2 operations :

1st Operation : it checks the ball position and see if they are affected by shockwave. If it is so, it generates the new direction of the ball, its velocity and its current position from where it should move from.

2nd Operation : it fills the PlayerInput Buffer with the following informations:
→ Tick (Obviously)
→ counter_shockwave (to trigger the event)
→ ball position, velocity and new direction (all those values has been generated from the first operation (above)).

All those operations are running under a ShockwaveInputSystem that is updated in the GhostInputSystemGroup.
The ball is a GhostObject in a Predicted State (because we want each client to be able to predict it).

Inside the GhostPredictionSystemGroup, we have a ShockwaveSystem that pulls all the data (with the Predicting tick) from the PlayerInput Buffer and applies it directly on the corresponding ball(s)…

The ShockwaveSystem also have a local variable counter_shockwave to compare with the counter_shockwave (from PlayerInput Buffer)…

Why do i have this weird transition when i apply a shockwave ? How can i fix it ?

Thanks in advance

Plus if want to synchronize the time in GhostPredictionGroup :

We have a variable Time which is a GhostField.
And then inside a System under GhostPredictionGroup, we do the following line :

Time -= UnityEngine.Time.delta;

Is that Correct ?

Thanks in advance

Correct.

If you are comparing the input to a local variable it sounds like the same problem you had with spawn_counter. When you roll back and re-simulate you will not compare against the correct value and the shockwave will not trigger the second time you predict it. For inputs you can read the inputs for tick-1 and compare the counter to that instead (you should also check that it is not a repeat by no triggering if both have the same tick). In 1.0 we have a InputEvent struct which handles this for you.

That is not correct, you need to use Time.DeltaTime from the system or World instead of UnityEngine. If not you are not using the time for the currently predicted simulation step, you are using the time for the current frame - which is not correct.