Having jittering while player is jumping

We are working on a multiplayer third-person shooter game using DOTS-ECS and NetCode for Entities.

I am facing one issue, which is a jitter when a player jumps. I have faced this issue before as well. At that time it was due to the GhostVariant. I have not used the GhostVariant as mentioned in this character controller sample documentation (https://github.com/Unity-Technologies/CharacterControllerSamples/tree/master/_Documentation). After adding this class it was working as expected.

But now I am facing this issue again. I am not able to figure out the issue here.

A few developers on Discord suggested that this issue might be due to the high ping value.
Currently, in-game I am getting around 150 Ping even if I connect to the nearest server.

I tried replicating this issue using PlayMode Tools by setting the custom RTT delay and jitter. I was able to reproduce this issue, but it is not consistent as much as the client connected to the dedicated server.

I have a few questions here:

  1. Is the jitter while jumping due to the high ping value?
  2. Why I am getting the higher ping value even if I connect to the closet region? What could be the reason for this high ping value?



The fact you are seeing your character motion become jittery may be due to multiple reasons.

First of all I would strongly suggest to remove any CharacterController interpolation or in general physics interpolation to start excluding some root causes and bisect the problem,

What camera setup you have? Are the camera following the player or static ? In the former, are you using the LocalTransform or the LocalToWorld matrix to follow the character?

In case of character interpolation, you must use the LocalToWorld or the character will start looking jittering back and forth because of that.

In general if higher ping start getting the character jitter it may be related to some misprediction.

I would start logging some state (like position / rotation) and verify actually the value you are getting for a specific tick on both client and server. It is hard to give more suggestion without actually seeing other code parts or some short video, to make understanding how big the jitter is and what the frequency.

You can also use the NetDbg tool to check what values have been mispredicted, and what is the actual delta.

Question: did you try when using the playmode tool to set jitter to 0 bu keep latency? Does this make the overall behaviour better? Because, if this is the case, this even more suggest that the local client is definitively doing something different in the prediction loop in respect to the server.

1 Like

I use this sample project as the base structure. This is a first-person shooter game but I changed the character controller to a third-person using sample from the CharacterController Package from the package manager.

My orbit camera is also a ghost prefab and I use rotation and position values from this ghost object for the main camera.

I tried debugging the problem using the Netcode browser debug to check where I got the ghost prediction errors. I found that when a player jumps I get higher prediction errors in two fields Character LocalTransform and relative velocity from the Character Kinematice Body.

I am also getting other production errors, but to narrow down the problem, I commented on all that code. At last, these two fields were there while getting the jitter while jumping.

One interesting thing is I am not getting the jitter while jumping players in the localhost if the player’s ping value is around 30-50. If I set the higher ping value jittering is happening on the local host as well.

On the dedicated server even on the lower ping value like 30, I am getting the jittering problem.

I also tried Smoothing the local transform in case of prediction errors. It reduces the jitter amount. @CMarastoni Am I in the right direction or I am missing something?

using System;
using AOT;
using Unity.Assertions;
using Unity.Burst;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;


/// <summary>
/// The MegacityMetro-tuned prediction error <see cref="SmoothingAction"/> function for the <see cref="LocalTransform"/> component.
/// </summary>
[BurstCompile]
public unsafe struct ClientSmoothingAction
{
    /// <summary>
    /// The default value for the <see cref="DefaultSmoothingActionUserParams"/> if the no user data is passed to the function.
    /// Position is corrected if the prediction error is at least 1 unit (usually mt) and less than 10 unit (usually mt)
    /// </summary>
    public sealed class DefaultStaticUserParams
    {
        internal static readonly SharedStatic<float> maxDist = SharedStatic<float>.GetOrCreate<DefaultStaticUserParams, MaxDistKey>();
        internal static readonly SharedStatic<float> delta = SharedStatic<float>.GetOrCreate<DefaultStaticUserParams, DeltaKey>();

        static DefaultStaticUserParams()
        {
            maxDist.Data = 20;
            delta.Data = 0.5f;
        }
        class MaxDistKey { }
        class DeltaKey { }
    }

    /// <summary>
    /// Return a the burst compatible function pointer that can be used to register the smoothing action to the
    /// <see cref="GhostPredictionSmoothing"/> singleton.
    /// </summary>
    public static readonly PortableFunctionPointer<GhostPredictionSmoothing.SmoothingActionDelegate>
        Action = new(SmoothingAction);

    [BurstCompile(DisableDirectCall = true)]
    [MonoPInvokeCallback(typeof(GhostPredictionSmoothing.SmoothingActionDelegate))]
    private static void SmoothingAction(IntPtr currentData, IntPtr previousData, IntPtr usrData)
    {
        ref var trans = ref UnsafeUtility.AsRef<LocalTransform>((void*)currentData);
        ref var backup = ref UnsafeUtility.AsRef<LocalTransform>((void*)previousData);

        float maxDist = DefaultStaticUserParams.maxDist.Data;
        float delta = DefaultStaticUserParams.delta.Data;

        Assert.IsTrue(usrData.ToPointer() == null);

        var dist = math.distance(trans.Position, backup.Position);
        if (dist < maxDist && dist > delta && dist > 0)
        {
            trans.Position = backup.Position + (trans.Position - backup.Position) * delta / dist;
        }
    }
}

Adding prediction smoothing is sort of putting a bandaid on a problem without removing it. It’s useful don’t get me wrong, but there could be other issues.
A common cause for mispredictions could be tick batching. It defaults to 4 ticks batched. If only one side batches, you could get discrepancies. Does reducing tick batching to 1 (effectively disabling it) solve those jitters?
Quantization can also cause mispredictions.
The physics section in our doc talks about those here
They are good optimizations, but can cause mispredictions. I’d test disabling those to see if they are the cause. If they are, then yes prediction smoothing would be your friend here :slight_smile:

As you suggested I tried two things

  1. Updated the batch size from 4 to 1
  2. Updated Quantization values from 1000 to 0 (no optimization). I updated the Quantization value on the TrackedTransform_DefaultVariant.

But still no luck. I still get the jitter while jumping.


I tested the character jumping by reducing the time scale. And found that even with no RTT delay I getting jitter it’s just that it is not visible in the game view.

You can check in the attached video that I am getting multiple debug logs which means CharacterControlUtilities.StandardJump is getting called multiple times. I have tested this on a separate client-server as well. On server I getting the single call but on the client side I am getting the multiple calls.

All increasing the RTT also increases the debug count I am not sure why. I found a documentation snapping mechanism to try to snap the character. Due to multiple calls of StandardJump character might be snapping.

I am trying to update the logic so that StandardJump does not get multiple calls.

Is this bug from the Character Controller package or I am doing something wrong?

Link for the video demonstration

public readonly partial struct ThirdPersonCharacterAspect : IAspect, IKinematicCharacterProcessor<ThirdPersonCharacterUpdateContext>
{
    public readonly KinematicCharacterAspect CharacterAspect;
    public readonly RefRW<ThirdPersonCharacterComponent> CharacterComponent;
    public readonly RefRW<ThirdPersonCharacterControl> CharacterControl;
    public readonly RefRW<ActiveWeapon> ActiveWeapon;

    public void PhysicsUpdate(ref ThirdPersonCharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
    {
        ref ThirdPersonCharacterComponent characterComponent = 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 characterComponent.StepAndSlopeHandling);
        CharacterAspect.Update_GroundPushing(in this, ref context, ref baseContext, characterComponent.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();
    }


    private void HandleVelocityControl(ref ThirdPersonCharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
    {
        float deltaTime = baseContext.Time.DeltaTime;
        ref KinematicCharacterBody characterBody = ref CharacterAspect.CharacterBody.ValueRW;
        ref ThirdPersonCharacterComponent characterComponent = ref CharacterComponent.ValueRW;
        ref ThirdPersonCharacterControl characterControl = ref CharacterControl.ValueRW;

        // Rotate move input and velocity to take into account parent rotation
        if (characterBody.ParentEntity != Entity.Null)
        {
            characterControl.MoveVector = math.rotate(characterBody.RotationFromParent, characterControl.MoveVector);
            characterBody.RelativeVelocity = math.rotate(characterBody.RotationFromParent, characterBody.RelativeVelocity);
        }

        CharacterControl.ValueRW.IsGrounded = characterBody.IsGrounded;

        if (characterBody.IsGrounded)
        {
            // Move on ground
            float3 targetVelocity = characterControl.MoveVector * characterComponent.GroundMaxSpeed;
            CharacterControlUtilities.StandardGroundMove_Interpolated(ref characterBody.RelativeVelocity, targetVelocity, characterComponent.GroundedMovementSharpness, deltaTime, characterBody.GroundingUp, characterBody.GroundHit.Normal);

            // Jump
            if (characterControl.Jump)
            {
                Debug.Log($"characterControl.Jump : {characterControl.Jump}");
                CharacterControlUtilities.StandardJump(ref characterBody, characterBody.GroundingUp * characterComponent.JumpSpeed, false, characterBody.GroundingUp);
            }
        }
        else
        {
            CharacterControl.ValueRW.Jump = false;

            // Move in air
            float3 airAcceleration = characterControl.MoveVector * characterComponent.AirAcceleration;
            if (math.lengthsq(airAcceleration) > 0f)
            {
                float3 tmpVelocity = characterBody.RelativeVelocity;
                CharacterControlUtilities.StandardAirMove(ref characterBody.RelativeVelocity, airAcceleration, characterComponent.AirMaxSpeed, characterBody.GroundingUp, deltaTime, false);

                // Cancel air acceleration from input if we would hit a non-grounded surface (prevents air-climbing slopes at high air accelerations)
                if (characterComponent.PreventAirAccelerationAgainstUngroundedHits && CharacterAspect.MovementWouldHitNonGroundedObstruction(in this, ref context, ref baseContext, characterBody.RelativeVelocity * deltaTime, out ColliderCastHit hit))
                {
                    characterBody.RelativeVelocity = tmpVelocity;
                }
            }

            // Gravity
            CharacterControlUtilities.AccelerateVelocity(ref characterBody.RelativeVelocity, characterComponent.Gravity, deltaTime);

            // Drag
            CharacterControlUtilities.ApplyDragToVelocity(ref characterBody.RelativeVelocity, deltaTime, characterComponent.AirDrag);
        }
    }

    public void VariableUpdate(ref ThirdPersonCharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
    {
        ref KinematicCharacterBody characterBody = ref CharacterAspect.CharacterBody.ValueRW;
        ref ThirdPersonCharacterComponent characterComponent = ref CharacterComponent.ValueRW;
        ref ThirdPersonCharacterControl characterControl = ref CharacterControl.ValueRW;
        ref quaternion characterRotation = ref CharacterAspect.LocalTransform.ValueRW.Rotation;
        ActiveWeapon activeWeapon = ActiveWeapon.ValueRO;

        // Add rotation from parent body to the character rotation
        // (this is for allowing a rotating moving platform to rotate your character as well, and handle interpolation properly)
        KinematicCharacterUtilities.AddVariableRateRotationFromFixedRateRotation(ref characterRotation, characterBody.RotationFromParent, baseContext.Time.DeltaTime, characterBody.LastPhysicsUpdateDeltaTime);

        // Rotate towards move direction
        if (math.lengthsq(characterControl.MoveVector) > 0f)
        {
            CharacterControlUtilities.SlerpRotationTowardsDirectionAroundUp(ref characterRotation, baseContext.Time.DeltaTime, math.normalizesafe(characterControl.MoveVector), MathUtilities.GetUpFromRotation(characterRotation), characterComponent.RotationSharpness);
        }
    }

    #region Character Processor Callbacks
    public void UpdateGroundingUp(
        ref ThirdPersonCharacterUpdateContext context,
        ref KinematicCharacterUpdateContext baseContext)
    {
        ref KinematicCharacterBody characterBody = ref CharacterAspect.CharacterBody.ValueRW;

        CharacterAspect.Default_UpdateGroundingUp(ref characterBody);
    }

    public bool CanCollideWithHit(
        ref ThirdPersonCharacterUpdateContext context,
        ref KinematicCharacterUpdateContext baseContext,
        in BasicHit hit)
    {
        return PhysicsUtilities.IsCollidable(hit.Material);
    }

    public bool IsGroundedOnHit(
        ref ThirdPersonCharacterUpdateContext context,
        ref KinematicCharacterUpdateContext baseContext,
        in BasicHit hit,
        int groundingEvaluationType)
    {
        ThirdPersonCharacterComponent characterComponent = CharacterComponent.ValueRO;

        return CharacterAspect.Default_IsGroundedOnHit(
            in this,
            ref context,
            ref baseContext,
            in hit,
            in characterComponent.StepAndSlopeHandling,
            groundingEvaluationType);
    }

    public void OnMovementHit(
            ref ThirdPersonCharacterUpdateContext context,
            ref KinematicCharacterUpdateContext baseContext,
            ref KinematicCharacterHit hit,
            ref float3 remainingMovementDirection,
            ref float remainingMovementLength,
            float3 originalVelocityDirection,
            float hitDistance)
    {
        ref KinematicCharacterBody characterBody = ref CharacterAspect.CharacterBody.ValueRW;
        ref float3 characterPosition = ref CharacterAspect.LocalTransform.ValueRW.Position;
        ThirdPersonCharacterComponent characterComponent = CharacterComponent.ValueRO;
        ref ThirdPersonCharacterControl characterControl = ref CharacterControl.ValueRW;

        CharacterAspect.Default_OnMovementHit(
            in this,
            ref context,
            ref baseContext,
            ref characterBody,
            ref characterPosition,
            ref hit,
            ref remainingMovementDirection,
            ref remainingMovementLength,
            originalVelocityDirection,
            hitDistance,
            characterComponent.StepAndSlopeHandling.StepHandling,
            characterComponent.StepAndSlopeHandling.MaxStepHeight,
            characterComponent.StepAndSlopeHandling.CharacterWidthForStepGroundingCheck);
    }

    public void OverrideDynamicHitMasses(
        ref ThirdPersonCharacterUpdateContext context,
        ref KinematicCharacterUpdateContext baseContext,
        ref PhysicsMass characterMass,
        ref PhysicsMass otherMass,
        BasicHit hit)
    {
        // Custom mass overrides
    }

    public void ProjectVelocityOnHits(
        ref ThirdPersonCharacterUpdateContext context,
        ref KinematicCharacterUpdateContext baseContext,
        ref float3 velocity,
        ref bool characterIsGrounded,
        ref BasicHit characterGroundHit,
        in DynamicBuffer<KinematicVelocityProjectionHit> velocityProjectionHits,
        float3 originalVelocityDirection)
    {
        ThirdPersonCharacterComponent characterComponent = CharacterComponent.ValueRO;

        CharacterAspect.Default_ProjectVelocityOnHits(
            ref velocity,
            ref characterIsGrounded,
            ref characterGroundHit,
            in velocityProjectionHits,
            originalVelocityDirection,
            characterComponent.StepAndSlopeHandling.ConstrainVelocityToGroundPlane);
    }
    #endregion
}
 /// <summary>
 /// Apply inputs that need to be read at a fixed rate.
 /// It is necessary to handle this as part of the fixed step group, in case your framerate is lower than the fixed step rate.
 /// </summary>
 [UpdateInGroup(typeof(PredictedFixedStepSimulationSystemGroup), OrderFirst = true)]
 [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation | WorldSystemFilterFlags.ServerSimulation)]
 [BurstCompile]
 public partial struct ThirdPersonPlayerFixedStepControlSystem : ISystem
 {
     [BurstCompile]
     public void OnCreate(ref SystemState state)
     {
         state.RequireForUpdate<NetworkTime>();
         state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll<ThirdPersonPlayer, ThirdPersonPlayerInputs>().Build());
     }

     [BurstCompile]
     public void OnUpdate(ref SystemState state)
     {
         ThirdPersonPlayerFixedStepControlJob job = new ThirdPersonPlayerFixedStepControlJob
         {
             LocalTransformLookup = SystemAPI.GetComponentLookup<LocalTransform>(true),
             CharacterControlLookup = SystemAPI.GetComponentLookup<ThirdPersonCharacterControl>(false),
             OrbitCameraLookup = SystemAPI.GetComponentLookup<OrbitCamera>(false),
         };
         state.Dependency = job.Schedule(state.Dependency);
     }

     [BurstCompile]
     //[WithAll(typeof(Simulate))]
     public partial struct ThirdPersonPlayerFixedStepControlJob : IJobEntity
     {
         [ReadOnly]
         public ComponentLookup<LocalTransform> LocalTransformLookup;
         public ComponentLookup<ThirdPersonCharacterControl> CharacterControlLookup;
         public ComponentLookup<OrbitCamera> OrbitCameraLookup;

         void Execute(RefRW<ThirdPersonPlayerInputs> playerCommands, in ThirdPersonPlayer player, in CommandDataInterpolationDelay commandInterpolationDelay)
         {
             if (CharacterControlLookup.HasComponent(player.ControlledCharacter))
             {
                 ThirdPersonCharacterControl characterControl = CharacterControlLookup[player.ControlledCharacter];

                 float3 characterUp = MathUtilities.GetUpFromRotation(LocalTransformLookup[player.ControlledCharacter].Rotation);

                 // Get camera rotation, since our movement is relative to it.
                 quaternion cameraRotation = quaternion.identity;

                 if (OrbitCameraLookup.HasComponent(player.ControlledCamera))
                 {
                     // Camera rotation is calculated rather than gotten from transform, because this allows us to 
                     // reduce the size of the camera ghost state in a netcode prediction context.
                     // If not using netcode prediction, we could simply get rotation from transform here instead.
                     OrbitCamera orbitCamera = OrbitCameraLookup[player.ControlledCamera];
                     cameraRotation = OrbitCameraUtilities.CalculateCameraRotation(characterUp, orbitCamera.PlanarForward, orbitCamera.PitchAngle);
                 }

                 float3 cameraForwardOnUpPlane = math.normalizesafe(MathUtilities.ProjectOnPlane(MathUtilities.GetForwardFromRotation(cameraRotation), characterUp));
                 float3 cameraRight = MathUtilities.GetRightFromRotation(cameraRotation);

                 // Move
                 if (!characterControl.IsInFinisherState)
                 {
                     characterControl.MoveVector = (playerCommands.ValueRO.MoveInput.y * cameraForwardOnUpPlane) + (playerCommands.ValueRO.MoveInput.x * cameraRight);
                     characterControl.MoveVector = MathUtilities.ClampToMaxLength(characterControl.MoveVector, 1f);

                     //move axis
                     characterControl.MoveAxisValue = playerCommands.ValueRO.MoveInput;

                     // Set MoveVector to the camera's forward direction
                     float3 cameraForward = math.mul(cameraRotation, new float3(0, 0, 1));
                     characterControl.RotationVector = MathUtilities.ClampToMaxLength(cameraForward, 1f);
                 }
                 else
                 {
                     characterControl.MoveVector = 0;

                     //move axis
                     characterControl.MoveAxisValue = 0;

                     // Set MoveVector to the camera's forward direction
                     characterControl.RotationVector = 0;
                 }

                 // Set Climbing Status
                 playerCommands.ValueRW.IsClimbing = characterControl.IsClimbing;

                 // Jump
                 if (characterControl.IsGrounded)
                 {
                     if (playerCommands.ValueRO.JumpPressed)
                     {
                         Debug.LogError("m_DefaultActionsMap JumpPressed");
                         characterControl.Jump = true;
                     }
                     else
                         characterControl.Jump = false;

                     if (characterControl.IsInFinisherState)
                         characterControl.Sprint = false;
                     else
                         characterControl.Sprint = playerCommands.ValueRO.SprintHeld;

                     characterControl.Crouch = playerCommands.ValueRO.CrouchHeld;
                     characterControl.IsDancing = playerCommands.ValueRO.DanceHeld;
                     characterControl.DanceIndex = playerCommands.ValueRO.DanceIndex;
                     characterControl.IsAttackPerformed = playerCommands.ValueRO.AttackPerformed.IsSet;
                     characterControl.SlidePressed = playerCommands.ValueRO.SlidePressed;
                 }
                 else
                 {
                     characterControl.ClimbPressed = playerCommands.ValueRO.ClimbPressed;
                 }
                 characterControl.DashPressed = playerCommands.ValueRO.DashPressed;
                 characterControl.IsAiming = playerCommands.ValueRO.AimPressed;
                 characterControl.IsThrowingGrenade = playerCommands.ValueRO.GrenadeThrow;
                 characterControl.IsShooting = playerCommands.ValueRO.ShootPressed.IsSet;
                 CharacterControlLookup[player.ControlledCharacter] = characterControl;
             }
         }
     }
 }

Given the setup you did and verified that with jitter network condition actually the problem become visible (because now packet doesn’t not arrive every frame) and checked that the value are actually mispredicted, we can focus on that part.

We need to check in more details what are you doing when issuing the jump command and the whole prediction logic to understand what it is going on.

Do you a project to share or could you please share here some of that relevant code?

I was finally able to solve this issue. It was mainly due to how I handled the input in the ThirdPersonPlayerFixedStepControlSystem. In that, I was updating the characterControl.Jump only if the player is grounded. That’s the reason why once the player jump is true IsGrounded value will be false so characterControl.The jump value will be set to false with little delay. After updating jumping is working properly.

Thank you for the support!