How can I fix visual effects on the Autonomous Proxy when experiencing high latency

I am very new to replication in Unreal and I am having a lot of issues trying to make a smooth experience. From the perspective of the server everything looks really smooth and works nicely, however, from the Autonomous proxies experience when there is some lag everything gets very jerky. The worst part is when the ball jumps as this causes the camera to shoot far too high.

Any pointers to what I should look into to resolve this would be greatly appreciated. I’m also attaching my code so you can see what is going on under the hood.

This is the movement related code, essentially on tick we make “moves” based on the current input and then simulates the move locally. At the end of the move it stores that move as the LastMove for replication purposes.

/**
Every frame we are creating moves to then be simulated.
If there is an active dash timer we should progess it towards 0.
*********************************************************************************/
void USDSBallMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    if(GetOwner()->GetLocalRole() == ROLE_AutonomousProxy || GetOwner()->GetRemoteRole() == ROLE_SimulatedProxy)
    {
        FBallMove NewMove = CreateMove(DeltaTime);
        SimulateMove(NewMove);
    }


	// Subtract the time between frames from the dash timer if the dash timer is active
	if(DashTimer > 0.f)
	{
		DashTimer = FMath::Max(0.f, DashTimer - DeltaTime);
	}

}

/**
Control the movement of the ball, called every frame.
*********************************************************************************/
void USDSBallMovementComponent::SimulateMove(const FBallMove& Move) 
{
	if(!MyBall) return;
	// Store the pawns velocity and copy the vertical velocity (z) for safe keeping
	FVector Velocity, DashVelocity = MyBall->SphereCollider->GetPhysicsLinearVelocity();


	// Limit the speed of the pawn if the pawn is moving too quick on the horizontal planes.
	UpdateBallsCurrentVelocity(Move, Velocity);

	if(Move.bJumped)
	{
		// Add the impulse to the ball to perform the jump.
		MyBall->SphereCollider->AddImpulse(FVector(0.f, 0.f, JumpForce * 1000.f));
	}

	if(Move.bDashed)
	{
		DashVelocity.Normalize();
		DashVelocity *= DashForce * 1000.f;

		// Add the impulse to the ball to perform the dash.
		MyBall->SphereCollider->AddImpulse(DashVelocity);

		// Set the length of time that we're to dash for.
		DashTimer =  1.5f;
	}

    LastMove = Move;
    // Reset the jump state at the end of the move
    bJumped = false;
	bDashed = false;
}

/**
Create a NewMove based on the balls current state.
*********************************************************************************/
FBallMove USDSBallMovementComponent::CreateMove(float DeltaTime) 
{
	FBallMove NewMove;
    NewMove.DeltaTime = DeltaTime;
    NewMove.Timestamp = GetWorld()->GetTimeSeconds();
    NewMove.InputLatitude = InputLatitude;
    NewMove.InputLongitude = InputLongitude;
    NewMove.bJumped = bJumped;
	NewMove.bDashed = bDashed;
	NewMove.DashTimer = DashTimer;

    return NewMove;
}

/**
Have the ball bearing perform a jump.
*********************************************************************************/
void USDSBallMovementComponent::Jump() 
{
	if(!MyBall || bJumped) return;
	// Only jump if we're in contact with something, normally the ground.

	if(MyBall->InContact == true)
	{
        bJumped = true;
	}	
}

Then there is a replication component that replicates the important stuff on tick. The replication component stores unacknowledged moves and iterates through them to update the client positions on the server itself, any unacknowledged move that is older than the last performed move is deleted. The replication component also interpolates the Simulated Proxy positions for a smooth looking networked experience, or so I thought.

/*
Called every frame to update the last move that the ball performed and then send this information to the
appropriate location based on what network role the ball has.
*************************************************************************************************************/
void USDSBallReplicationComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if(!MovementComponent) return;

	FBallMove LastMove = MovementComponent->GetLastMove();

	// This is the local controlled pawn playing on a server.
	if(GetOwnerRole() == ROLE_AutonomousProxy)
	{
		UnacknowledgedMoves.Add(LastMove);
		Server_SendMove(LastMove);
	}

	// This should only be called on the Listen server host.
	if(GetOwner()->GetRemoteRole() == ROLE_SimulatedProxy && MyPawn && MyPawn->IsLocallyControlled())
	{
		UpdateServerState(LastMove);
	}

	// Network controlled pawns on client machine
	if(GetOwnerRole() == ROLE_SimulatedProxy)
	{
		ClientTick(DeltaTime);
	}
}

/*
Called every frame on network controlled pawns on the client mahcines.
Used to create a spline to smoothly interpolate the balls location in the world under sub-par network conditions.
***************************************************************************************************************/
void USDSBallReplicationComponent::ClientTick(float DeltaTime) 
{
	ClientTimeSinceUpdate += DeltaTime;

	if(ClientTimeSinceUpdate < KINDA_SMALL_NUMBER || ClientTimeBetweenLastUpdates == 0.f) return;
	if(!MovementComponent || !MeshOffsetRoot || !MyPawn) return;

	// Spline related interpolation for smoothing out movement.
	CreateSpline();

	// Use curves to smoothly move the vehicle to a new location to avoid jerky network movement in a more acceptable way for vehicles.
	MyPawn->SphereCollider->SetWorldLocation(Spline.InterpolateLocation(GetLerpRatio()));

	// Update the vehicles velocity so that it is appropriate for the new curve that it is following.
	InterpolateVelocity();

	// Linear-ly apply the new rotation to line up the vehicle with its movement.
	MyPawn->SphereCollider->SetWorldRotation(FQuat::Slerp(ClientStartTransform.GetRotation(),  ServerState.Transform.GetRotation(), GetLerpRatio()));
}

/*
Sending a move to the server so that it can be processed there and recieved by the clients.
We also simulate the move locally so that the player can see what is happening beforehand.
We also keep track of how much time has been simulated by the client to make sure that they are not cheating.
***********************************************************************************************************************/
void USDSBallReplicationComponent::Server_SendMove_Implementation(FBallMove Move) 
{
	if(!MovementComponent) return;

	ClientSimulatedTime += Move.DeltaTime;
	MovementComponent->SimulateMove(Move);

	UpdateServerState(Move);
}

/*
TODO: Look at the move and make sure that it is valid. Also compare the client simulated
time with the actual game time.
*******************************************************************************************/
bool USDSBallReplicationComponent::Server_SendMove_Validate(FBallMove Move) 
{
    // TODO: Cheat protection.
	return true;
}

/*
Compare the array of moves with the Last Move that was performed, if there are moves that
are newer than the last move then we need to keep them for future iteration.
*********************************************************************************************/
void USDSBallReplicationComponent::ClearAcknowledgedMoves(FBallMove LastMove) 
{
	TArray<FBallMove> NewMoves;

	for (const FBallMove& Move : UnacknowledgedMoves)
	{
		if(Move.Timestamp > LastMove.Timestamp)
		{
			NewMoves.Add(Move);
		}
	}
	
	UnacknowledgedMoves = NewMoves;
}

/*
Update the server state based on information about the current state of the ball
****************************************************************************************************/
void USDSBallReplicationComponent::UpdateServerState(const FBallMove& Move) 
{
	if(!MovementComponent || !MyPawn) return;
	ServerState.LastMove = Move;
	ServerState.Transform = MyPawn->SphereCollider->GetComponentTransform();
	ServerState.Velocity = MovementComponent->GetVelocity();
}

/*
Since the server state has been updated we want to forward this trigger appropriately
depending on whether the ball is an autonomous proxy or a simulated proxy.
**************************************************************************************/
void USDSBallReplicationComponent::OnRep_ServerState() 
{
	switch(GetOwnerRole())
	{
		case ROLE_AutonomousProxy:
			AutonomousProxy_OnRep_ServerState();
			break;
		case ROLE_SimulatedProxy:
			SimulatedProxy_OnRep_ServerState();
			break;
		default:
			break;
	}
}

/*
This ball is a simulated proxy so we can update the ClientTimeBetweenLastUpdates to be
the time since the last update, ClientTimeSinceUpdate is reset when this happens so that 
progression in tick can keep track of how long there was between updates.

We then need to make note of the sphere colliders current transformation since this is
what is receiving forces and moving around the world, take note of it's velocity too.
The Transform and Velocity are used to create curves to smoothly interpolate this balls
position if it is a Simulated Proxy, this is essential for good networked experiences.

Doing this gives us the data required to create nice looking curves that represent where
other players have moved.
**************************************************************************************/
void USDSBallReplicationComponent::SimulatedProxy_OnRep_ServerState() 
{
	if(!MovementComponent || !MeshOffsetRoot || !MyPawn) return;

	ClientTimeBetweenLastUpdates = ClientTimeSinceUpdate;
	ClientTimeSinceUpdate = 0.f;

	ClientStartTransform = MyPawn->SphereCollider->GetComponentTransform(); // Not mesh offsetting.
	ClientStartVelocity = MovementComponent->GetVelocity(); // Not mesh offsetting.

	// NOTE: Mesh offsetting as currently implemented causes unwated pivot rotations that cause the Simulated Proxy to "Bounce"

	// ClientStartTransform.SetLocation(MeshOffsetRoot->GetComponentLocation()); // With Mesh Offsetting.
	// ClientStartTransform.SetRotation(MeshOffsetRoot->GetComponentQuat()); // With Mesh Offsetting.
	//ClientStartVelocity = MovementComponent->GetVelocity(); // With Mesh Offsetting.
	
	// MyPawn->SphereCollider->SetWorldTransform(ServerState.Transform); // With Mesh Offsetting
}

/*
Functionallity that is called on the clients controlled ball
******************************************************************************************/
void USDSBallReplicationComponent::AutonomousProxy_OnRep_ServerState() 
{
	if(!MovementComponent || !MyPawn) return;

	MyPawn->SphereCollider->SetWorldTransform(ServerState.Transform);
	MyPawn->SphereCollider->SetPhysicsLinearVelocity(ServerState.Velocity, false);

	ClearAcknowledgedMoves(ServerState.LastMove);
	
	for (const FBallMove& Move : UnacknowledgedMoves)
	{
		MovementComponent->SimulateMove(Move);
	}
}

Hi!
I have a same issue. Autonomous proxy is not smooth. Did you manage to fix this?
Best regards

Make sure your pawn doesn’t have “Replicate movement” enabled, it could be fighting with your custom replication.