Custom movement component giving a lot more server corrections on dedicated server VS. playing in editor as Listen Server or client modes.

I have two videos here for comparison, I just now got done implementing my first custom movement component and all its client side prediction for saved moves and such following several different source pieced together from Delgoodie and a few other sources. I’m still getting jitter on my dedicated server, my PIE listen server mode seems to be working as expected, but I get a lot more corrections when playing on my dedicated server that im launching via an editor flag -server. I’ve been banging my head against the keyboard for weeks looking for a solution to this or at least a direction I can start improving or optimizing my code (which isnt a lot as of yet).

Here is my listen server: (seems relatively normal)

Here is my dedicated server (Running on my local host along side editor):

That does seem to be a lot more than normal. BUT, the server in general will always have a bit of corrections. Just depends on your margin of error. The jittering from corrections can be smoothed with network smoothing.

Turn on the ping emulation and test at 100ms, 3% loss.

It didnt change much

Play in client mode, 2 player, new viewports.
Set the server to fixed FPS 30 (30Hz).

Client mode “ALWAYS” runs a dedicated server in the background. No need to launch params etc.

After setting my multiplayer options like you suggested:

what happened to the animation?

The way I was doing it previously I was replicating a bunch of bools to control those. I turned off ALL replicating vars in my character just in case this was wacky RPC stuff. Im also gonna be redoing all my animation blend spaces to run off replicated stuff coming through the movement components values rather than using RPC bools, from what I understand its better practice.

Turn it all back on and retest using the settings. Smoothing interps A → B, without animation you get flipbook popping.

Turned it back on, didnt change much.

Adjust your smoothing. Defaults are a bit harsh (snappy). I use the Character Movement Component (CMC) pretty much strictly. So for reference here’s what I’ve got my smoothing set to.

Smooth Location Time and Smooth Rotation Time. This controls how fast you get to the corrected location/rotation. The higher the value the more easing there is. But don’t go too crazy, you’ll desync the owning client.


Just so you know, High pingers get a lot of smoothing because updates are so far behind the servers sim.

Mine was set up almost identical to yours already in my options. I tried playing with the smoothing earlier, and it had no effect. It still isnt from what it seems.

Jump it up another 50-100ms and retest. If that doesn’t resolve it, then you need to start looking at prediction code. Floating point precision errors, Save moves, Acks etc.

To help narrow it down test movement without rotation. No mouse input.
If there’s no change then it’s all standard movement (Fwd/Bwd, L/R). I did notice a lot more in left/right and backward vs forward.

I guess it has to be something in the code then id assume. I have no idea what though, this has happened to me with 3 different iterations of implementing delgoodies(and others) custom movement components, and I followed their code as well as I could with implementing it with the information I’ve gathered so far… Im just at the end of my rope with this, what seems so trivial to fix seems to be giving me the largest headache I’ve had with multiplayer dev to date.


// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

/**
 * 
 */
UCLASS()
class MONSTERWARS_API UMWCharacterMovementComponent : public UCharacterMovementComponent {
    
	GENERATED_BODY()
	bool safe_wants_to_sprint;
    
    UPROPERTY(EditDefaultsOnly) 
    float sprint_max_walk_speed;

    UPROPERTY(EditDefaultsOnly) 
    float walk_max_walk_speed;

    public:
        UMWCharacterMovementComponent();

        virtual FNetworkPredictionData_Client* GetPredictionData_Client() const override;

        UFUNCTION(BlueprintCallable)
        void SprintPressed();

        UFUNCTION(BlueprintCallable)
        void SprintReleased();
        
    protected:
        virtual void UpdateFromCompressedFlags(uint8 Flags) override;
        virtual void OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity) override;


    class FSavedMove_MonsterWars_Character : public FSavedMove_Character {
        typedef FSavedMove_Character Super;
        uint8 saved_wants_to_sprint:1;

        virtual bool CanCombineWith(const FSavedMovePtr& NewMove, ACharacter* InCharacter, float MaxDelta) const override;
        virtual void Clear() override;
        virtual uint8 GetCompressedFlags() const override;
        virtual void SetMoveFor(ACharacter* C, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData) override;
        virtual void PrepMoveFor(ACharacter* C) override;
    };

    class FNetworkPredictionData_Client_MWCharacter : public FNetworkPredictionData_Client_Character {
        public:
            typedef FNetworkPredictionData_Client_Character Super;
            FNetworkPredictionData_Client_MWCharacter(const UMWCharacterMovementComponent& ClientMovement);
            virtual FSavedMovePtr AllocateNewMove() override;
        
    };
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "MWCharacterMovementComponent.h"
#include "GameFramework/Character.h"

UMWCharacterMovementComponent::UMWCharacterMovementComponent() {

}

FNetworkPredictionData_Client* UMWCharacterMovementComponent::GetPredictionData_Client() const {
    check(PawnOwner != nullptr);

    if (ClientPredictionData == nullptr) {
        UMWCharacterMovementComponent* MutableCMC = const_cast<UMWCharacterMovementComponent*>(this);
        MutableCMC->ClientPredictionData = new FNetworkPredictionData_Client_MWCharacter(*this);
        MutableCMC->ClientPredictionData->MaxSmoothNetUpdateDist = 92.f;
        MutableCMC->ClientPredictionData->NoSmoothNetUpdateDist = 140.f;

    }
    
    return ClientPredictionData;
}

void UMWCharacterMovementComponent::UpdateFromCompressedFlags(uint8 Flags) {
    Super::UpdateFromCompressedFlags(Flags);

    safe_wants_to_sprint = (Flags & FSavedMove_Character::FLAG_Custom_0) != 0;
}


void UMWCharacterMovementComponent::OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity) {
    Super::OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity);
    if (MovementMode == MOVE_Walking) {
        if (safe_wants_to_sprint) {
            MaxWalkSpeed = sprint_max_walk_speed;
        } else {
            MaxWalkSpeed = walk_max_walk_speed;
        }
    }
}

void UMWCharacterMovementComponent::SprintPressed() {
    safe_wants_to_sprint = true;
}
void UMWCharacterMovementComponent::SprintReleased() {
    safe_wants_to_sprint = false;
}

bool UMWCharacterMovementComponent::FSavedMove_MonsterWars_Character::CanCombineWith(const FSavedMovePtr& NewMove, ACharacter* InCharacter, float MaxDelta) const {

    FSavedMove_MonsterWars_Character* NewMWSavedMove = static_cast<FSavedMove_MonsterWars_Character*>(NewMove.Get());
    if (saved_wants_to_sprint != NewMWSavedMove->saved_wants_to_sprint) {
        return false;
    }
    return FSavedMove_Character::CanCombineWith(NewMove, InCharacter, MaxDelta);
}

void UMWCharacterMovementComponent::FSavedMove_MonsterWars_Character::Clear() {
    FSavedMove_Character::Clear();
    saved_wants_to_sprint = 0;
}
uint8 UMWCharacterMovementComponent::FSavedMove_MonsterWars_Character::GetCompressedFlags() const {
    uint8 Result = Super::GetCompressedFlags();
    if (saved_wants_to_sprint) {
        Result |= FLAG_Custom_0;
    }
    return Result;
}
void UMWCharacterMovementComponent::FSavedMove_MonsterWars_Character::SetMoveFor(ACharacter* C, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData) {
    FSavedMove_Character::SetMoveFor(C, InDeltaTime, NewAccel, ClientData);
    UMWCharacterMovementComponent* CMC = Cast<UMWCharacterMovementComponent>(C->GetCharacterMovement());
    saved_wants_to_sprint = CMC->safe_wants_to_sprint;
}
void UMWCharacterMovementComponent::FSavedMove_MonsterWars_Character::PrepMoveFor(ACharacter* C) {
    Super::PrepMoveFor(C);
    UMWCharacterMovementComponent* CMC = Cast<UMWCharacterMovementComponent>(C->GetCharacterMovement());
    CMC->safe_wants_to_sprint = saved_wants_to_sprint;
}

UMWCharacterMovementComponent::FNetworkPredictionData_Client_MWCharacter::FNetworkPredictionData_Client_MWCharacter(const UMWCharacterMovementComponent& ClientMovement) : Super(ClientMovement) {

}

FSavedMovePtr UMWCharacterMovementComponent::FNetworkPredictionData_Client_MWCharacter::AllocateNewMove() {
    return FSavedMovePtr(new FSavedMove_MonsterWars_Character);
}

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "../Components/Movement/MWCharacterMovementComponent.h"
#include "MonsterWarsCharacter.generated.h"


UCLASS(config=Game)
class AMonsterWarsCharacter : public ACharacter
{
	GENERATED_BODY()

	/** Camera boom positioning the camera behind the character */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class USpringArmComponent* CameraBoom;

	/** Follow camera */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class UCameraComponent* FollowCamera;

	/** Jump Input Action */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	class UInputAction* JumpAction;

	/** Move Input Action */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	class UInputAction* MoveAction;

	/** Look Input Action */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	class UInputAction* LookAction;

    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
    class UInputAction* RunAction;



protected:
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Movement)
    class UMWCharacterMovementComponent* CustomMovementComponent;
    

    
public:
	AMonsterWarsCharacter(const FObjectInitializer& ObjectInitializer);
    virtual void Jump() override;

    virtual void Landed(const FHitResult& Hit) override;
	    
	/** Called for movement input */
	void Move(const FInputActionValue& Value);
    void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode);
    void StopMove(const FInputActionValue& Value);

	/** Called for looking input */
	void Look(const FInputActionValue& Value);
    
    void Run(const FInputActionValue& Value);

    void StopRun(const FInputActionValue& Value);

    void AdjustJumpingRotation();

    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty> & OutLifetimeProps) const override;

protected:
	// APawn interface
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

    virtual void Tick(float DeltaTime) override;
	
	// To add mapping context
	virtual void BeginPlay();

public:
	/** Returns CameraBoom subobject **/
	FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
	/** Returns FollowCamera subobject **/
	FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};

#include "MonsterWarsCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
//#include "Kismet/GameplayStatics.h"
#include "Net/UnrealNetwork.h"


//////////////////////////////////////////////////////////////////////////
// AMonsterWarsCharacter

AMonsterWarsCharacter::AMonsterWarsCharacter(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer.SetDefaultSubobjectClass<UMWCharacterMovementComponent>(ACharacter::CharacterMovementComponentName)){
	// Set size for collision capsule

    CustomMovementComponent = Cast<UMWCharacterMovementComponent>(GetCharacterMovement());
	GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
		
	// Don't rotate when the controller rotates. Let that just affect the camera.
	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	// Configure character movement
	//GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input...	
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f); // ...at this rotation rate

	// Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint
	// instead of recompiling to adjust them
	GetCharacterMovement()->JumpZVelocity = 700.f;
	GetCharacterMovement()->AirControl = 0.35f;
	GetCharacterMovement()->MaxWalkSpeed = 300.f;
	GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
	GetCharacterMovement()->BrakingDecelerationWalking = 100.f;
    GetCharacterMovement()->PerchAdditionalHeight = 10.0f;
    

	// Create a camera boom (pulls in towards the player if there is a collision)
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->SetupAttachment(RootComponent);
	CameraBoom->TargetArmLength = 400.0f; // The camera follows at this distance behind the character	
	CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller

	// Create a follow camera
	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
	FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation
	FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm

    this->bReplicates = true;
    this->SetReplicates(true);
    this->SetReplicateMovement(true);

}

void AMonsterWarsCharacter::BeginPlay() {
	Super::BeginPlay();

    if (GetLocalRole() != ROLE_Authority) {
        UE_LOG(LogTemp, Warning, TEXT("CHARACTER_START_CLIENT"));
    } else {
        UE_LOG(LogTemp, Warning, TEXT("CHARACTER_START_SERVER"));
    }
}

void AMonsterWarsCharacter::Tick(float DeltaTime) {
    Super::Tick(DeltaTime);
}

void AMonsterWarsCharacter::AdjustJumpingRotation() {

}

void AMonsterWarsCharacter::Jump() {
    Super::Jump();


}

void AMonsterWarsCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode) {
    Super::OnMovementModeChanged(PrevMovementMode, PreviousCustomMode);
    switch(GetCharacterMovement()->MovementMode) {

        case 1:
            GetCharacterMovement()->bOrientRotationToMovement = false;
            bUseControllerRotationYaw = true;
            GetCharacterMovement()->RotationRate = FRotator(0.f, 0.f, 0.f); // Adjust as needed

            break;
        case 3:
            //TODO: Make this only able to happen between 45 degree offsets from camera forward position.
            GetCharacterMovement()->bOrientRotationToMovement = true;
            bUseControllerRotationYaw = false;
            GetCharacterMovement()->RotationRate = FRotator(0.f, 150.f, 0.f); // Adjust as needed

            break;
    }
}

void AMonsterWarsCharacter::Landed(const FHitResult& Hit) {
    Super::Landed(Hit);

    if (GetLocalRole() != ROLE_Authority) {
        //UE_LOG(LogTemp, Warning, TEXT("CLIENT: HasLanded"));
    } else {
        //UE_LOG(LogTemp, Warning, TEXT("SERVER: HasLanded"));
    }
}

void AMonsterWarsCharacter::Move(const FInputActionValue& Value) {

    FVector2D MovementVector = Value.Get<FVector2D>();
	if (Controller != nullptr) {        
		// find out which way is forward
		const FRotator Rotation = Controller->GetControlRotation();
		const FRotator YawRotation(0, Rotation.Yaw, 0);

		// get forward vector
		const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
	
		// get right vector 
		const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

		// add movement 
		AddMovementInput(ForwardDirection, MovementVector.Y);
		AddMovementInput(RightDirection, MovementVector.X);
	}

}




void AMonsterWarsCharacter::StopMove(const FInputActionValue& Value) {

}

void AMonsterWarsCharacter::Run(const FInputActionValue& Value) {

    CustomMovementComponent->SprintPressed(); 

}

void AMonsterWarsCharacter::StopRun(const FInputActionValue& Value) {

    CustomMovementComponent->SprintReleased();
}

void AMonsterWarsCharacter::Look(const FInputActionValue& Value) {
	// input is a Vector2D
	FVector2D LookAxisVector = Value.Get<FVector2D>();

	if (Controller != nullptr) {
		// add yaw and pitch input to controller
		AddControllerYawInput(LookAxisVector.X);
		AddControllerPitchInput(LookAxisVector.Y);
	}
}

void AMonsterWarsCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) {
	// Set up action bindings
	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent)) {
		
		//Jumping
		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);

		//Moving
		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AMonsterWarsCharacter::Move);
		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &AMonsterWarsCharacter::StopMove);

        //Running
        EnhancedInputComponent->BindAction(RunAction, ETriggerEvent::Started, this, &AMonsterWarsCharacter::Run);
		EnhancedInputComponent->BindAction(RunAction, ETriggerEvent::Completed, this, &AMonsterWarsCharacter::StopRun);

		//Looking
		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AMonsterWarsCharacter::Look);
	}
}


void AMonsterWarsCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> & OutLifetimeProps) const {
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    
}

Out of curiosity why are you developing your own movement component? I can see the academic side of it.

Character Movement Component (CMC) is robust and the standard. But there is a new Mover Component in development. It’s experimental, but will be replacing CMC in the future.

Im just trying to get the basics of a multiplayer game functional and working so that I can start to expand outward with the development process. I want to learn the right way to implement the multiplayer movement systems to get as good of possible network performance that I can, with proper client/ remote server architecture and code practice.

That’s pretty much handled in Mover and CMC. More so in Mover. Combine that with IRIS and you get the best of both.

I was really hoping not to have to rely on anything experimental for this honestly. I mean thousands of games before mine have had multiplayer without too many hitches in unreal. Im just trying to figure out at this point why my snowflake set up is so special edge cased at this point.

Those other games use the battle hardened CMC.

Can always strip down CMC and see how they handle prediction and corrections. There’s also the overview.

1 Like