Listen Server unexpected replication behavior

I’m experiencing some very strange behavior with replication while using a listen server and one additional client (for a total of two players), so I suspect that my understanding of replication in UE4 is somewhat flawed. To illustrate this, I have setup the following conditions (for the sake of simplicity, I will call the player who is running the listenServer player1, and I will call the player who is connecting to the listen server player2):
-in Player class (which extends ACharacter):



UPROPERTY(replicated) bool testvar = false;
UFUNCTION(Server, Reliable) void ServerSetTestvar(ASurvivalGameCharacter* player);


-in Player OnFire:



ServerSetTestvar(this);


-in Player Tick:



if (testvar)
Util::disp(this, "TESTVAR SET"); // custom screen print method


-ServerSetTestvar definition:



void ASurvivalGameCharacter::ServerSetTestvar_Implementation(ASurvivalGameCharacter* player) {
player->testvar = true;
}


In short, I have a boolean that I expect to print a statement repeatedly once it is set to true.
I tested this code under two circumstances:

  1. With no replication condition set for testvar in GetLifetimeReplicatedProps
  2. With testvar set to COND_SkipOwner in GetLifetimeReplicatedProps:


DOREPLIFETIME_CONDITION(ASurvivalGameCharacter, testvar, COND_SkipOwner);


For the first test (no replication condition):
Expectation:
-Upon triggering OnFire as player1: the real player1 on the host will print TESTVAR SET, and the simulated player1 on the client will also print TESTVAR SET
-Upon triggering OnFire as player2: the real player2 on the client will print TESTVAR SET, and the simulated player2 on the host will also print TESTVAR SET
Actual Result:
-Upon triggering OnFire as player1: only the real player1 on the host prints TESTVAR SET
-Upon triggering OnFire as player2: only the simulated player2 on the host prints TESTVAR SET

For the second test (COND_SkipOwner):
Expectation:
-Upon triggering OnFire as player1: only the simulated player1 on the client will print TESTVAR SET
-Upon triggering OnFire as playerr2: only the simulated player2 on the host will print TESTVAR SET
Actual Result:
-Upon triggering OnFire as player1: the real player1 on the host prints TESTVAR SET, and the simulated player1 on the client also prints TESTVAR SET
-Upon triggering OnFire as player2: only the simulated player2 on the host prints TESTVAR SET

As you can see, my expectations for variable replication do not match the actual results of my tests. What’s particularly unexpected is that COND_SkipOwner seemingly causes the host to replicate to the simulated player1, whereas I would strictly expect COND_SkipOwner to cause fewer replications.
Can anyone shed some light onto this issue? I feel like I must be misunderstanding something fundamental about replication in UE4, and have been racking my brain over this for some time now. Any help would be greatly appreciated.

Your expectations are correct but something in the way you execute the ServerSetTestvar_Implementation must be off.

I’m glad to hear that my head’s in the right place at least. For the sake of disambiguation, I threw the above replication test into a fresh repo: GitHub - rystills/UE4ReplicationTest: Be sure to run as a listen server with two players! Sample f with the test code at the bottom of AUE4ReplicationTestCharacter.h and AUE4ReplicationTestCharacter.cpp, preceded by /* REPLICATION TEST */. This should put us all on the same page, and make it easy to spot whatever I’m doing wrong.

Not entirely sure what it is you’re wanting to do, but you’re calling a Server RPC to set a variable which doesn’t replicate to the actors owner, but you’re not setting the variable locally.

This will give you different behaviour depending on who calls it.

The Listen Server

  1. Will call ServerSetTestVar() and set the variable to true.
  2. The ‘true’ state will replicate to all other players (it will also of course be true locally, it’s the Server).

Result: State will be true for everyone.

The Client

  1. Will call ServerSetTestVar(), the Server will then set the variable to true when it receives the call.
  2. The ‘true’ state will replicate to everybody but the owning player, because the property has COND_SkipOwner.

Result: State will be true for everyone apart from the owning client.

Yes I forgot that the listen server obviously set the variable no matter what. This is why listen-servers are more complicated to work with than dedicated servers in this regard.

I see, so the player who is hosting the listen server always replicates replicated variables, not just in ‘Server’ functions. With that knowledge, just as you described, the result when using COND_SkipOwner is what I would expect: when player1 shoots, both the real and simulated player1 set testvar to true, but when player2 shoots, only the simulated player2 sets testvar to true.
That said, I still don’t quite understand the result when not using a replication condition. With no replication condition, when either player shoots, only the server copy of that player sets testvar to true, whereas I would expect testvar to be replicated to the client machine as well in that scenario. This was a point of confusion for me before too; specifying COND_SkipOwner seemingly causes a replicated variable set by the host to replicate to the client machines, even though I would expect that replication to always occur. Is there more to replication conditions that I’m simply not seeing? You can see this unexpected behavior in the demo I linked above if you comment out the DOREPLIFETIME_CONDITION at the bottom of UE4ReplicationTestCharacter.cpp.

The reason you’re only seeing the Server copy of your players setting the boolean is because you’re only setting the variable on the Server. If for whatever reason you wanted to print this out on your local client, you would need to set the variable locally.

It’s extremely important that this distinction is made; the Server copy of the game’s state is the authoritative, true copy. This means that if I were a little punk using my CheatEngine hax, and I decided to set your testvar boolean to True just for fun, it wouldn’t matter because the Server still holds the authoritative state of the game. The other players in the game won’t care that my local client has decided to set it True if it is still, in fact, False.

This is all kind of contextual though, too… a trivial setup like this makes it harder to understand imo. I would suggest running your tests using something more practical, like CharacterMovement. Try setting different MaxWalkingSpeed values on Server vs. Client, for example. Observe what your local client sees, vs what the Server sees, vs what another client sees.

I did some more research and found that, contrary to what the docs led me to believe, variables marked as replicated don’t actually replicate unless you give them a DOREPLIFETIME_CONDITION. Applying a DOREPLIFETIME_CONDITION of COND_None to testvar causes it to get set on both the host and client version of the player that shot, as expected.

For a standard replicated property with no additional conditions, you can just use the more popular DOREPLIFETIME() macro. You don’t need to use DOREPLIFETIME_CONDITION macro unless you want to attach a custom condition (the default is COND_None).

COND_None is a usually only used explicitly when you want to be able to change the condition in child-classes, or when using a larger macro like DOREPLIFETIME_CONDITION_NOTIFY().

The COND_SkipOwner condition from the original code is the reason you were getting odd behaviour here (see my post above). For reference, COND_SkipOwner is usually used when you have a property that both the Server and Owning Client wish to modify and/or have authority over, but remote clients need to get their updates from the Server.

The ShooterGame template has an example of this in it’s weapon class. BTW as old as that template is, it’s still a highly valuable resource for studying multiplayer programming in UE4.