Where we are in the series: this is part 1 of six. We start with the question everyone gets wrong on their first multiplayer feature, which is "how do I just send this object to the client?"
The short version is that you don't. Unreal does not let you pick up a UObject, lob it over the socket, and have an identical copy land on the other side. That mental model comes from REST and message queues, where you serialise a blob and ship it. Unreal's replication is a state synchronisation system, not a transport for objects, and the sooner you internalise that distinction the less you will suffer.
Here is the plan for the six parts, so you know where this is going:
- This part: why a fat replicated
UObjectis the wrong shape, and what actually replicates. USTRUCTpayloads and exposing them to Blueprint.- Custom
NetSerialize, packing fields by hand to save bandwidth. FFastArraySerializer, replicating lists without resending the whole thing.- Reconstructing state on the client, RepNotify, and the Blueprint surface.
- A complete worked inventory example, plus the gotchas that bite in PIE.
what actually replicates
Replication in Unreal lives on the AActor. An actor flagged with bReplicates = true gets a channel on each relevant client's connection, and the server periodically diffs the actor's replicated properties against what it believes each client last acknowledged. Only the properties that changed get serialised, and only to clients for whom the actor is currently relevant. That last clause matters: replication is per-connection and relevance-filtered, which is why "just send the object" is meaningless. There is no single recipient.
A property only replicates if you opt it in. Two things are required. First, the UPROPERTY(Replicated) specifier:
UCLASS()
class APickup : public AActor
{
GENERATED_BODY()
public:
APickup();
UPROPERTY(Replicated)
int32 ChargesRemaining = 0;
protected:
virtual void GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
Second, you register the property in GetLifetimeReplicatedProps, which is where the engine learns which properties to track and under what conditions:
#include "Net/UnrealNetwork.h"
void APickup::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(APickup, ChargesRemaining);
}
And of course the actor has to actually replicate:
APickup::APickup()
{
bReplicates = true;
}
Forget either the UPROPERTY(Replicated) or the DOREPLIFETIME, and the property silently stays put on the server whilst the client shows whatever the default was. There is no compile error and no warning. This is the single most common "why won't it replicate" support question, and the answer is nearly always one of those two halves missing.
the diagram I wish I'd had on day one
sequenceDiagram
participant S as Server (authority)
participant C1 as Client A channel
participant C2 as Client B channel
S->>S: tick, diff replicated props
Note over S: ChargesRemaining changed 3 -> 2
S->>C1: delta: ChargesRemaining = 2
S->>C2: delta: ChargesRemaining = 2
Note over C2: Actor not relevant, nothing sent
C1->>C1: apply, fire OnRep if any
The server is the single source of truth. Clients receive deltas. There is no symmetric "object" being passed around, just a stream of property updates per connection, gated by relevance and priority.
why not a fat UObject
So why can't you make a big UInventoryData object with thirty UPROPERTY fields and replicate that? A few reasons, and they stack up.
UObjects are not network-addressable by default. Actors get replication channels; arbitrary UObjects do not. You can replicate a UObject as a subobject of an actor (via ReplicateSubobjects and registering it with the replication system), but it is fiddly, it carries the full cost of a tracked object on every connection, and it gives you a per-object channel overhead you usually do not want for what is really just a bag of data.
The bandwidth shape is wrong too. If your data is conceptually a value, a stat block, a damage event, an inventory line, then a separate replicated object means the engine tracks lifetime, reference, and relevance for a thing that has no independent existence. You pay object-graph costs for what should be a few bytes inline.
And the relevance and ownership story gets muddy. A replicated subobject inherits its owning actor's relevance, but now you are reasoning about two lifetimes instead of one. When the data is small and owned, a USTRUCT replicated inline as a property of the actor is simpler, cheaper, and easier to make correct.
The rule of thumb I have landed on after enough bruises: replicate actors when the thing has presence in the world and an independent lifetime, replicate structs when the thing is a value owned by an actor, and reach for replicated subobjects only when you genuinely need polymorphism or an independent reference identity on the client. Most "data object" problems are the middle case, and we will spend the next few parts making that case as efficient and as Blueprint-friendly as possible.
next
In part 2 we define an actual USTRUCT payload, make it a first-class Blueprint type, and look at exactly how it rides along inside an actor's replication. Later parts go under the hood with custom NetSerialize (part 3), delta arrays (part 4), client reconstruction (part 5), and a full inventory build (part 6).
Get the shape right first. Optimising the serialisation of the wrong abstraction is just polishing a mistake.