Search Unity

Resolved Predicted Characters Ghosts Jitter When SimulationTickRate is 30

Discussion in 'NetCode for ECS' started by Opeth001, Sep 8, 2023.

  1. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    hello everyone, I am encountering an issue related to client-side predicted characters. There is a significant jitter problem that intermittently manifests and disappears every few seconds. I have followed all the steps outlined in the Character Controller Networking section.

    Note: In my case, it's a top-down game, so I don't need variable updates, and the rotation is directly determined by the the input joystick .

    Here is my code implementation :

    Code (CSharp):
    1.  
    2. [UpdateInGroup(typeof(GhostInputSystemGroup))]
    3. [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
    4. public partial class ThridPersonPlayerInputsSystem : SystemBase
    5. {
    6.     private InputManager _inputManager;
    7.  
    8.     protected override void OnCreate()
    9.     {
    10.         RequireForUpdate(SystemAPI.QueryBuilder().WithAll<ControlledCharacters, ThirdPersonPlayerCommands>().Build());
    11.         RequireForUpdate<GameResources>();
    12.         RequireForUpdate<NetworkTime>();
    13.         RequireForUpdate<NetworkId>();
    14.  
    15.         // Get the Local InputManager instance
    16.         _inputManager = InputManager.instance;
    17.     }
    18.  
    19.     protected override void OnUpdate()
    20.     {
    21.         NetworkTick tick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
    22.  
    23.         foreach (var (playerCommands, player, ghostOwner, entity) in SystemAPI
    24.                      .Query<RefRW<ThirdPersonPlayerCommands>, RefRW<ControlledCharacters>, GhostOwner>()
    25.                      .WithAll<GhostOwnerIsLocal>().WithEntityAccess())
    26.         {
    27.             playerCommands.ValueRW = default;
    28.  
    29.             // Move
    30.             playerCommands.ValueRW.MoveInput = new JoystickData(new float2(_inputManager.MovementJoystick.Horizontal,
    31.                 _inputManager.MovementJoystick.Vertical));
    32.  
    33.             //Aim Input
    34.             playerCommands.ValueRW.AimInputDelta = new JoystickData(new float2(_inputManager.Aimoystick.Horizontal,
    35.                 _inputManager.Aimoystick.Vertical));
    36.  
    37.             player.ValueRW.LastKnownCommandsTick = tick;
    38.             player.ValueRW.LastKnownCommands = playerCommands.ValueRW;
    39.         }
    40.     }
    41. }
    42.  
    43. ___________________________________
    44.  
    45. [UpdateInGroup(typeof(PredictedFixedStepSimulationSystemGroup), OrderFirst = true)]
    46. [BurstCompile]
    47. public partial struct ThirdPersonPlayerFixedStepControlSystem : ISystem
    48. {
    49.     [BurstCompile]
    50.     public void OnCreate(ref SystemState state)
    51.     {
    52.         state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll<ControlledCharacters, ThirdPersonPlayerCommands>()
    53.             .Build());
    54.     }
    55.  
    56.     [BurstCompile]
    57.     public void OnDestroy(ref SystemState state)
    58.     {
    59.     }
    60.  
    61.     [BurstCompile]
    62.     public void OnUpdate(ref SystemState state)
    63.     {
    64.         foreach (var (playerCommands, player, entity) in SystemAPI
    65.                      .Query<ThirdPersonPlayerCommands, ControlledCharacters>().WithAll<Simulate>().WithEntityAccess())
    66.         {
    67.             // Character
    68.             if (SystemAPI.HasComponent<CharacterControl>(player.ControlledCharacter))
    69.             {
    70.                 var characterControl = SystemAPI.GetComponent<CharacterControl>(player.ControlledCharacter);
    71.  
    72.                 //Get the MovementInput back as Float2
    73.                 var moveInput = playerCommands.MoveInput.ToFloat2();
    74.  
    75.                 characterControl.MoveVector = new float3(moveInput.x, 0, moveInput.y);
    76.                 characterControl.MoveVector = MathUtilities.ClampToMaxLength(characterControl.MoveVector, 1f);
    77.  
    78.                 // Aim
    79.                 characterControl.AimVector = playerCommands.AimInputDelta.ToFloat2();
    80.  
    81.                 SystemAPI.SetComponent(player.ControlledCharacter, characterControl);
    82.             }
    83.         }
    84.     }
    85. }
    86.  
    87. ___________________________________
    88.  
    89. [UpdateInGroup(typeof(KinematicCharacterPhysicsUpdateGroup))]
    90. [BurstCompile]
    91. public partial struct ThirdPersonCharacterPhysicsUpdateSystem : ISystem
    92. {
    93.     private EntityQuery _characterQuery;
    94.     private FirstPersonCharacterUpdateContext _context;
    95.     private KinematicCharacterUpdateContext _baseContext;
    96.  
    97.     [BurstCompile]
    98.     public void OnCreate(ref SystemState state)
    99.     {
    100.         _characterQuery = KinematicCharacterUtilities.GetBaseCharacterQueryBuilder()
    101.             .WithAll<
    102.                 KinematicCharacterConfig,
    103.                 CharacterControl>()
    104.             .Build(ref state);
    105.  
    106.         _context = new FirstPersonCharacterUpdateContext();
    107.         _context.OnSystemCreate(ref state);
    108.         _baseContext = new KinematicCharacterUpdateContext();
    109.         _baseContext.OnSystemCreate(ref state);
    110.  
    111.         state.RequireForUpdate(_characterQuery);
    112.         state.RequireForUpdate<NetworkTime>();
    113.         state.RequireForUpdate<PhysicsWorldSingleton>();
    114.     }
    115.  
    116.     [BurstCompile]
    117.     public void OnDestroy(ref SystemState state)
    118.     {
    119.     }
    120.  
    121.     [BurstCompile]
    122.     public void OnUpdate(ref SystemState state)
    123.     {
    124.         if (!SystemAPI.HasSingleton<NetworkTime>())
    125.             return;
    126.  
    127.         _context.OnSystemUpdate(ref state);
    128.         _baseContext.OnSystemUpdate(ref state, SystemAPI.Time, SystemAPI.GetSingleton<PhysicsWorldSingleton>());
    129.  
    130.         ThirdPersonCharacterPhysicsUpdateJob job = new ThirdPersonCharacterPhysicsUpdateJob
    131.         {
    132.             Context = _context,
    133.             BaseContext = _baseContext,
    134.         };
    135.         job.ScheduleParallel();
    136.     }
    137.  
    138.     [BurstCompile]
    139.     [WithAll(typeof(Simulate))]
    140.     public partial struct ThirdPersonCharacterPhysicsUpdateJob : IJobEntity, IJobEntityChunkBeginEnd
    141.     {
    142.         public FirstPersonCharacterUpdateContext Context;
    143. [LIST=1]
    144. [*]        public KinematicCharacterUpdateContext BaseContext;
    145. [/LIST]
    146.  
    147.         void Execute(FirstPersonCharacterAspect characterAspect)
    148.         {
    149.             characterAspect.PhysicsUpdate(ref Context, ref BaseContext);
    150.         }
    151.  
    152.         public bool OnChunkBegin(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
    153.         {
    154.             BaseContext.EnsureCreationOfTmpCollections();
    155.             return true;
    156.         }
    157.  
    158.         public void OnChunkEnd(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask, bool chunkWasExecuted)
    159.         {
    160.         }
    161.     }
    162. }
    163.  
    164. ___________________________________
    165.  
    166. public void PhysicsUpdate(ref FirstPersonCharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
    167. {
    168.     ref KinematicCharacterConfig characterConfig = ref CharacterComponent.ValueRW;
    169.     ref KinematicCharacterBody characterBody = ref CharacterAspect.CharacterBody.ValueRW;
    170.     ref float3 characterPosition = ref CharacterAspect.LocalTransform.ValueRW.Position;
    171.  
    172.     // First phase of default character update
    173.     CharacterAspect.Update_Initialize(in this, ref context, ref baseContext, ref characterBody, baseContext.Time.DeltaTime);
    174.     CharacterAspect.Update_ParentMovement(in this, ref context, ref baseContext, ref characterBody, ref characterPosition, characterBody.WasGroundedBeforeCharacterUpdate);
    175.     CharacterAspect.Update_Grounding(in this, ref context, ref baseContext, ref characterBody, ref characterPosition);
    176.  
    177.     // Update desired character velocity after grounding was detected, but before doing additional processing that depends on velocity
    178.     HandleVelocityControl(ref context, ref baseContext);
    179.  
    180.     // Second phase of default character update
    181.     CharacterAspect.Update_PreventGroundingFromFutureSlopeChange(in this, ref context, ref baseContext, ref characterBody, in characterConfig.StepAndSlopeHandling);
    182.     CharacterAspect.Update_GroundPushing(in this, ref context, ref baseContext, characterConfig.Gravity);
    183.     CharacterAspect.Update_MovementAndDecollisions(in this, ref context, ref baseContext, ref characterBody, ref characterPosition);
    184.     CharacterAspect.Update_MovingPlatformDetection(ref baseContext, ref characterBody);
    185.     CharacterAspect.Update_ParentMomentum(ref baseContext, ref characterBody);
    186.     CharacterAspect.Update_ProcessStatefulCharacterHits();
    187. }
    188.  
    Jitter Video:
     
  2. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    @philsa-unity

    The jitter problem seems to be connected to the CharacterInterpolationSystem in the Character Controller package. It unexpectedly happens when we set the target framerate to 60 and the SimulationTickRate to 30. However, if we change the SimulationTickRate to a different value, such as 29 or 60, the jitter issue vanishes completely.

    Additionally, the jitter problem also occurs in the Character Controller's OnlineFPS Sample when the target framerate is set to 60 and the SimulationTickRate is 30. However, it might not be visible in the first-person view but becomes noticeable when viewed from a top-down perspective.

    Video of the SimulationTickRate set to 29:

     
    Last edited: Sep 8, 2023
  3. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    Update:

    I've recently discovered that the issue originates from the netcode package. The problem arises because the PredictedFixedStepSimulationSystemGroup.Timestep does not match the 1f / simulationTickRate rule. Even when the simulationTickRate is set to 30, the PredictedFixedStepSimulationSystemGroup.Timestep remains at its default value of 1f/60. By manually updating this value, the jitter is completely eliminated.
     
    OrientedPain likes this.
  4. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    Thanks for the report Opeth! We'll take a look. Have you got an exact repro? In terms of:
    1. How you set SimulationTickRate to 30?
    2. And when?
    3. And how you read and write the PredictedFixedStepSimulationSystemGroup.Timestep?
    4. EDIT: Also, what version of netcode? This appears to be working in master.
    EDIT2: Note that, to set the ClientServerTickRate, you need to set it on the ServerWorld only, and before clients connect. The ServerWorld will automatically forward this to the client (after the client connects, via RPC).

    Example code:
    Code (CSharp):
    1.  
    2.         // Inside your ServerWorld ISystem:
    3.         [BurstCompile]
    4.         public void OnCreate(ref SystemState state)
    5.         {
    6.             var clientServerTickRate = new ClientServerTickRate();
    7.             clientServerTickRate.ResolveDefaults();
    8.             clientServerTickRate.SimulationTickRate = clientServerTickRate.NetworkTickRate = 30;
    9.             state.EntityManager.CreateSingleton(clientServerTickRate);
    10.         }
     
    Last edited: Sep 13, 2023
  5. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    I use the approach utilized in the character controller package sample, which creates an instance of the ClientServerTickRate. The SimulationFixedTimeStep is read-only and automatically configured by the netcode package based on the SimulationTickRate.


    I read and write the timestep value during the world's creation on both the client and server using the PredictedFixedStepSimulationSystemGroup. This method is necessary to manually adjust the SimulationFixedTimeStep RefreshRate.


    1.0.15


    This is how the character controller package and I are doing it.

    Also: the bug is reproducible on the character controller package samples, it is required to set the SimulationTickRate to 30 in order to reproduce it. Any other value will work correctly.
     
  6. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    896
    Just to clarify, the SimulationFixedTimeStep group is not required to run at the same rate as the SimulationTickRate but can be any integer multiple of it (greater than 1). We already fixed that and it is now automatically set and adjusted in a different way base on the ClientServerTickRate settings (the changes is not available yet publicly).

    The fact that the SimulationFixedTimeStep was running at 60hz and the Simulation at 30hz should work as expected, because what it is doing is nothing more than running the physics update twice per frame.

    The fact a jitter start occurring because of interpolation (in the CharacterController) may suggest there is some issues with that specific logic or we have again some problem with the calculation of the interpolation factor and the resulting interpolated LocalToWorldMatrix (if this is what the CharacterInterpolationSystem is modifying).

    Indeed we used that setup in another old sample (simulation at 60hz, physics at 120hz) and it was working great (but not using the CharacterController stuff).
    It worth investigating why anyway on our end to see if there are some gotchas there.
     
    Opeth001 likes this.
  7. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    115
    Hi, sorry I'm late to the conversation

    A lot of different things could potentially be at fault here:
    - built-in character interpolation system
    - How camera code is netcodified, or how it follows its target
    - the code that makes the animated (mecanim?) mesh follow the entity
    - something else?

    Because of all this, I think a repro project would be necessary. I've attempted to repro this but haven't been able to so far
     
    Opeth001 likes this.
  8. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    The jitter persists even when the Camera Follow System is disabled, and the character mesh is set to a basic capsule rendered via the Graphics package and without of any animations.

    to me, it seems like the CharacterInterpolationRememberTranformSystem is running at a higher speed than the CharacterInterpolationSystem. this causes the CharacterInterpolationSystem to skip some frames, resulting in the jittering effect.
     
  9. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    115
    Just to be clear, the jitter should happen Under these conditions?
    • Application.targetFramerate is 60
    • clientServerTickRate.SimulationTickRate is 30
    • PredictedFixedStepSimulationSystemGroup.TimeStep is 1f / 60f
    and:
    • PredictedFixedStepSimulationSystemGroup.TimeStep gets set in both Client and Server worlds.
    • clientServerTickRate.SimulationTickRate gets set in the server world only in a system's OnCreate (the OnlineFPS sample is actually wrong about setting this up in the Client world too. This changes in the next update).
     
    Last edited: Sep 26, 2023
  10. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    115
    Correction: I made some changes to my test and now I'm able to repro.

    It seems to happen when:
    • TargetFrameRate = -1 | FixedRate = 60 | SimulationTickRate = 30
    But does not happen when:
    • TargetFrameRate = -1 | FixedRate = 60 | SimulationTickRate = 60
    • TargetFrameRate = -1 | FixedRate = 30 | SimulationTickRate = 30

    So at the moment I'd be inclined to think this happens when the SimulationTickRate is not the same value as the PredictedFixedStepSimulationSystemGroup's update rate. (my test is running at around 200fps, for reference). Highly likely that something needs to change in the character interpolation system in order to fix this

    I'll be investigating this and will get back to you
     
    Last edited: Sep 26, 2023
    AtomicElbow and Opeth001 like this.
  11. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,129
    Have u tested at latest 1.1.0-exp.1? Does it still get the same result?
     
  12. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    115
    Also happening in 1.1.0-exp.1, when ClientServerSettings.PredictedFixedStepSimulationTickRatio = 2.
    (equivalent of making the PredictedFixedStepSimulationSystemGroup update at 2x the rate of ClientServerTickRate.SimulationTickRate)
     
    optimise likes this.
  13. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    115
    Just dropping in to give an update on this

    After some testing, I've discovered that the value of Time.ElapsedTime during the PredictedFixedStepSimulationSystemGroup update is sometimes not the exact time at which the fixed update should've happened (exactly fixedTimeStep after the previous fixed update); but instead it's the ElapsedTime of the present regular simulation frame. I could be wrong but it looks like the incorrect time only happens when PredictedFixedStepSimulationSystemGroup updates during a partial tick update (which can never happen if FixedRate == SimulationTickRate, because in that case if we need a fixed update, that also necessarily means we need a full catchup tick simulation AND THEN do an extra partial tick afterwards). This gives interpolation code incorrect values to work with, which results in broken interpolation

    I've managed to take the character package out of the equation and repro this issue using just a simple rigidbody player with interpolation, which means this isn't an issue specifically with the character interpolation. Test project is attached if you're curious. ClientServerTickRate can be tweaked in GameSetupSystem, and some debug logs showing the timing issue can be activated by uncommenting the code in DebugSystems

    This has yet to be fully confirmed, but we have an issue to track this and we'll be looking into it
     

    Attached Files:

    Last edited: Oct 4, 2023
    Opeth001, AtomicElbow and optimise like this.
  14. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,129
    I see. That's why I see at android build PredictedFixedStepSimulationSystemGroup will have unstable huge spike up and down on main thread even I connect to local pc server that has extremely low ping.
     
  15. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,129
    Hi. Any new update? I would like it work properly at mobile platform especially at Android with SimulationTickRate = 30.
     
  16. philsa-unity

    philsa-unity

    Unity Technologies

    Joined:
    Aug 23, 2022
    Posts:
    115
    We don't have an estimate for when the fix can be done, so until then I'd simply recommend setting your fixed update to 30 too in that case
     
    Opeth001 and optimise like this.