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.
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)
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?
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
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.
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.
#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
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.
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.