Inaccurate DeltaTime in Client Character's PerformMovement()

Hi, my project involves force-based acceleration (jet thrusters) calculated in PerformMovement. Since it’s force/time based, it depends on reliable DeltaTime values. I’ve been having all kind of strange issues that don’t make any sense.

Trying to nail down the problem, I’ve created a completely bare project based on the Third Person Template, and added a custom CharacterMovementComponent to the default character. My custom class keeps a running sum of DeltaTime in PerformMovement and logs it to screen. In a perfect world, this output should increase like a real clock in seconds. Instead, as framerate increases over around 30 FPS, it starts counting up faster and faster on the client. The host is always OK.

Here’s a gif, client is the inset window. Watch how fast the client’s time sum drifts off. This is around 80 FPS - if I increase to a few hundred FPS, the drift skyrockets.

If I move my logging into TickComponent, it counts up correctly (real time). I think this means that the client’s ReplicateMoveToServer() is modifying the DeltaTime before it passes it to PerformMovement(). The only function I see that modifies DeltaTime is in FNetworkPredictionData_Client_Character::UpdateTimeStampAndDeltaTime(…) which tries to modify DeltaTime to match what it “thinks” the server’s going to use for DeltaTime. Obviously it isn’t doing a very good job at high FPS right now. If I limit the client’s FPS to about 30, it stops drifting.

This seems like an engine bug to me but before I try reporting it I’d greatly appreciate any input/suggestions. The UpdateTimeStamp… method that seems to be the culprit isn’t virtual and I’m trying not to modify the engine, or copy hundreds of lines of parent methods into my own class just to change this, but maybe I’ll have to?

Thanks.



// HEADER FILE FOR MOVEMENTCOMPONENT
// ==============================
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "HumanCharacterMovementComponent.generated.h"

UCLASS()
class TICKTEST_API UHumanCharacterMovementComponent : public UCharacterMovementComponent
{
    GENERATED_BODY()

protected:
    float PMTime;

public:
    virtual void PerformMovement(float DeltaTime) override;
};



// CPP FILE FOR MOVEMENTCOMPONENT
// ==============================

#include "HumanCharacterMovementComponent.h"
#include "GameFramework/Character.h"
#include <EngineGlobals.h>
#include <Runtime/Engine/Classes/Engine/Engine.h>

void UHumanCharacterMovementComponent::PerformMovement(float DeltaTime)
{
    PMTime += DeltaTime;

    // Only display accumulated time for the client's character instance. Display on both host and client so we can compare them.
    if ((CharacterOwner->IsLocallyControlled() && CharacterOwner->Role != ROLE_Authority) || (!CharacterOwner->IsLocallyControlled() && CharacterOwner->Role == ROLE_Authority))
        if (GEngine)
            GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, FString::SanitizeFloat(PMTime));

    Super::PerformMovement(DeltaTime);
}

After some discussion with the helpful people in Discord, I think that I failed to understand that whenever the client combines moves for the server, it first reverts its current move, then figures out a combined move, then replays the combined move. If a person is naively doing what I’m doing and running basic logic in PerformMovement that depends on DeltaTime, this means that when the PerformMovement method is called every frame, it could be called with that frame’s DeltaTime, or it might be called with a summation of multiple frames’ DeltaTimes based on a combined move. It would be necessary to build extra code to handle the revert/combining of moves to deal with this properly. Hopefully this info might help someone else one day, I’ll post again if I reveal anything else that would have helped a future-me.