Search Unity

Question Change Ghost position with Predicted ghost mode

Discussion in 'NetCode for ECS' started by Ali_Bakkal, Sep 8, 2021.

  1. Ali_Bakkal

    Ali_Bakkal

    Joined:
    Jan 26, 2013
    Posts:
    90
    Hello Everyone,

    We are making a multiplayer pong game using Unity Netcode.
    The ball is synchronized across all game clients.

    The expected outcome is :
    • When we press the mega shoot button on the client machine, we want to change the direction of the ball instantly without any weird jumps or jitterings.
    All our tests are made through localhost (so no latency from the network is involved) with powerful computers.

    What we tried :
    1) First Test :

    We set the GhostAuthoringComponent of the ball to Interpolation
    => the ball was moving smoothly but when we pressed the mega shoot button, there was a latency before the ball changes its direction (this outcome ruins player experience).

    Conclusion of the first test:
    The ball belongs to the server but it needs to be predicted by all clients, in order to have a good player experience.

    Basically all client needs to change the ball position without waiting for the server’s response.

    2) Second test:
    So we changed the GhostAuthoringComponent of the ball to Predicted
    => When we pressed the mega shoot button, the ball was changing immediately its position but during the transition, the ball was jumping back and forth before taking the correct direction. The ball is less smoother than interpolation.

    Here is the observed behaviour:



    And here is the code that change position of the ball inside GhostPredictionGroup:

    Code (CSharp):
    1. [BurstCompile]
    2. [RequireComponentTag(typeof(BallTagComponent))]
    3. struct ShootBallJob : IJobForEachWithEntity<Translation, PhysicsVelocity, PredictedGhostComponent>
    4. {
    5.     public uint currentTick;
    6.     public float ball_positionX;
    7.     public float ball_positionZ;
    8.     public float ball_directionX;
    9.     public float ball_directionZ;
    10.  
    11.     public void Execute(Entity entity, int index, ref Translation position, ref PhysicsVelocity velocity, [ReadOnly] ref PredictedGhostComponent prediction)
    12.     {
    13.         if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
    14.             return;
    15.  
    16.         position.Value.x = ball_positionX;
    17.         position.Value.z = ball_positionZ;
    18.  
    19.         velocity.Linear.x = ball_directionX * 2;
    20.         velocity.Linear.z = ball_directionZ * 2;
    21.     }
    22. }
    Any help on how to remove this jittering is welcome!

    Thanks in advance for your help

    @timjohansson @CMarastoni
     
  2. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    How are you setting the public floats on the system for position and direction? Those needs to be updated every prediciton tick to the pos / vel you are supposed to have at that tick, if you leave them at the most recent value your prediction logic will be off since not all inputs to the system are rolled back. That would explain at least some of the results you are seeing.

    Generally speaking you should only use state stored on the entity and serialized + command data from the predicted tick as inputs to your prediction update logic since that is how we make sure it is rolled back to the correct state.
    You can get away with using some cached values and feeding values in from other systems which are also predicted, but you need to be extremely careful with how you handle that input state and know exactly how it is rolled back
     
  3. Ali_Bakkal

    Ali_Bakkal

    Joined:
    Jan 26, 2013
    Posts:
    90
    Thanks for your quick answer!

    We are setting the ball’s position and direction by running this logic in ClientSimulationSystemGroup:
    We calculate the direction and the position to apply to the ball, then we fill in the input buffer with those infos associated with the tick.

    Here is the corresponding code :
    Code (CSharp):
    1. [BurstCompile]
    2. [RequireComponentTag(typeof(BallTagComponent))]
    3.     struct ShockwaveJob : IJobForEachWithEntity<Translation, BallAuthoringComponent, PhysicsVelocity>
    4.     {
    5.         public float shockwave_maxRadius;
    6.         public float3 positionPlayer;
    7.         public NativeArray<ResultShockwaveDataComponent> result;
    8.  
    9.         public void Execute(Entity entity, int index, [ReadOnly] ref Translation position, ref BallAuthoringComponent ballAuthoringComponent, ref PhysicsVelocity velocity)
    10.         {
    11.             float3 positionBall = position.Value;
    12.             float3 _direction = positionBall - this.positionPlayer;
    13.             float sqrMagnitude = (_direction.x * _direction.x) + (_direction.z * _direction.z);
    14.             float _distance = math.sqrt(sqrMagnitude);//_direction.magnitude;
    15.  
    16.             if (_distance <= this.shockwave_maxRadius)
    17.             {
    18.                 ResultShockwaveDataComponent resultShockwaveDataComponent = new ResultShockwaveDataComponent();
    19.                 resultShockwaveDataComponent.ball_positionX = positionBall.x;
    20.                 resultShockwaveDataComponent.ball_positionZ = positionBall.z;
    21.                 resultShockwaveDataComponent.ball_directionX = _direction.x;
    22.                 resultShockwaveDataComponent.ball_directionZ = _direction.z;
    23.                 result[index] = resultShockwaveDataComponent;
    24.             }
    25.         }
    26.     }
    Then after we get this position and direction in a system that runs inside the GhostPredicionSystemGroup with the following code:
    Code (CSharp):
    1. [BurstCompile]
    2.     [RequireComponentTag(typeof(PlayerTagComponent), typeof(PlayerInput))]
    3.     struct GetPlayerCommandJob : IJobForEachWithEntity<Translation, PlayerAuthoringComponent, PredictedGhostComponent>
    4.     {
    5.         public uint currentTick;
    6.         [ReadOnly] public BufferFromEntity<PlayerInput> inputFromEntity;
    7.         public NativeArray<InputDataComponent> result;
    8.  
    9.         public void Execute(Entity entity, int index, [ReadOnly] ref Translation position, [ReadOnly] ref PlayerAuthoringComponent playerAuthoringComponent, [ReadOnly] ref PredictedGhostComponent prediction)
    10.         {
    11.             if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
    12.                 return;
    13.  
    14.             var input = inputFromEntity[entity];
    15.             PlayerInput inputData;
    16.             input.GetDataAtTick(currentTick, out inputData);
    17.            
    18.             PlayerCommand playerCommand = (PlayerCommand)inputData.playerCommand;
    19.             if (playerCommand != PlayerCommand.Shockwave)
    20.                 return;
    21.  
    22.             InputDataComponent inputDataComponent = new InputDataComponent();
    23.             inputDataComponent.playerCommand = inputData.playerCommand;
    24.             inputDataComponent.ball_positionX = inputData.positionX;
    25.             inputDataComponent.ball_positionZ = inputData.positionZ;
    26.             inputDataComponent.ball_directionX = inputData.directionX;
    27.             inputDataComponent.ball_directionZ = inputData.directionZ;
    28.             result[index] = inputDataComponent;
    29.         }
    30.     }
    Finally we apply the position and the direction inside the job we put in our first post.

    Does it help you to better understand?
    What idea can we implement to fix the glitch?

    Thanks in advance.
    @timjohansson @CMarastoni
     
  4. Ali_Bakkal

    Ali_Bakkal

    Joined:
    Jan 26, 2013
    Posts:
    90
  5. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    Looking at the code again now, I'm pretty sure the problem is that you are using physics to move the ball. Netcode 0.6 does not support predicted physics out of the box, which means that unless you did something very custom with physics what will happen is that you prediction loop updates the PhysicsVelocity many times, but physics runs after the prediction group and only moves the ball once, using the delta time for the most recent tick.

    The easiest fix from a pure netcode point of view is to only set the position and not use PhysicsVelocity in the prediction loop, but that can cause complications in other parts of your code depending on how you use physics.
     
  6. Ali_Bakkal

    Ali_Bakkal

    Joined:
    Jan 26, 2013
    Posts:
    90
    @timjohansson

    1) If you have to make a pong game with the ability to deviate the ball instantanously by pressing a button, how would you implement it with the current version of Netcode ?

    2) How the ball would be handled in general (Client and Server)? What part of the ball logic would be in Prediction Group ? Would you use the Unity Physics ?
     
  7. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    I would have the ball predicted and update everything related to it in the GhostPredictionSystemGroup. I would make sure it has position and velocity which are serialized so they get rolled back correctly and I would have a system which reads inputs and change the velocity of the ball directly if the button is pressed (in the GhostPredictionSystemGroup).
    I would avoid using any kind of "bool keyPress" in the inputs but instead store something like "int keyPressCount" to make it better at handling late or lost inputs on the server.

    I would not use physics with netcode 0.6 because physics cannot update the ball position inside the ghost prediction loop. You can do shape-casts inside the prediction logic (using unity physics) to find collisions, but you cannot yet have unity physics actually move the ball. We are planning to make it possible to use full physics simulation for that in the future.
     
  8. Ali_Bakkal

    Ali_Bakkal

    Joined:
    Jan 26, 2013
    Posts:
    90
    @timjohansson

    If I follow your advice,

    1) How would you do the ball movement in the prediction group ?
    You would write the equation of speed and use it to change ball position ?

    2) How would you detect the collision ? Or trigger ? is it possible to use only Unity Physics for that ? (If Yes How ?)
    Or should we use Raycast ?

    3) For the ball bounces, would you use equation of refraction to change ball position?

    4) What states/data should be shared between client and server regarding the ball ? How would you configure the GhostAuthoringComponent ? Untick PhysicVelocity ?

    5) Finally, how would you know/test that the code in Prediction Group is perfectly rolling back ? (Any tips to debug if the rolled back is wrong, would be very helpful)

    Thank you in advance for your help ! It helps us a lot
     
  9. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    Yes, position += velocity*deltaTime

    I would start with shape cast and see if that is good enough (called collider cast in unity physics). https://docs.unity3d.com/Packages/com.unity.physics@0.6/manual/collision_queries.html#collider-cast
    Perform he cast before you move the object, if it hits something move to where you hit, change velocity based on how you want to bounce and do the same thing again.

    I would start with something simple which only handles spheres being hit at a single point then refine from there as needed.

    I would make sure the velocity is serialized (translation and rotation already should be). PhysicsVelocity is not serialized by default, and the ball would be kinematic plus not using PhysicsVelocity anyway so you can leave that alone for now. Do make sure you set the physics body to kinematic though.

    Apart from just looking at it and see how it feels you can open NetDbg from the menu `Multiplayer > Open NetDbg` it has a checkbox at the top for `Show prediction errors` which will show you graphs over how large your prediction errors are frame to frame. It normalizes what 1 "unit" in the graph means so you can mostly see when something is more wrong than usually, but if you select a frame you can see the actual values for prediction errors in the text.
     
    Ali_Bakkal likes this.
  10. Ali_Bakkal

    Ali_Bakkal

    Joined:
    Jan 26, 2013
    Posts:
    90
    @timjohansson
    Thank you for your detailed explanation!
    Let us know when the Unity Physics will be supported by Netcode.

    For the meantime, we will try to implement your suggestion.

    Best