OnRep is not firing on Client side when running a Dedicated Server (4.26.2)

Hey,

I’m running two player client in PIE with a simulated dedicated server from PIE. Every locally controlled Pawn creates a replicated actor on the Server.

So I have two of that Actor on the server, but only one for each client, and the server is the owner of those actors.

Those actors have a replicated member which gets changed on the server, however, its OnRep only fires on the server.

.h stuff

UFUNCTION(BlueprintCallable, Server, Reliable)
void Server_SetTestMemberAuth(const int NewTest);
UPROPERTY(Transient, ReplicatedUsing=OnRep_Test)
int Test;
UFUNCTION()
void OnRep_Test() const;

.cpp stuff

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

	DOREPLIFETIME(AGrid, Test);
}
void AGrid::Server_SetTestMemberAuth_Implementation(const int NewTest)
{
	if (!HasAuthority())
	{
		return;
	}

	Test = NewTest;
	OnRep_Test();
}
void AGrid::OnRep_Test() const
{
	if (HasAuthority())
	{
		UE_LOG(LogTemp, Warning, TEXT("OnRep_Test: Server"));
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("OnRep_Test: Client"));
	}
}

I’ve tried the exact same setup in Blueprints, updating a replicated test int member from a server RPC, and that RepNotify runs on both clients and the server.

What am I missing here?

Do you do this by hand or you let Unreal handle this? Also does AGrid have replication enabled?

This is how I create the AGrid actor on the server:

UFUNCTION(Server, Reliable)
void Server_CreateGridAuth();
void ATowerDefensePawn::BeginPlay()
{
	Super::BeginPlay();

	if (IsLocallyControlled())
	{
		Server_CreateGridAuth();	
	}
}
void ATowerDefensePawn::Server_CreateGridAuth_Implementation()
{
	if (HasAuthority())
	{
		UWorld* World = GetWorld();
		if (World)
		{
			FActorSpawnParameters SpawnParameters;
			SpawnParameters.Owner = this;
			SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
			Grid = Cast<AGrid>(World->SpawnActor(GridClassToSpawn, &FTransform::Identity, SpawnParameters));
		}
	}
}

This is the constructor of my Grid, the Actor that I spawn:

AGrid::AGrid(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{	
	GridVisualizer = CreateDefaultSubobject<UGridVisualizerComponent>(TEXT("Grid Visualizer"));
	if (GridVisualizer)
	{
		RootComponent = GridVisualizer;
	}

	bReplicates = true;
	bAlwaysRelevant = true;

	NumColumns = 50;
	NumRows = 50;
	CellSize = 64;

	Selection.BottomLeftCell = INT_MIN;
	Selection.TopRightCell = INT_MIN;
}

EDIT: Also, whether or not it replicates, or its relevancy is not touched during runtime, or changed in the blueprint, this is the only place I’ve touched it. If I just double check the blueprint, relevancy and replicates boxes are ticked as they should be.

A few points:

The “if (HasAuthority())” checks in the _Implementation function are not needed. That will only ever execute on the authority, unless you are calling MyFunction_Implementation directly (which you shouldn’t be).

You have a race condition in BeginPlay. Using IsLocallyControlled() there depends on the Controller and it’s properties replicating before the pawn calls BeginPlay - which you can’t garauntee will occur 100% of the time. Anything that is dependant on the state/existence of another actor needs to be driven via OnRep callbacks. Handling this in AController::SetPawn() would be the best place. Better yet, I would just have the server spawn these things itself - I’m not sure why the client would specifically request it in this case.

The last point is that the OnRep will only fire if the server actually replicates something, and by default only if the received value is different from the local one. If you are setting ‘Test’ to the same value, or if the client has manipulated it locally, the OnRep will not fire.

1 Like

Hey Jamsh, thanks for the pointers, I have a few things I’d like to get back to you on.

The HasAuthority in the server functions will get removed, I thought they werent necessary, but I didnt know, so I didnt take a chance on it, thanks for clarifying it.

The reason Im telling the server to spawn it from my client is because I only want the owning client to get a Grid actor. Think of it like a PlayerController, all PlayerControllers exist on the Server, but only the PlayerController that is relevant for you exists on your Client, thats the behaviour Im after. However, I’m not at all sure that is what I have achieved, Im still quite new to networking even if I’ve worked in games for some time now.

I am calling

void AGrid::Server_SetTestMemberAuth_Implementation(const int NewTest)
{
	if (!HasAuthority())
	{
		return;
	}

	Test = NewTest;
	OnRep_Test();
}

every three seconds with a randomized int from Blueprint, and its OnRep only runs on the Server, and not on the Client. What confuses me so much is that I’ve done this exact Test implementation in the BP instead, and directly called that one instead of my C++ one from the same place with the same value, and it’s RepNotify runs on both client and server.

So if you only want a grid to be “relevant” to each client, the way to do that would be to make the grid actor only relevant to the owner (bRelevantOnlyOwner or something like that) - then spawn it server-side using the PlayerController as the owner. It doesn’t really matter who requests the server spawn something, that spawned actor will always be relevant and replicated to all players unless it’s setup not to be.

Why the OnRep isn’t firing I’m not sure, there’s no reason it shouldn’t be. I would check that the actor you are spawning is actually being spawned on the Server, and not individually by each client (if it’s spawned client-side, then the client does have authority).

Thanks for the pointers, I’ll look into making the spawning a bit better, hopefully that’s where my issue lies.

Hello again Jamsh,

Sorry to revive this thread like this, but I kind of put this in hold as I had my summer vacation and I postponed getting back at it for a few weeks after I started working again, but I’m hammering at it again.

I have since improved the spawning of the Grid from the Player like so:

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

	AttemptCreateGridAuth();
}
void ATowerDefensePawn::AttemptCreateGridAuth()
{
	if (HasAuthority())
	{
		Server_CreateGrid();	
	}	
}
.h:
UFUNCTION(Server, Reliable)
void Server_CreateGrid();

.cpp
void ATowerDefensePawn::Server_CreateGrid_Implementation()
{
	UWorld* World = GetWorld();
	if (World)
	{
		FActorSpawnParameters SpawnParameters;
		SpawnParameters.Owner = this;
		SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
		Grid = Cast<AGrid>(World->SpawnActor(GridClassToSpawn, &FTransform::Identity, SpawnParameters));
	}
}

I still have the exact same problem, hence why I’m reviving this thread, after changing the value on the server, the OnRep does not run on the client, only on the server.

The variable I’m trying to replicate and fire an OnRep from is my Selection, which is a USTRUCT I made that looks like this:

USTRUCT()
struct FGridSelection
{
	GENERATED_BODY()

	FGridSelection()
	{
		
	}
	
	FGridSelection(const FIntPoint& BottomLeft, const FIntPoint& TopRight)
	{
		BottomLeftCell = BottomLeft;
		TopRightCell = TopRight;
	}
	
	// Will be INT_MIN if we don't have a selection
	FIntPoint BottomLeftCell;
	FIntPoint TopRightCell;
};

Here is the declaration of it in the Grids’ .h:

private:
	
	UPROPERTY(Transient, ReplicatedUsing=OnRep_Selection)
	FGridSelection Selection;
	UFUNCTION()
	void OnRep_Selection() const;

Here are the two functions which are called from BP, which are not firing the OnRep on the Client, but on the Server only:

void AGrid::Server_ClearGridSelection_Implementation()
{
	Selection = FGridSelection(INT_MIN, INT_MIN);
	OnRep_Selection();
}

void AGrid::Server_SetGridSelection_Implementation(const FIntPoint& BottomLeftCell, const FIntPoint& TopRightCell)
{
	if (!IsCellIdValid(BottomLeftCell) || !IsCellIdValid(TopRightCell))
	{
		return;
	}

	// If bottom left cell is further up or to the right of the top right cell.
	if (BottomLeftCell.X > TopRightCell.X || BottomLeftCell.Y > TopRightCell.Y)
	{
		UE_LOG(LogTemp, Warning, TEXT("Trying to Set Grid Selection when BottomLeftCell param is above or to the right of TopRightCell param"));
		return;
	}
	
	UE_LOG(LogTemp, Warning, TEXT("Server_SetGridSelection: %s"), *ULogHelper::GetActorNetRoleString(this));
	Selection = FGridSelection(BottomLeftCell, TopRightCell);
	OnRep_Selection();
}

Worth noting that the Id is correct, and the LeftCell is below the TopRight cell, so that is not colliding with anything.

Here is the actual OnRep function itself

void AGrid::OnRep_Selection() const
{
	if (GridVisualizer)
	{
		UE_LOG(LogTemp, Warning, TEXT("OnRep_Selection: %s"), *ULogHelper::GetActorNetRoleString(this));
		
		const FVector2D& GridBottomLeftPos = GetPointFromAnchor(EGridAnchor::GA_BottomLeft);
		const FVector2D& GridTopRightPos = GetPointFromAnchor(EGridAnchor::GA_TopRight);
		
		// This works even if the BottomLeft and TopRight cell are the same cell, will just get different anchors of the same cell.
		const FVector2D& SelectionCellBottomLeftPos = GetPointFromCell(Selection.BottomLeftCell, EGridAnchor::GA_BottomLeft);
		const FVector2D& SelectionCellTopRightPos = GetPointFromCell(Selection.TopRightCell, EGridAnchor::GA_TopRight);
	
		GridVisualizer->SetSelection(GridBottomLeftPos, GridTopRightPos, SelectionCellBottomLeftPos, SelectionCellTopRightPos);	
	}
}

Finally here is the BP_Grid actor which, and this is the BeginPlay flow which is the only logic called from BP.

I’ve also used this log-help method to analyze if my code is running on server or client. You can see it used in the AGrid::Server_SetGridSelection_Implementation()

FString ULogHelper::GetActorNetRoleString(const AActor* Actor)
{
	const ENetMode Mode = Actor->GetNetMode();
	FString NetName;
	switch (Mode)
	{
	case ENetMode::NM_Client:
		NetName = TEXT("Client");
		break;
	case ENetMode::NM_Standalone:
		NetName = TEXT("Standalone");
		break;
	case ENetMode::NM_ListenServer:
		NetName = TEXT("Listen Server");
		break;
	case ENetMode::NM_DedicatedServer:
		NetName = TEXT("Dedicated Server");
		break;
	default: ;
	}

	return NetName;
}

I realize this is quite the comment, and I apologize about that, I just really don’t understand this problem, and I’d love to understand it. Hopefully you or anyone else can shed some light on this problem that I’m having, thanks!

So, I managed to solve this yesterday. The reason why my OnRep wasn’t firing on the client, was because the data type of my replicated variable was a custom made USTRUCT, and that USTRUCT’s members wasnt marked as UPROPERTY. Here is the dif that solved the entire problem:

From this:

USTRUCT()
struct FGridSelection
{
	GENERATED_BODY()

	FGridSelection()
	{
		
	}
	
	FGridSelection(const FIntPoint& BottomLeft, const FIntPoint& TopRight)
	{
		BottomLeftCell = BottomLeft;
		TopRightCell = TopRight;
	}
	
	// Will be INT_MIN if we don't have a selection
	FIntPoint BottomLeftCell;
	FIntPoint TopRightCell;
};

To this:

USTRUCT()
struct FGridSelection
{
	GENERATED_BODY()

	FGridSelection()
	{
		
	}
	
	FGridSelection(const FIntPoint& BottomLeft, const FIntPoint& TopRight)
	{
		BottomLeftCell = BottomLeft;
		TopRightCell = TopRight;
	}
	
	// Will be INT_MIN if we don't have a selection
	UPROPERTY()
	FIntPoint BottomLeftCell;
	UPROPERTY()
	FIntPoint TopRightCell;
};

Ah at last, yeah that makes perfect sense, as only UPROPS’ can be replicated (unless you use a custom NetSerialize implementation)