Weird replication problem if replicated property set to class default

I’m having a weird issue with replication and wondered if it’s just because I’m doing something unexpected that you’re not supposed to.

To sum it up in a TLDR;

Replication doesn’t seem to work if an actor’s property is set to non-default in the editor, and then changed to the default when the game starts.

I’ve made a super basic example class:



#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ReplicationTest.generated.h"

UCLASS( ClassGroup=(TestActor), Blueprintable, BlueprintType)
class MVENGINE_API AReplicationTest : public AActor
{
    GENERATED_BODY()

public:
    // Sets default values for this actor's properties
    AReplicationTest();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

    // Replication notification for locking
    UFUNCTION()
    virtual void OnRep_Replication();

public:
    UPROPERTY(ReplicatedUsing=OnRep_Replication, BlueprintReadWrite, EditAnywhere)
    int32 ReplicationTest = 0;
    UPROPERTY(BlueprintReadOnly, EditAnywhere)
    USceneComponent* Root;
};


With an implementation that lets me debug things:



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

// Sets default values
AReplicationTest::AReplicationTest()
{
    UE_LOG(LogTemp, Display, TEXT("%s [Client %i] %s (ReplicationTest: %i)"), __FUNCTIONW__, GPlayInEditorID, *GetName(), ReplicationTest);
    Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    RootComponent = Root;
    SetReplicates(true);
}

void AReplicationTest::BeginPlay()
{
    UE_LOG(LogTemp, Display, TEXT("%s [Client %i] %s (ReplicationTest: %i)"), __FUNCTIONW__, GPlayInEditorID, *GetName(), ReplicationTest);
    Super::BeginPlay();
}

void AReplicationTest::OnRep_Replication()
{
    UE_LOG(LogTemp, Display, TEXT("%s [Client %i] %s (ReplicationTest: %i)"), __FUNCTIONW__, GPlayInEditorID, *GetName(), ReplicationTest);
}

void AReplicationTest::GetLifetimeReplicatedProps(TArray< FLifetimeProperty >& OutLifetimeProps) const
{
    UE_LOG(LogTemp, Display, TEXT("%s [Client %i] %s (ReplicationTest: %i)"), __FUNCTIONW__, GPlayInEditorID, *GetName(), ReplicationTest);
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(AReplicationTest, ReplicationTest);
}


I’ve then added three instances of this actor to a map, **ReplicationValue0, **ReplicationValue1 and ReplicationValue2 and I set their ‘ReplicationTest’ values in the editor, Details panel to 0, 1 and 2 respectively.

Next I create a level blueprint to do this on just the server:

and I run this as a dedicated server and a single player in editor, Everything works as expected - here’s the console with a filter for anything with ‘AReplicationTest’



LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue1 (ReplicationTest: 1)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue2 (ReplicationTest: 2)
LogBlueprintUserMessages: [ReplicationTest_C_1] Server: AReplicationTest setup
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 1] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 2] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue2 (ReplicationTest: 5)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue1 (ReplicationTest: 4)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue0 (ReplicationTest: 3)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue0 (ReplicationTest: 3)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue1 (ReplicationTest: 4)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue2 (ReplicationTest: 5)


You can see that the new player (Client 2) that joins the server (Client 1) receives the three OnRep_Replication’s before BeginPlay as the values have changed on the server. The client reports the correct 3, 4, 5 values for the respective AReplicationTest actors.

However, I then change the blueprint to this:

What I’m expecting to happen is that the all of the actor’s ReplicationTest values are set to 0 on the server, and when the client joins it receives two OnRep_Replication’s after the values are set to 0 (for the two that had ReplicationTest set to 1 and 2). However, here’s the log from that run:



LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue1 (ReplicationTest: 1)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue2 (ReplicationTest: 2)
LogBlueprintUserMessages: [ReplicationTest_C_3] Server: AReplicationTest setup
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 1] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 2] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue1 (ReplicationTest: 1)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue2 (ReplicationTest: 2)


You can see here that OnRep_Replication is never called on the client, and it never receives the values being set to 0. There is now a difference between the server and client, as the three actors have ReplicationTest’s of 0,0,0 on the server and 0,1,2 on the client.

Tweaking bits of code here and there to see if I could narrow down if I’m doing something, wrong, I noticed that if I change the class definition so that this:


int32 ReplicationTest = 0;

is this:


int32 ReplicationTest = -1;

and then run with the same blueprint above (setting all three to 0), the log shows this:



LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue2 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue1 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue0 (ReplicationTest: -1)
LogBlueprintUserMessages: [ReplicationTest_C_3] Server: AReplicationTest setup
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue2 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue1 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue0 (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 1] Default__ReplicationTest (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 2] Default__ReplicationTest (ReplicationTest: -1)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 2] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue0 (ReplicationTest: 0)


and everything works fine. The client receives all three OnRep_Replication’s and the values once again match on the server and client.

So it seems like replication-on-join fails to occur if the current value is equal to either the class default, or the value loaded from the editor. I can also confirm this same behaviour with blueprints.

I’ve tried this in both 4.23, 4.24 and 4.25 and it appears this behaviour happens in all.

Is this just correct, expected behaviour? is there something special I have to do when I’m replicating a value that’s set to the class/BP default value? Have I done something incredibly dumb somewhere in my code (I’m not that experienced with C++ replication so not 100% sure)?

I’ve attached a 4.25 blueprint version of this issue with the 3/4/5 and 0/0/0 example maps above:

Have you yet tried putting


bAlwaysRelevant = true;

in the actor’s constructor?

I have, but it didn’t seem to help.

It seems like the main problem is that an actor’s replication doesn’t occur at any point for joining clients if the actor’s property is set in the constructor, then changed in postloaded, then changed back in the beginplay.

Haven’t yet found a workaround for this, or any indication of what to do to fix it. Still scratching my head trying to find out if there’s a way to manually reset the replication ‘cache’ so that it knows the value has changed and should be sent to joining clients.

This is correct behaviour. The Server will only send the value if it believes the Client has an out-of-date value, and (by default) the Client will only call the OnRep function if the value it receives from the Server is different to the local value it currently has.

In C++, you can force the OnRep function to be called whenever the value is received from the Server even if it matches the clients’ local value. You can achieve this with the following macro. This will at least allow you to debug whether the Server is sending anything at all.



DOREPLIFETIME_CONDITION_NOTIFY(AReplicationTest, ReplicationTest, COND_None, REPNOTIFY_Always);


You can also add a parameter to the OnRep, which will be the Clients’ previous value, e.g:



UFUNCTION() void OnRep_MyInt(const int32 PreviousValue);


You can try comparing PreviousValue to Current Value when using REPNOTIFY_Always to see if the Server actually sent the property at all, but since the value you are changing it too matches the value it already has in editor at load time, the Server likely won’t send it.

Don’t forget that replicated properties are assessed for changes at the end of a frame. If you set a bool from false->true->false in the same frame, the Server won’t sent anything because it will determine that the value did not change at all.

I’d actually tried this and found that the OnRep function never seems to be called. If there’s nothing else I have to do with replication, it seems like something is not quite working right with replication. For example, in this scenario:

  1. Start the server,
  2. Wait any amount of time (I tried 30 seconds),
  3. Join a new client

The authority and client are still incorrect, and the onRep is never received by the client, even if notify condition is set to always


LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue2 (ReplicationTest: 2)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue1 (ReplicationTest: 1)
LogTemp: Display: AReplicationTest::BeginPlay [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogBlueprintUserMessages: [ReplicationTest_C_1] Server: AReplicationTest setup
LogTemp: Display: AReplicationTest::ResetReplication [Client 1] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::ResetReplication [Client 1] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::ResetReplication [Client 1] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AMVEngineGameModeBase::PostLogin [Client 1] AReplicationTest ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AMVEngineGameModeBase::PostLogin [Client 1] AReplicationTest ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AMVEngineGameModeBase::PostLogin [Client 1] AReplicationTest ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 1] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 2] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue2 (ReplicationTest: 2)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue1 (ReplicationTest: 1)
LogTemp: Display: AReplicationTest::BeginPlay [Client 2] ReplicationValue0 (ReplicationTest: 0)
LogBlueprintUserMessages: [ReplicationTest_C_1] Server: AReplicationTest Values are 0, 0, 0
LogBlueprintUserMessages: [ReplicationTest_C_1] Client 1: AReplicationTest Values are 0, 1, 2

LogTemp: Display: AReplicationTest::AReplicationTest [Client 3] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 3] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::AReplicationTest [Client 3] ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AMVEngineGameModeBase::PostLogin [Client 1] AReplicationTest ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AMVEngineGameModeBase::PostLogin [Client 1] AReplicationTest ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AMVEngineGameModeBase::PostLogin [Client 1] AReplicationTest ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 3] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 3] ReplicationValue2 (ReplicationTest: 2)
LogTemp: Display: AReplicationTest::BeginPlay [Client 3] ReplicationValue1 (ReplicationTest: 1)
LogTemp: Display: AReplicationTest::BeginPlay [Client 3] ReplicationValue0 (ReplicationTest: 0)
LogBlueprintUserMessages: [ReplicationTest_C_1] Client 1: AReplicationTest Values are 0, 1, 2


You can see above that even Client 3, which joins 30 seconds after ReplicationTest has been set to 0,0,0 for all three instances of the actor, has values that do not much the server because it never receives a replication event.

The server logic seems to be ‘if Replicated value == Constructor-set-default then don’t send’, which sort of makes sense. The problem, though, is that the server also seems to check ‘if Replicated value == Loaded value PostLoad then don’t send’.

So the issue I’m finding is - if the server doesn’t send the replicated value on login to the client in the above situation, how does the client know if the value == Constructor-set-default or Editor-set-value? Right now in my example the client will not receive the onrep and will assume it’s the *Editor-set-value, *even though that means it differs from authority’s value. Or am I getting confused?


So a practical example in my game is locked doors. I have a notifying replicating value ‘bLocked’ which should be replicated. *bLocked *defaults to false. I then make a level with 10 doors. 5 of them have *bLocked *set to true in the map in the editor.

If I start a server like this, and join a client, no replications are sent. Both the client and server believe 5 doors have bLocked* as true***, 5 doors have bLocked as false, just as the umap specifies.

However, if I have the server ‘load’ a game before the client joins (say if I’m making a persistent world where the door’s lock state is remembered begin sessions) and this load means that all 10 doors have bLocked set to false.

When the client joins this time, again no replications are sent. The server thinks all 10 doors have bLocked set to false from loading the game. However, the client now thinks that 5 doors are locked still (since it never received a replication and it loaded the values from the umap serialization before PostLoad).


Something interesting I’ve just found is that this whole problem only seems to happen if the server has changed the value from editor-set-serialized value, to class default in the first few seconds of the server being instantiated. For example:

  1. Start server. Set ReplicationTest to 0 in PostLoad. BeginPlay or the first Tick,
  2. Join a client.

Replication never occurs. The ReplicationTest values do not match between the client or server. Server says 0,0,0 and client says 0,1, 2.

However,

  1. Start a server. Wait 5 seconds. Set ReplicationTest to 0,
  2. Join a client.

in this instance, the client is sent the replications before BeginPlay and debug shows onRep is called. ReplicationTest is correct on the client (0,0,0) and server (0,0, 0).

For example, here’s my example above (Start server, then join Client 3 30 seconds later, as I did before, only this time I have set the ReplicatoinTest value using a 5 second timer rather than in BeginPlay:


LogTemp: Display: AMVEngineGameModeBase::PostLogin [Client 1] AReplicationTest ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AMVEngineGameModeBase::PostLogin [Client 1] AReplicationTest ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AMVEngineGameModeBase::PostLogin [Client 1] AReplicationTest ReplicationValue0 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::GetLifetimeReplicatedProps [Client 3] Default__ReplicationTest (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 3] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::OnRep_Replication [Client 3] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 3] ReplicationValue2 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 3] ReplicationValue1 (ReplicationTest: 0)
LogTemp: Display: AReplicationTest::BeginPlay [Client 3] ReplicationValue0 (ReplicationTest: 0)
LogBlueprintUserMessages: [ReplicationTest_C_8] Client 2: AReplicationTest Values are 0, 0, 0

The replication seems to be sent correctly this time, even though the only thing that has changed is that the value was changed 5 seconds after the game started, rather than in BeginPlay.

Could this be a bug related to the server and how replicated values are cached to diff them?

That makes sense to me, the Client and Server shouldn’t be following different paths in PostLoad (too early for that anyway) - so it’s effectively redundant data. The values you set in editor are serialized from the map, so the Server and Client both know what those values are and the Server doesn’t need to send them.

PostLoad is too early to be changing values Server-Side - the actor channel is created after that and thus so is the replication shadow data. It’s likely that changing properties that early means the shadow data contains those changes already. You may want to change properties later than that, such as PostInitializeComponents or BeginPlay maybe.

Also, map-startup actors are “spawned” client-side even though they are replicated - but they will not receive any network updates until they become network relevant like anything else. Once they become network relevant, the Server will create an actor channel and the channel will resolve to the already-existing actor. You can change this behaviour by settig bNetLoadClient to false, which IIRC destroys the actor client-side once loaded, and a new actor will be spawned just like a runtime-spawned replicated actor.

The issue I’m having is that the values do actually differ from the serialized map data. The serialized data has *ReplicationTest *for the three actors as 0,1 and 2. However, my BeginPlay on the server (or a Tick) then changes them to 0,0,0. If a client then joins after this point (when the values are 0,0,0 on the server), shouldn’t the server be telling the client ‘Hey two of these values differ from the serialised map that the client/server have! It’s 0,0 now’?

Right now nothing gets sent, even when I confirm that *ReplicationTest *for the three actors is 0,0,0 before the new client joins. Also, I can wait minutes and add a new client (using late join in the editor) and the server still doesn’t send the newly joining client any kind of replication data.

I’ve tried in both those functions and it doesn’t seem to change. The server still fails to send replication data to the client on join.

The only ways I’ve found so far to make this work as I’d expect is

  • On the server, wait 1-2 seconds after BeginPlay before changing ReplicationTest on the server. If this is done, any newly joined client will be sent replication events for the two changed ReplicationTests,
  • Have the server change the value of ReplicationTest to something else, and then back to 0 (1-2 seconds after the server has begun play). If the value has changed (say I change ReplicationTest to 5 at 1 second, and then 0 at 2 seconds, any newly joined clients are sent the replication events.

It just seems to be the situation where I change *ReplicationTest *too quickly after the server has started.

Investigating more, it doesn’t seem to be that the ReplicationTest isn’t sent because it’s the default value. The fact that waiting 1-2 seconds and then changing to 0 working means that it seems like the server is mistaking the data for redundant when it’s not. Afterall, if the map is serialized as 0,1,2 and the server doesn’t tell the client ‘it’s 0,0,0 now’ or ‘it’s 0,1,2 now’, how should the client know whether *ReplicationTest *should be the map serialized data, or the class default?

Just to confirm what I think is happening at the moment in a timeline:
[TABLE=“border: 1, cellpadding: 1, width: 100%”]

		**Server Starts**
	
	
		**Server **loads map. 3 x ReplicationActor. ReplicationTests are class default of 0 for all
	
	
		**Server **map-serialization recalled/duplicated. *ReplicationTest *for each is 0,1,2 respectively
	
	
		**Server **BeginPlay. *ReplicationTest *is [0,1,2]
	
	
		**Server **loads save-game. Save game means that ReplicationActor *ReplicationTests *set to 0,0,0 respectively
	
	
		*.
		. Wait 10 seconds to join a client
		.*
	
	
		**Client joins**
	
	
		**Client **loads map. 3 x ReplicationActor. ReplicationTests are class default of 0 for all
	
	
		**Client **map-serialization recalled/duplicated. *ReplicationTest *for each is 0,1,2 respectively
	
	
		**Client ***receives no replication events*
	
	
		**Client **BeginPlay. *ReplicationTest *is [0,1,2]
	
	
		.
		*. Wait until game time is 20 seconds
		.*
	
	
		Server print *ReplicationTest*: [0,0,0]
		Client print *ReplicationTest*: [0,1,2]

This is what I could get to work:
[TABLE=“border: 1, cellpadding: 1, width: 100%”]

		**Server Starts**
	
	
		**Server **loads map. 3 x ReplicationActor. ReplicationTests are class default of 0 for all
	
	
		**Server **map-serialization recalled/duplicated. *ReplicationTest *for each is 0,1,2 respectively
	
	
		**Server **BeginPlay. *ReplicationTest *is [0,1,2]
	
	
		*.
		.** Server** waits 1 second
		.*
	
	
		**Server **loads save-game. Save game means that ReplicationActor *ReplicationTests *set to 0,0,0 respectively
	
	
		*.
		. Wait 10 seconds to join a client
		.*
	
	
		**Client joins**
	
	
		**Client **loads map. 3 x ReplicationActor. ReplicationTests are class default of 0 for all
	
	
		**Client **map-serialization recalled/duplicated. *ReplicationTest *for each is 0,1,2 respectively
	
	
		**Client ***receives replication events for the 2 changed ReplicationActors. **ReplicationTest* of 0,0 respectively
	
	
		**Client **BeginPlay. *ReplicationTest *is [0,1,2]
	
	
		*.
		. Wait until game time is 20 seconds
		.*
	
	
		Server print *ReplicationTest*: [0,0,0]
		Client print *ReplicationTest*: [0,0,0]

What I’m expecting to happen is that even if the server has no 1 second wait, the replication events should be sent to the client on join if *ReplicationTest *differs from the serialized values.

could it be because override was not added?

virtual void OnRep_Replication() override;

or does this function not require that?

Not sure it can override, as the function *OnRep_Replication *doesn’t exist in the base class.

I found this in actor.ccp

void AActor::OnRep_Instigator() {}

its not doing nothing. guess no one has implemented it yet?

Default server behavior is that a FProperty which current value didn’t change (on server), since last replication cycle, will not be picked for a replication broadcast next frame; That also applies to members of a replicated structure (UE3/UDK sends the whole struct, UE4 picks a member value change).

Taking an example where ReplicationTest is set to 1 in the umap, is this caused because ReplicationTest is set to 0 in the constructor,1 when read from serialized and then 0 in the BeginPlay? (ie this all happening too fast/i the same frame and so the server thinks it never changed from 0 and doesn’t replicate)?

Is there any kind of workaround for this problem? This seems like a bit of a big flaw - as it stands I can’t figure out a way for the client to know if the value should be the constructor default value, or the map serialize value in this situation. There seems to be no way to tell? It seems less than ideal that replicated values differ on the client from the authority as soon as a player connects.

Is there a way to force a replication cycle, or force the caching of an FProperty for replication?

Some people make a struct with a byte flag or a boolean property then flip it true/false just to force a replication where nothing really changed.

Others just make a RPC call somewhere to force update values.

I had considered an RPC call, but it seemed like it would be redundant -

  • If i had a door that has bLocked as false in the umap, but true in the savegame, replication would work correctly and so I’d be sending an RPC on top of onrep,
  • With something like a ‘locked door’ actor, the server owns the actor and so I’d have to use NetMulticast. All the door, chest, etc. statuses would be sent again to every player every time someone joins the game.

I guess I could send an RPC to something like the PlayerController via *Client, Reliable *on BeginPlay so that only the new player receives it.

I’m not too keen on using a struct as it feels like it would make actor setup in the editor more obfuscated/complicated/hacky. RIght now I pop down a ‘Door’ actor and just tick a ‘Locked’ checkbox. A struct would look a bit messier. I’d also read bad things about structs in structs and so have avoided them so far where not needed (though this might be older UE4 versions). It sounds like this may be the only way to get around this, though - thanks for the suggestion!

The only other idea I had was to use a hidden enum for the door locked state that had ‘Default’, ‘Unlocked’ and ‘Locked’, but this too would result in redundant data being sent (ie. if a door is unlocked in the umap it would be sending this unlocked replication, even though the door is already unlocked on the client from the client umap serialization).

Perhaps I should rethink how I’m doing doors/chests/etc. so that it’s RPC based rather than property replication. It just seemed that property replication was perfect for something like this and what it should be used for.

I’ve submitted this as a bug. It feels like post-serialization/duplication should be the starting point for FProperty replication and not the constructor?

I doubt they will accept a bug report, this is part of core optimizations for development of Fortnite.

Surely it’s a bug, though, that replicated properties on the authority can be mismatched from the newly connecting clients? Doesn’t this defeat the point of replicated properties and undermine server authority?

It does - so I would file a bug report anyway.

Looking through again, I think I can spot a bug which I also ran into and took a while to track down. I’ve brought this up with Epic on UDN, and the advice I recieved from Epic was to NOT call SetReplicates() in the constructor, and instead set bReplicates = true; directly (also, be sure to call the parents’ constructor).

What happened was that SetReplicates() actually adds the actor to the networking system in it’s state there and then, but it somehow also caused the system to ignore any deserialized values after that.

I was setting Network Dormancy to DORM_Initial for example, but Replication Graph was adding it in DORM_Awake state (the parents’ default value). It took a while to work out why that was happening, but the callstack revealed that the actor was being added to the networking system before it was fully setup and deserialized.

It’s stupid I know. My suggestion to Epic was that the block/assert on calls to SetReplicates in the constructor because it can create all kinds of problems, but it doesn’t look like they’ve changed that yet.


RPC calls make no sense here, and it’s just a recipe for a buggy unmaintanable system if you have to use them to “fix” some other underlying issue, so I agree you shouldn’t use them. Properties should be used for anything that determines state.

That being said, sometimes structs with wrapping counters in place of bools can be useful to detect changes you would otherwise miss (e.g, a door opening and closing on same network frame), but that doesn’t seem like the issue here. There’s nothing wrong with struct replication BTW, it works fine.

Thanks for the suggestion! That didn’t seem to change anything, unfortunately. It does look like SetReplicate adds the actor and its state but that doesn’t seem to affect anything with this issue.

A nice easy workaround would be if I could just clear the ‘cache’ that the server replication is using to compare for changes (or force a property cache update). If I could clear it before loading a save game I’m pretty sure it would fix this problem. I’ve tried ForceNetUpdate, ForcePropertyCompare etc. but none of them seem to do much.

You can try to force a replication once your client has joined. Maybe something like:



if (myActor->GetNetDriver())
{
    for (UNetConnection* Connection : myActor->GetNetDriver()->ClientConnections)
    {
        UActorChannel* Channel = Connection->ActorChannels.FindRef(myActor);

        if​​​ (Channel != nullptr)
            Channel->ReplicateActor();
    }
}