This took me ages to track down but I finally found it… my game uses ‘floaty’ vehicle physics, and my custom movement component is written in exactly the same way as the Character Movement Component - and uses the INetworkPredictionInterface. I was having an issue where clients would be catapulted into the air when they initially spawn in - and the altitude would depend on how long the server instance of the game had been running.
I have finally tracked the issue down the way that APlayerController interacts with the interface - and while this does also affect the engines ‘Character Movement Component’, the issue is less prevalent there because of the way CMC’s movement is calculated. Note that this issue ONLY affects clients, it does not affect the server. The fix should be pretty simple, it just means changes to this section of engine code:
PlayerController.cpp
if ((GetRemoteRole() == ROLE_AutonomousProxy) && !IsNetMode(NM_Client) && !IsLocalPlayerController())
{
// force physics update for clients that aren't sending movement updates in a timely manner
// this prevents cheats associated with artificially induced ping spikes
// skip updates if pawn lost autonomous proxy role (e.g. TurnOff() call)
if (GetPawn() && !GetPawn()->IsPendingKill() && GetPawn()->GetRemoteRole() == ROLE_AutonomousProxy && GetPawn()->bReplicateMovement)
{
INetworkPredictionInterface* NetworkPredictionInterface = Cast<INetworkPredictionInterface>(GetPawn()->GetMovementComponent());
if (NetworkPredictionInterface)
{
FNetworkPredictionData_Server* ServerData = NetworkPredictionInterface->GetPredictionData_Server();
const float TimeSinceUpdate = ServerData ? GetWorld()->GetTimeSeconds() - ServerData->ServerTimeStamp : 0.f;
const float PawnTimeSinceUpdate = TimeSinceUpdate * GetPawn()->CustomTimeDilation;
if (PawnTimeSinceUpdate > FMath::Max<float>(DeltaSeconds+0.06f,AGameNetworkManager::StaticClass()->GetDefaultObject<AGameNetworkManager>()->MAXCLIENTUPDATEINTERVAL * GetPawn()->GetActorTimeDilation()))
{
//UE_LOG(LogPlayerController, Warning, TEXT("ForcedMovementTick. PawnTimeSinceUpdate: %f, DeltaSeconds: %f, DeltaSeconds+: %f"), PawnTimeSinceUpdate, DeltaSeconds, DeltaSeconds+0.06f);
const USkeletalMeshComponent* PawnMesh = GetPawn()->FindComponentByClass<USkeletalMeshComponent>();
if (!PawnMesh || !PawnMesh->IsSimulatingPhysics())
{
NetworkPredictionInterface->ForcePositionUpdate(PawnTimeSinceUpdate);
ServerData->ServerTimeStamp = GetWorld()->GetTimeSeconds();
}
}
}
}
What this block is doing is getting the servers prediction data for a client-controlled pawn - and trying to work out when the client last sent an update to the server. If the time between updates is too long, then the server force-calculates the position locally by calling ‘ForcePositionUpdate’ on the object.
The problem is - it uses ‘CurrentWorldTime - LastRecievedTime’ essentially. The problem is that when ‘FNetworkPredictionData_Server’ is created by a Pawns movement component, ‘ServerTimeStamp’ is initialized to 0 instead of the current world time. This means that when a player joins a match late, or when they are respawned with a new pawn during gameplay - ForcePositionUpdate gets called with a HUGE value for DeltaSeconds.
In my game, this was causing clients to be catapulted into the air when the respawned - and the altitude depended on how long it had been since the world was created. A simple fix would be to make sure that when ‘FNetworkPredictionData_Server’ is created, it is initialized with the current world time in seconds. This is how the prediction data is created in CharacterMovementComponent:
CharacterMovementComponent.cpp
if (!ServerPredictionData)
{
UCharacterMovementComponent* MutableThis = const_cast<UCharacterMovementComponent*>(this);
MutableThis->ServerPredictionData = new FNetworkPredictionData_Server_Character(*this);
}
return ServerPredictionData;
But the constructor for FNetworkPredictionData_Server is this:
(NetworkPredictionInterface.h)
FNetworkPredictionData_Server()
: ServerTimeStamp(0.f)
{}
In both CMC and my custom Movement Component, ForcePositionUpdate just runs the movement code with whatever delta time you pass in, which in some cases can be an insanely huge number. This might in some cases, cause client characters to also move with extreme values until they recieve an update from a client.
CharacterMovementComponent.cpp - Line 7082
PerformMovement(DeltaTime);
So, I would request that this be fixed so that when created - FNetworkPredictionData_Server initializes ServerTimeStamp to whatever the current server time is. I’ve fixed this in my own game, but I can’t change CharacterMovementComponent. Could submit a PR if required.