NetcodePredictionFixedRateManager runs on Partial Ticks

I have been debugging jitter in my project which uses NetCode for Entities, and Unity Physics. This became very noticable when I added a system to sync my player entities transform out into a game object, and target it with a cinemachine camera.

I had started with using an interpolated rigid body, which I noticed a lot of jitter with. To better understand what was happening I built a custom interpolation system and stumbled upon a hunk of code in NetcodePredictionFixedRateManager com.unity.netcode/Runtime/Snapshot/GhostPredictionSystemGroup.cs at master · needle-mirror/com.unity.netcode · GitHub

Adding an epsilon to the DeltaTime can cause the numerator to “overflow” into a an extra remaining update, which is actually for a PartialTick. With my simulation running at 64hz, the epsilon is ~6% of my timestep, and causes my systems to run for a partial tick pretty frequently.

This has an impact on the physics engine as com.unity.netcode/Runtime/Physics/PredictedPhysicsSystemGroup.cs at master · needle-mirror/com.unity.netcode · GitHub only guards against “FirstTimeFullyPredictingTick” if there are no ghosts with velocities (I think is the logic here, I’m not sure).

If this com.unity.physics/Unity.Physics/ECS/GraphicsIntegration/Systems/BufferInterpolatedRigidBodiesMotion.cs at master · needle-mirror/com.unity.physics · GitHub runs on a partial tick, and then a full tick on the next update, it will on the frame of the partial tick snapshot a “double move” (because we’re actually a tick ahead of where we should be), and then on the next frame is will likely to no move at all, because we’re now processing the same partial tick as a full tick.

I’m pretty fresh to both all the technologies here, so I apologies for any logical errors. Hopefully the code pointers help to follow my empirical experiences here.

I’m also no expert on floating point accuracy problems, but it seems the worry is an extremely small DeltaTime. If that is the case, a guard like

 if (group.World.Time.DeltaTime < m_TimeStep)
{
    return false;
}

Would ensure that the calculation is always close to 1, or whatever simulation::fixed multiple is expected without risking processing ticks before we should.

I’m basing my assumption that PredictedFixedStepSimulationSystemGroup shouldn’t run partial ticks on the comment associated with it

    /// <summary>
    /// <para>A fixed update group inside the ghost prediction. This is equivalent to <see cref="FixedStepSimulationSystemGroup"/> but for prediction.
    /// The fixed update group can have a higher update frequency than the rest of the prediction, and it does not do partial ticks.</para>
    /// <para>Note: This SystemGroup is intentionally added to non-netcode worlds, to help enable single-player testing.</para>
    /// </summary>

It also seems somewhat wasteful that the physics system re-snapshots re-simulated ticks, as BufferInterpolatedRigidBodiesMotion runs in PhysicsSystemGroup though I understand this is likely due to NetCode wrapping around Physics, rather than Physics knowing about NetCode. If Physics were to check

            if (!networkTime.IsFinalFullPredictionTick)
                return;

like GhostPredictionHistorySystem does, this problem would likely be less obvious, though I would still need to guard the movement system for my kinematic rigid body from running partial ticks, as that causes double movement within a frame as well.

TL;DR:

Had issues with interpolating kinematic rigid bodies using NetCode for Entites and Unity Physics.
Removing the epsilon from this line com.unity.netcode/Runtime/Snapshot/GhostPredictionSystemGroup.cs at 27b500c085edbce2e5f21547833cd5bb5138f2b4 · needle-mirror/com.unity.netcode · GitHub and running my movement system in [UpdateInGroup(typeof(AfterPhysicsSystemGroup))] (since the physics snapshots before physics runs, if you update before your prev and current are always the same) fixed all jittering.

2 Likes

[QUOTE=
Had issues with interpolating kinematic rigid bodies using NetCode for Entites and Unity Physics.
Removing the epsilon from this line com.unity.netcode/Runtime/Snapshot/GhostPredictionSystemGroup.cs at 27b500c085edbce2e5f21547833cd5bb5138f2b4 · needle-mirror/com.unity.netcode · GitHub and running my movement system in [UpdateInGroup(typeof(AfterPhysicsSystemGroup))] (since the physics snapshots before physics runs, if you update before your prev and current are always the same) fixed all jittering.[/QUOTE]

We’ve been noticing this for a while and had abandoned physics from netcode + physics + CharacterController because having that kind of jitter wasn’t acceptable. Hopefully this is a fix that will get looked at by the netcode team.

physics is designed to run only for full ticks, or what we declare as such. Because of that all systems in the PredictedFixedStepSimulationSystemGroup are running at fixed time rate (the physics rate) and only runs if there are predicted ghost (just to mention).

I think the logic in the rate manager need some update because adding a 0.001f is incorrect. This should be at least in relation to the required time step.

But then yes, this can cause some jittery behavior because we are also not passing the correct delta time.
We can indeed do a little better job here, but still allowing physics running the partial tick but then use interpolation for compensate the transform. This is something to explore though.

If you run your system in AfterPhysicsSystemGroup, for sure it is run smoothly and has no jitter: it runs also for partial ticks and he has continuous motion, contrarily, the fixed group time step make the motion of object moving looks jittery from the perspective of objects that doesn’t.

If you sync the gameobject, you should not use the LocalTransform but the LocalToWorldMatrix (because of the physics smoothing, otherwise the camera will also jitter and cause other stuff looks jittery).

This is definitely a major problem, and we will for sure looks at this.

The interpolation that physics already supports is working quite well for me. It already runs every frame (in the TransformSystemGroup https://github.com/needle-mirror/com.unity.physics/blob/63282c6ff34e35be9728dcca8060cdc7082f5323/Unity.Physics/ECS/GraphicsIntegration/Systems/SmoothRigidBodiesGraphicalMotion.cs#L19). It seems to me that ensuring the invariant that PredictedFixedStepSimulationSystemGroup only runs on full ticks, at a fixed rate, would resolve the jittery behaviour without changing the expectations of the physics system (e.g. that it could run on partial ticks). I think if it did run on partial ticks you would no longer need the interpolation system, but then you would have to deal with all the downsides of physics running with variable delta times as well as the perf implications of running it every frame.

Correct me if I’m misunderstanding here. Netcode moves the entire FixedStepSimulationGroup (https://github.com/needle-mirror/com.unity.netcode/blob/27b500c085edbce2e5f21547833cd5bb5138f2b4/Runtime/Physics/PredictedPhysicsSystemGroup.cs#L106). This contains the PhysicsSystemGroup (https://github.com/needle-mirror/com.unity.physics/blob/63282c6ff34e35be9728dcca8060cdc7082f5323/Unity.Physics/ECS/Base/Systems/PhysicsSystemGroups.cs#L14 ) which in turn contains the AfterPhysicsSystemGroup (https://github.com/needle-mirror/com.unity.physics/blob/63282c6ff34e35be9728dcca8060cdc7082f5323/Unity.Physics/ECS/Base/Systems/PhysicsSystemGroups.cs#L244).

Therefore my movement logic is running at a fixed rate, along with the rest of the physics systems. It is however only effecting my kinematic rigidbody’s LocalTransform after all dynamic bodies have been processed. This causes a 1 frame delay to the impact of my kinematic rigidbody’s movements on the dynamic bodies in the system.

This is required because the physics snapshotting happens after PhysicsInitializeGroup (https://github.com/needle-mirror/com.unity.physics/blob/63282c6ff34e35be9728dcca8060cdc7082f5323/Unity.Physics/ECS/GraphicsIntegration/Systems/BufferInterpolatedRigidBodiesMotion.cs#L32). I can follow the logic that the snapshotting should happen after the current state of PhysicsVelocity/LocalTransform is synced into the physics world, but the snapshotting doesn’t use the data from the physics world, it gets it directly from the ECS components (and does some unsafe shenanigans to sync a LocalTransform into a RigidTransform) that the physics world doesn’t touch until ExportPhysicsWorld. If this system ran before PhysicsInitializeGroup, I could run my movement code between BufferInterpolatedRigidBodiesMotion and PhysicsInitializeGroup and elminate the 1 frame delay.

Certainly I’m giving laser focused examples based on the fact I’m using a kinematic body and attempting to abuse code that seems to have been written for dynamic ones (e.g. see this comment https://github.com/needle-mirror/com.unity.physics/blob/63282c6ff34e35be9728dcca8060cdc7082f5323/Unity.Physics/ECS/GraphicsIntegration/Components/PhysicsGraphicalComponents.cs#L8). Maybe there is an invariant I don’t understand that requires BufferInterpolatedRigidBodiesMotion to run before PhysicsInitializeGroup.

Thanks for the tip, I did have issues related to LocalTransform vs LocalToWorld as well as the ordering of my systems but I had figured them out.

Here are some gifs I recorded to demonstrate things in my jitter test scene. The blue cube is moved in a monobehaviour in FixedUpdate at a constant speed and interpolated in Update. The red cube is an entity with a kinematic rigid body. It moves at the same constant speed, using the same inputs as the blue cube. The red cube has the Bounding Boxes from the NetCode Multiplayer PlayMode Tools turned on. I’m not expecting them to line up perfectly but this does still demonstrate the existing, rather harsh, jitter.

Current code with 20ms RTT jitter and 0ms RTT Delay
gMCMQEML7m1AwUlT8f

Current code with 20ms RTT jitter and 400ms RTT Delay
vSuD8lu1OtgGEjBjwW

My patch with 20ms RTT jitter and 0ms RTT Delay
Ou4yZUCtoZsgY94s2H

My patch with 20ms RTT jitter and 400ms RTT Delay
zu7LfeOIcRqz2XK1qV

If anyone has a suggestion where I can upload these gifs at 60fps, the effect is much more obvious

Yes, this is the real “bug”: PredictedFixedStepSimulationSystemGroup should already and MUST run only for full ticks (or close to it).
All physics systems should also run only at fixed time step and full ticks and not for partial ticks. This is the intended behavior.
If you need a system that affect kinematics and need to run for partial ticks, that must be put in the PredictedSimulationSystemGroup. This is how everything has been designed around (technically).

The fact that PredictedFixedStepSimulationSystemGroup run for a partial tick (because of the rounding) and because we are actually passing an elapsed time slightly in the future should be handled correctly by the interpolator, but because the ElapsedTime may be now greater than the realtime elapsed one (we are forcing a tick when it should not be the case), it is indeed slightly unexpected there.

Overall, yes, more proper handling may remove the jitter for interpolated bodies.

The BufferInterpolatedRigidBodiesMotion as many other should only run for the last full tick (not for others, but it is not a change we can do in physics). It is possible though to customize the physics loop to do that yourself (we show that in sample for 1.2).

You are absolutely correct, actually it was me forgetting that is running in the same group. Sorry, my mistake.
However, there is a slightly difference in behavior if the kinematic object is not interpolated (of course) and if you are now running after the physics updated all other objects. And important facts, kinematics physics objects are still simulated (as they should be). If there is any velocity set the objects also moves (and collide as well, so, ensure the velocity is not set either if you mean to teleport the object via position update).

So, long story short: we are going to avoid the rounding (at least in the way it is now) and properly address recorded time in case of partials (if we will not enforce this case, but we should).

Thanks for all the info! I’ll take a look for that example to customize the physics loop