Replicating variables

As simple as possible

Introduction

Replicating a variable in an Unreal Engine online setting, means that the server and every client connected to it, all have an identical copy of that variable. And whenever the server changes the value of it’s copy, the clients change to that same value on their copies. That’s it.

Yup, apparently there is a lot more going on, but hopefully you get the basic idea. From now on I will be using the words “variable” and “property” interchangeably, as they are essentially the same thing; members of a replicated UE object whose values need to be kept in sync across all clients and server.

A second glance into variable replication.

First off, it’s crucial to note that only variables within classes that inherently support replication, like replicated AActor and UActorComponent, can be replicated. UObject itself does not directly support replication but can be part of the replicated state if it is referenced by a replicated property in a AActor or UActorComponent.

Now, each replicated AActor has an associated UActorChannel that serves as the “conduit” for sending replication data between the server and clients. This channel is not only responsible for replicating the AActor‘s own variables but also those of its associated UActorComponents.

Replicated member variables of that AActor are transmitted through this channel as part of FRepLayout, which is a data part of a larger replication system, which in turn determines how the replicated properties of an AActor should be packed, serialized, and sent over the network.

Caveat warning:

Properties replicate from server to clients …as soon as possible.

Technically speaking, the server does not just replicate the variables down to clients automatically, in the exact time their value changes. Instead, it periodically checks each replicated AActor through their UActorChannel to see what variable needs replicating due to its value being changed. Now, how often “periodically” means is dictated by the AActor‘s NetUpdateFrequency.

Networks being what they are, with physical distances and all that, there is no way to guarantee that clients will pick up every single value change of a replicated property, if the server rapidly changes that value. One simple example is, changing replicated properties values on Tick(). Replicated properties on Tick() probably won’t have identical values to all clients at any given time.

And while changing the value of a replicated property on a client is possible, the server will override the client’s value of that property with the value of the server’s variable copy on next update (if no RPCs are used, but that’s a story for another day).

I won’t go any deeper than that at the moment, or claim to know every detail of that hyper-complex, albeit well designed system.

Instead I will try to provide more of a reference on how to replicate member variables (properties) of AActors in an online multiplayer setting, by declaring them as UPROPERTY() with the proper specifiers for each case.


UPROPERTY(Replicated)
The baseline way

Whenever a Replicated variable’s value is changed on the server, the server will replicate that change to all clients on next update. This is the simplest implementation, but it doesn’t scale that well for network efficiency, as projects get larger with hundreds of replicated properties.

Read the hyper-verbose commenting for usage:

C++
// MyActor.h ...

public:
  // Override GetLifetimeReplicatedProps to register replicated UPROPERTYs.
  void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override;
  
  // Replicated variable
  UPROPERTY(Replicated)
  int32 IntReplicated = 0;
  
// ...

C++
// MyActor.cpp ...

#include "Net/UnrealNetwork.h" // for DOREPLIFETIME macros

AMyActor::AMyActor()
{
    // Only replicated Actors can have their member variables replicated.
	bReplicates=true;
}

// ...

void AMyActor::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    DOREPLIFETIME(AMyActor, IntReplicated);
}

// ...


UPROPERTY(ReplicatedUsing = CallbackSignature)
Replicate with a callback

Same process (and caveats) as in Replicated will occur, only the function whose signature is defined after = will execute as well.

Read the hyper-verbose comments for usage:

C++
// MyActor.h ...

public:
  // Override GetLifetimeReplicatedProps to register replicated UPROPERTYs.
  void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override;

  // Replicated variable with a callback
  UPROPERTY(ReplicatedUsing=OnRep_IntReplicateUsing)
  int32 IntReplicateUsing;

  // It's common convention to name replication callback functions 'OnRep_<var_name>'.
  UFUNCTION()
  void OnRep_IntReplicateUsing();

// ...

C++
// MyActor.cpp ...

#include "Net/UnrealNetwork.h" // for DOREPLIFETIME macros

AMyActor::AMyActor()
{
    // Only replicated Actors can have their member variables replicated.
	bReplicates=true;
}

// ... rest of Actor.cpp

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

void AMyActor::OnRep_IntReplicateUsing()
{
    // Do things after variable replication, like UI updates.
}

// ... rest of Actor.cpp

UPROPERTY(ReplicatedUsing = CallbackSignature)
the PushModel version

Now I know this is going to contradict everything you ‘ve read so far but, when a PushModel Replicated variable is modified on the server, that modification will not replicate by default to clients on next update.

The reason for that is, post 4.27, Unreal Engine features a more complete PushModel, which reduces network traffic and server work load by truly replicating variables only when marked as “dirty” on the server.

Before that can be demonstrated though, we need to setup our project to use PushModel.

First we set our project to use PushModel in Config/DefaultEngine.ini:

INI
[SystemSettings]
net.IsPushModelEnabled=1

Then we link NetCore module to our project’s module in <Project_Name>.Build.cs.

C#
using UnrealBuildTool;

public class MyProject: ModuleRules
{
    public MyProject(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[]
        {
            "Core",
            "CoreUObject",
            "Engine",
            "InputCore",
            "NetCore"      // <-- for PushModel
        });

        PrivateDependencyModuleNames.AddRange(new string[]{ });
    }
}

From now on, it’s business as usual. This time we are going to need a test function to mark the replicated property as “dirty” in order to have it replicate to clients. Read the hyper-verbose comments for usage:

C++
// MyActor.h ...

public:
    // Override GetLifetimeReplicatedProps to register replicated UPROPERTYs.
    void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override;
   
    // Replicated
    UPROPERTY(ReplicatedUsing=OnRep_IntPushReplicateUsing)
    int32 IntPushReplicateUsing = 0;
   
    // It's common convention to name replication callback functions 'OnRep_<var_name>'.
    UFUNCTION()
    void OnRep_IntPushReplicateUsing();
    
    /**
	 * Simple function to test property replication
	 * @param Amount to add to the value of the replicated property
	 */
	UFUNCTION(BlueprintCallable)
	void AddValue(int32 delta);

// ...

C++
// MyActor.cpp ...

#include "Net/UnrealNetwork.h" // for DOREPLIFETIME macros

AMyActor::AMyActor()
{
    // Only replicated Actors can have their member variables replicated.
	bReplicates=true;
}

// ..

void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    // Contains several parameters that can be passed to
	// DOREPLIFETIME_WITH_PARAMS to control how variables are replicated.
	FDoRepLifetimeParams RepParams;

	// Set FDoRepLifetimeParams as Push-based 
	RepParams.bIsPushBased = true;
	RepParams.RepNotifyCondition = REPNOTIFY_OnChanged;
	DOREPLIFETIME_WITH_PARAMS_FAST(AMyActor, IntPushReplicateUsing, RepParams);
}

void AMyActor::OnRep_IntPushReplicateUsing()
{
    GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Blue,         FString::Printf(TEXT("IntPushReplicatedUsing = %d"), IntPushReplicateUsing), true);
}

void AMyActor::AddValue(int32 delta)
{
	if (HasAuthority())
	{
		if (delta != 0)
		{
			IntPushReplicateUsing+=delta;
			// Replicated variable should be marked as dirty in order to get replicated.
			MARK_PROPERTY_DIRTY_FROM_NAME(AMyActor, IntPushReplicateUsing, this);
		}
	}
}

// ...

Using the PushModel, variables that have changed are flagged immediately, and only these flagged variables are then sent to connected clients. This minimizes the amount of data that must be scanned, compared, and ultimately sent over the network; according to developers at Epic of course.

Keep in mind that this feature is still under heavy development and the methods of using the API are subject to change as Unreal Engine’s source is updated. That said, I will be updating this post accordingly.

Replicating ActorComponents

If the properties you need to replicate are member variables of a UActorComponent then you need to set the UActorComponent for replication with SetIsReplicatedByDefault(true); like this:

C++
UMyActorComponent::UMyActorComponent()
{
    // Only replicated ActorComponents can have their member variables replicated.
    SetIsReplicatedByDefault(true);
}

bReplicates variable is private for Actor Components and not meant to be used on Construction. The rest of the implementation remains the same as in AActor.

Conclusion

And that is basic property replication. It’s as simple as I can possibly give it, omitting further setup including replication conditions, Netupdatefrequency tweaking, relevancy and using the USignificanceManager. The aforementioned factors can all be used in conjunction to further optimize network load. Hopefully we can dive into those too later on.