Predicted Characters Ghosts Jitter When SimulationTickRate is 30

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 :

[UpdateInGroup(typeof(GhostInputSystemGroup))]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
public partial class ThridPersonPlayerInputsSystem : SystemBase
{
    private InputManager _inputManager;

    protected override void OnCreate()
    {
        RequireForUpdate(SystemAPI.QueryBuilder().WithAll<ControlledCharacters, ThirdPersonPlayerCommands>().Build());
        RequireForUpdate<GameResources>();
        RequireForUpdate<NetworkTime>();
        RequireForUpdate<NetworkId>();

        // Get the Local InputManager instance
        _inputManager = InputManager.instance;
    }

    protected override void OnUpdate()
    {
        NetworkTick tick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;

        foreach (var (playerCommands, player, ghostOwner, entity) in SystemAPI
                     .Query<RefRW<ThirdPersonPlayerCommands>, RefRW<ControlledCharacters>, GhostOwner>()
                     .WithAll<GhostOwnerIsLocal>().WithEntityAccess())
        {
            playerCommands.ValueRW = default;

            // Move
            playerCommands.ValueRW.MoveInput = new JoystickData(new float2(_inputManager.MovementJoystick.Horizontal,
                _inputManager.MovementJoystick.Vertical));

            //Aim Input
            playerCommands.ValueRW.AimInputDelta = new JoystickData(new float2(_inputManager.Aimoystick.Horizontal,
                _inputManager.Aimoystick.Vertical));

            player.ValueRW.LastKnownCommandsTick = tick;
            player.ValueRW.LastKnownCommands = playerCommands.ValueRW;
        }
    }
}

___________________________________

[UpdateInGroup(typeof(PredictedFixedStepSimulationSystemGroup), OrderFirst = true)]
[BurstCompile]
public partial struct ThirdPersonPlayerFixedStepControlSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll<ControlledCharacters, ThirdPersonPlayerCommands>()
            .Build());
    }

    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        foreach (var (playerCommands, player, entity) in SystemAPI
                     .Query<ThirdPersonPlayerCommands, ControlledCharacters>().WithAll<Simulate>().WithEntityAccess())
        {
            // Character
            if (SystemAPI.HasComponent<CharacterControl>(player.ControlledCharacter))
            {
                var characterControl = SystemAPI.GetComponent<CharacterControl>(player.ControlledCharacter);

                //Get the MovementInput back as Float2
                var moveInput = playerCommands.MoveInput.ToFloat2();

                characterControl.MoveVector = new float3(moveInput.x, 0, moveInput.y);
                characterControl.MoveVector = MathUtilities.ClampToMaxLength(characterControl.MoveVector, 1f);

                // Aim
                characterControl.AimVector = playerCommands.AimInputDelta.ToFloat2();

                SystemAPI.SetComponent(player.ControlledCharacter, characterControl);
            }
        }
    }
}

___________________________________

[UpdateInGroup(typeof(KinematicCharacterPhysicsUpdateGroup))]
[BurstCompile]
public partial struct ThirdPersonCharacterPhysicsUpdateSystem : ISystem
{
    private EntityQuery _characterQuery;
    private FirstPersonCharacterUpdateContext _context;
    private KinematicCharacterUpdateContext _baseContext;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        _characterQuery = KinematicCharacterUtilities.GetBaseCharacterQueryBuilder()
            .WithAll<
                KinematicCharacterConfig,
                CharacterControl>()
            .Build(ref state);

        _context = new FirstPersonCharacterUpdateContext();
        _context.OnSystemCreate(ref state);
        _baseContext = new KinematicCharacterUpdateContext();
        _baseContext.OnSystemCreate(ref state);

        state.RequireForUpdate(_characterQuery);
        state.RequireForUpdate<NetworkTime>();
        state.RequireForUpdate<PhysicsWorldSingleton>();
    }

    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        if (!SystemAPI.HasSingleton<NetworkTime>())
            return;

        _context.OnSystemUpdate(ref state);
        _baseContext.OnSystemUpdate(ref state, SystemAPI.Time, SystemAPI.GetSingleton<PhysicsWorldSingleton>());

        ThirdPersonCharacterPhysicsUpdateJob job = new ThirdPersonCharacterPhysicsUpdateJob
        {
            Context = _context,
            BaseContext = _baseContext,
        };
        job.ScheduleParallel();
    }

    [BurstCompile]
    [WithAll(typeof(Simulate))]
    public partial struct ThirdPersonCharacterPhysicsUpdateJob : IJobEntity, IJobEntityChunkBeginEnd
    {
        public FirstPersonCharacterUpdateContext Context;
[LIST=1]
[*]        public KinematicCharacterUpdateContext BaseContext;
[/LIST]

        void Execute(FirstPersonCharacterAspect characterAspect)
        {
            characterAspect.PhysicsUpdate(ref Context, ref BaseContext);
        }

        public bool OnChunkBegin(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
        {
            BaseContext.EnsureCreationOfTmpCollections();
            return true;
        }

        public void OnChunkEnd(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask, bool chunkWasExecuted)
        {
        }
    }
}

___________________________________

public void PhysicsUpdate(ref FirstPersonCharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
{
    ref KinematicCharacterConfig characterConfig = ref CharacterComponent.ValueRW;
    ref KinematicCharacterBody characterBody = ref CharacterAspect.CharacterBody.ValueRW;
    ref float3 characterPosition = ref CharacterAspect.LocalTransform.ValueRW.Position;

    // First phase of default character update
    CharacterAspect.Update_Initialize(in this, ref context, ref baseContext, ref characterBody, baseContext.Time.DeltaTime);
    CharacterAspect.Update_ParentMovement(in this, ref context, ref baseContext, ref characterBody, ref characterPosition, characterBody.WasGroundedBeforeCharacterUpdate);
    CharacterAspect.Update_Grounding(in this, ref context, ref baseContext, ref characterBody, ref characterPosition);

    // Update desired character velocity after grounding was detected, but before doing additional processing that depends on velocity
    HandleVelocityControl(ref context, ref baseContext);

    // Second phase of default character update
    CharacterAspect.Update_PreventGroundingFromFutureSlopeChange(in this, ref context, ref baseContext, ref characterBody, in characterConfig.StepAndSlopeHandling);
    CharacterAspect.Update_GroundPushing(in this, ref context, ref baseContext, characterConfig.Gravity);
    CharacterAspect.Update_MovementAndDecollisions(in this, ref context, ref baseContext, ref characterBody, ref characterPosition);
    CharacterAspect.Update_MovingPlatformDetection(ref baseContext, ref characterBody);
    CharacterAspect.Update_ParentMomentum(ref baseContext, ref characterBody);
    CharacterAspect.Update_ProcessStatefulCharacterHits();
}

Jitter Video:

@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:

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.

Thanks for the report Opeth! We’ll take a look. Have you got an exact repro? In terms of:

  • How you set SimulationTickRate to 30?
  • And when?
  • And how you read and write the PredictedFixedStepSimulationSystemGroup.Timestep?
  • 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:

        // Inside your ServerWorld ISystem:
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            var clientServerTickRate = new ClientServerTickRate();
            clientServerTickRate.ResolveDefaults();
            clientServerTickRate.SimulationTickRate = clientServerTickRate.NetworkTickRate = 30;
            state.EntityManager.CreateSingleton(clientServerTickRate);
        }

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.
[quote=“Niki Walker (Unity), post:4, topic:928653, username:NikiWalker”]
And how you read and write the PredictedFixedStepSimulationSystemGroup.Timestep?
**[/quote]
**
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.

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.

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

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.

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).

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

Have u tested at latest 1.1.0-exp.1? Does it still get the same result?

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)

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

9385880–1312970–NetcodeInterpolationIssue.zip (49 KB)

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.

Hi. Any new update? I would like it work properly at mobile platform especially at Android with SimulationTickRate = 30.

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