Movement replication works on client, but not on host

I set up a basic C++ unreal project to iron out some troubles I have with multiplayer.

Now in this most basic of all multiplayer projects, I got everything to work, BUT

Regardless how I launch (two independent games, or PIE, or PIE with run-under-on-process), the hosting window doesn’t show the movement of the pawn of the connected client. The client window does show the replicated movement of the host pawn.

What am I doing wrong? Is this a bug? I feel, at this basic stage (movement replication, activated by ticking the box) I shouldn’t actually have to bother, whether or not I am executing server or client code. But I might be wrong.

bumping this.

Any opinions on this one? What can cause different behavior in the host and client window? I am only using the replicate check box, no other logic.

if fails on join session in the file mygameinstance.cpp on line 45

GetSubsystem()->JoinSession(LPC, [this, LPC] (FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{};

is that your result from running the example project?

This is what I get, when I launch this twice (hitting the launch.bat twice).

PIE works fine for me, too.

I then click on one of the windows and can move the respective Pawn with A and D key.

Moving the host pawn replicates to the client. (see left window below)

But moving the client pawn doesn’t replicate to the host (in the right window, the right pawn has moved, but in the left windows, the right pawn is still at its starting position)

let me push my code to make sure we are actually running the same code … EDIT: done commit: c9e3caf0

No, I tried a packed version first, then did a version from within the editor. The bat file is very specific to your pc drive config.

of course … it has all my paths. That is a bit annoying for quick testing

Running Standalone with “Number of Players” 2, works for me, too.

Be ware that one first clicks inside the window to capture the mouse and then on the host or join button. Clicking twice on the join button already causes problems.

Ok seems that it did connect after a couple of tries. I’ll have to look into how your possession logic works. Are you using agamemode PostLogin to spawn and posses your clients?

No. I just put Player Start Actors and I have a default pawn set. That’s it. Trying to be minimal.

Won’t work. You need to make a custom game mode based on AGameModeBase and override OnPostLogin.
Once OnPostLogin is called it will give you the new players controller.

You need to get a pointer to UWorld probably best through UGameplayStatics and spawn your player character at the start position (getActorofClass → startpoisiton → getTransform pass in the loc & rot).

Pass in the transform (with scale (1,1,1)) and once it spawns get the passed in controller to posses the newly spawned class. Then you will get control of it in MP

And set default pawn to none in your gamemode

I overwrote PostLogin like this:

UCLASS()
class TUTORIALMPBASICS_API AMyGameModeBase : public AGameModeBase
{
	GENERATED_BODY()

protected:
	// event handlers
	virtual void OnPostLogin(AController* NewPlayer) override;
	virtual void BeginPlay() override;

	// implementing my own default pawn class, to avoid auto spawning
	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	TSubclassOf<class AMyPawn> MyDefaultPawnClass;
};
void AMyGameModeBase::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);

	if(!IsValid(MyDefaultPawnClass))
	{
		return;
	}
	
	UWorld* World = GetWorld();

	TArray<AActor*> Starts;
	UGameplayStatics::GetAllActorsOfClass(World, APlayerStart::StaticClass(), Starts);

	UE_LOG(LogNet, Warning, TEXT("%s: OnPostLogin: %d/%d"), *GetFullName(), GetNumPlayers(), Starts.Num())
	
	APlayerStart* Start = Cast<APlayerStart>(Starts[GetNumPlayers() - 1]);
	AMyPawn* Pawn = World->SpawnActor<AMyPawn>(MyDefaultPawnClass, Start->GetActorTransform());
	NewPlayer->Possess(Pawn);
}

void AMyGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	
	if(!IsValid(MyDefaultPawnClass))
	{
		UE_LOG(LogGameMode, Error, TEXT("%s: DefaultPawnClass null"))
	}
}

Producing the exact same result. In the host window, the client pawn won’t move. In the client window, both host and client pawn move, when I hit A or D in their respective window.

Note that possessing of my pawns seemingly worked fine, even with the old code (relying on DefaultPawnClass and auto-spawn).

Yes the part i mentioned works as intended you forgot to add any form of replication into your pawn

You need to implement

AActor::GetLifetimeReplicatedProps for your pawn and pass in what variables you want to replicate.

Setting “replicate” and “replicate movement” in the BluePrint of the Pawn isn’t enough?

Also note that some sort of replication is going on. At least, I believe so. In the client window, I can see the movement of the host pawn AND the client pawn.

Once I deactivate “replicate movement”, I can see both balls in both windows, but the movement only of the locally controlled ball - as expected.

You are only changing the variable on client or server for velocity. If client you need to call a ufunction with the server macro and reliable. You need to check the logic if it’s authoritive (server) or not (client)
if client call server update function if server just update the velocity

I slowly come to the conclusion that the behavior I see is intended.

“Replicate movement” is specifically meant to work together with the Movement Component, i.e. using ACharacter rather than APawn to efficiently manage movement based on user input.

If I set my pawn to move (on every tick) given some velocity, independent of user input, both pawns move in both windows, i.e. they are replicated (by the power of checking “replicate” in the blueprint) including their location. This is with “replicate movement” unchecked.


Just adding custom movement to my pawn w/o the movement component has two consequences:

  • my custom movement doesn’t get replicated, and
  • with “replicate movement” checked, the host movement does get replicated but the client movement doesn’t and it shouldn’t: On the host, I have to evaluate the user input of the client to move the client-controlled pawn and then replicate that.

Thanks @3dRaven for your help!

pawn .h


#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "MyPawn.generated.h"

UCLASS()
class TUTORIALMPBASICS_API AMyPawn : public APawn
{
	GENERATED_BODY()

public:
	// Sets default values for this pawn's properties
	AMyPawn();

protected:
	// VisibleAnywhere on properties that are pointers to UObjects (like this one) does allow editing of its properties;
	// If you put "EditAnywhere" instead, you could change the type of Body (e.g. from UStaticMeshComponent to
	// USplineComponent), which isn't what you usually want
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	TObjectPtr<UStaticMeshComponent> Body;
	
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Replicated)
	FVector Velocity = FVector::Zero();

public:	
	void AccelerateLeft();
	void AccelerateRight();

	UFUNCTION(Server, reliable)
	void ServerAccelerateLeft();

	UFUNCTION(Server,reliable)
	void ServerAccelerateRight();

	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	//virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

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


};

cpp

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


#include "MyPawn/MyPawn.h"
#include "Net/UnrealNetwork.h"

// Sets default values
AMyPawn::AMyPawn()
{
 	// Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	SetReplicates(true);
	SetReplicateMovement(true);
	Body = CreateDefaultSubobject<UStaticMeshComponent>(FName(TEXT("Body")));
	SetRootComponent(Body);
}

// Called when the game starts or when spawned
void AMyPawn::BeginPlay()
{
	Super::BeginPlay();
}

void AMyPawn::AccelerateLeft()
{
	if (GetLocalRole() == ROLE_Authority) {
		Velocity += FVector(0, -10, 0);
	}
	else {
		ServerAccelerateLeft();
	}
}

void AMyPawn::AccelerateRight()
{
	if (GetLocalRole() == ROLE_Authority) {
		Velocity += FVector(0, 10, 0);
	} else {
		ServerAccelerateRight();
	}
}

void AMyPawn::ServerAccelerateRight_Implementation() {
	AccelerateRight();
}

void AMyPawn::ServerAccelerateLeft_Implementation() {
	AccelerateLeft();
}



// Called every frame
void AMyPawn::Tick(float DeltaTime)
{

	Super::Tick(DeltaTime);
	//if (GetLocalRole() == ROLE_Authority) {
		SetActorLocation(GetActorLocation() + Velocity * DeltaTime);
		const float Dampening = 10.;
		Velocity = Velocity.Length() < Dampening * DeltaTime ? FVector::Zero() : Velocity + Velocity.GetUnsafeNormal() * -Dampening * DeltaTime;
	//}
}

void AMyPawn::GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(AMyPawn, Velocity);
}

Not a fan of updating parameters on tick. It can cause desync between client and server

1 Like

Thanks a lot!

This is my code that finally works:

AMyPawn, not much going on there. Only

  • Replicate Movement: false, no need to replicate location or anything
  • Replicated property: velocity, this together with the tick function takes care of correct movement

Not a fan of updating parameters on tick. It can cause desync between client and server

This is valuable advice indeed. As this is just a multiplayer learning experience, I simply removed the dampening from the tick function such that velocity only ever changes when I hit a button. If I were to implement dampening and I just had to change the velocity in the tick function, I would chose not to replicate velocity and instead, I would use a client rpc to make sure AccelerateLeft and AccelerateRight get executed on the client.

And MyPlayerController.h and MyPlayerController.cpp

Two design choices here:

  • I created an enum for all the actions. This is unfortunately redundant with unreals action binding system, but I use the EAction enum in SetupInputComponent to define only once what will happen at both client and server. This way, I can’t accidently move left on the client and right on the server due to some copy-paste mishap
  • BindAction then takes care of registering both cases, server and client. This implies use of closures, otherwise I can’t use the EAction parameter. HandleAction is where stuff actually happens and ServerRPC_HandleAction only wraps that one.