Technical blog

Jordon Brooks

Gameplay programming, Unreal Engine, and systems notes.

Posts - May 20, 2025

How to Network Replicate UObjects in Unreal Engine

7 min read -- … views

In Lunaris, we faced a significant design challenge: creating a flexible inventory system capable of supporting different item variations. Typically, inventory systems use an array of structs, each requiring identical properties. This limitation means every item would unnecessarily have properties like durability or food spoilage, even when irrelevant.

Our innovative solution was to base item slots on UObjects. However, this introduced two main concerns:

  1. Performance: Are UObjects heavier than structs?
    1. Replication: How can we network replicate UObjects, which Unreal Engine does not replicate by default?

Let’s address these concerns and explore the technical implementation step-by-step.

Are UObjects Heavy?

Technically, yes UObjects have slightly more overhead than structs, but practically, the difference is minimal. Unreal Engine routinely manages tens of thousands of short-lived UObjects, including components such as UInputAction, UInputMappingContext, and UI widgets, without significant performance issues. Thus, adding a manageable number of UObjects to an inventory system won’t noticeably impact performance.

Network Replicating UObjects: Step-by-Step

Let’s dive into how we network replicate UObjects using Unreal Engine’s replication system. Below is a clear, expanded line-by-line explanation of how we achieve this:

Create a Base Class for Networked UObjects

We start by creating a base class derived from UObject named UNetworkedUObject:

1UCLASS()
2class UNetworkedUObject : public UObject
3{
4    GENERATED_BODY()
5}

Enabling UObject Replication

UObjects are not replicated by default. To enable replication, we override IsSupportedForNetworking and simply return true:

1virtual bool IsSupportedForNetworking() const override { return true; };

Handling Remote Function Calls (RPCs)

Remote functions allow UObjects to invoke functions across network boundaries, crucial for multiplayer synchronization.

We override CallRemoteFunction to manually handle these calls:

 1// Declaration:
 2virtual bool CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack) override;
 3
 4// Implementation:
 5bool UNetworkedUObject::CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack)
 6{
 7    bool bCallRemoteFunction = UObject::CallRemoteFunction(Function, Parms, OutParms, Stack);
 8
 9    // Find the actor that owns this UObject
10    // We need this to get it's net driver
11    AActor* OuterActor = GetTypedOuter<AActor>();
12    if (!IsValid(OuterActor)) return false;
13
14    // Retrieve the NetDriver, which handles network replication
15    UNetDriver* NetDriver = OuterActor->GetNetDriver();
16    if (!IsValid(NetDriver)) return false;
17
18    // Call the function on remote
19    NetDriver->ProcessRemoteFunction(OuterActor, Function, Parms, OutParms, Stack, this);
20
21    return bCallRemoteFunction;
22}

Detailed Explanation

  • NetDriver: The [UNetDriver](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Engine/UNetDriver) is Unreal Engine’s core networking backbone. Each instance (one per active net connection or listen server) owns the low‑level sockets and is responsible for serialising outgoing data, deserialising incoming packets, and scheduling replication updates each frame. It tracks every remotely connected actor channel, decides which replicated properties or RPCs need to be sent, applies bandwidth and prioritisation rules, and guarantees reliability for Reliable functions. When we call [ProcessRemoteFunction](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Engine/UNetDriver/ProcessRemoteFunction?application_version=5.5), the NetDriver saves the function name and parameters into a network‐safe format, queues it on the correct actor channel, and ensures it arrives at the intended peer where it is re‑executed.
    • FFrame: [FFrame](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/CoreUObject/UObject/FFrame) represents a single stack frame inside Unreal’s VM (also referred to as the “UnrealScript VM bytecode executor”). Every time a reflected C++ or Blueprint function is invoked, the engine constructs an FFrame that stores a pointer to the byte‑code, local variables, the current node context, and (critically for RPCs) an iterator over the function’s parameters. When CallRemoteFunction is triggered, the same FFrame is forwarded so that the remote side can reconstruct the exact call with identical parameters and execution context, ensuring deterministic behaviour across the network.

Defining Function Callspace

We override GetFunctionCallspace to tell Unreal whether a function should execute locally or remotely:

1// Header:
2virtual int32 GetFunctionCallspace(UFunction* Function, FFrame* Stack) override;
3
4// Cpp:
5int32 UNetworkedUObject::GetFunctionCallspace(UFunction* Function, FFrame* Stack)
6{
7    UObject* OuterActor = GetTypedOuter<AActor>();
8    return (OuterActor ? OuterActor->GetFunctionCallspace(Function, Stack) : FunctionCallspace::Local);
9}

This delegation ensures proper execution context for networked functions.

Function Callspace determines where a function should execute (locally on the owning machine, remotely on clients, or on the server) and is critical for Unreal’s RPC dispatch. By overriding [GetFunctionCallspace](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/CoreUObject/UObject/UObject/GetFunctionCallspace?application_version=5.5), we explicitly route calls based on ownership and network authority. When a replicated RPC is called on the networked objects, Unreal queries this method. If it returns FunctionCallspace::Local, the engine will package the call and send it to the client; if it returns FunctionCallspace::Remote, the call dispatches to the server side. Without the correct callspace, your RPCs could run in the wrong context or most likely silently dropped, leading to inconsistent gameplay state across players.

We create an inline helper that wraps ReplicateSubobject to save boiler‑plate when you need to push a single UObject over the wire:

1FORCEINLINE bool ReplicateToChannel(
2    UActorChannel* Channel,
3    FOutBunch*     Bunch,
4    const FReplicationFlags* RepFlags
5)
6{
7    return Channel && Channel->ReplicateSubobject(this, *Bunch, *RepFlags);
8}

ReplicateSubobject is an engine‑provided method on UActorChannel that serializes a subobject’s replicated properties and queued RPC calls into the actor’s replication stream. Placing this call inside your actor’s ReplicateSubobjects function tells Unreal’s networking layer to include that UObject in network updates. Without calling ReplicateSubobject, the engine’s default replication pipeline will ignore your UObject entirely, and none of its state or remote functions would be transmitted to clients.

Although [ReplicateToChannel](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Engine/UActorChannel/ReplicateSubobject/1?application_version=5.5) is convenient, you must still call [ReplicateSubobject](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/Engine/UActorChannel/ReplicateSubobject/1?application_version=5.5) from the owning actor’s [ReplicateSubobjects](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/GameFramework/AActor/ReplicateSubobjects?application_version=5.5) function. In practice, you override:

 1virtual bool ReplicateSubobjects(
 2    UActorChannel* Channel,
 3    FOutBunch*     Bunch,
 4    const FReplicationFlags* RepFlags
 5) override
 6{
 7    Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
 8
 9    MyUObject->ReplicateToChannel(Channel, Bunch, RepFlags);
10    // or
11    Channel->ReplicateSubobject(MyUObject, *Bunch, *RepFlags);
12}

This approach keeps replication code readable while still leveraging the engine’s required ReplicateSubobject pipeline.

To replicate entire arrays of UObjects, use the following template:

 1template<typename TObjectType>
 2static FORCEINLINE bool ReplicateArrayToChannel(
 3    UActorChannel* Channel,
 4    FOutBunch* Bunch,
 5    const FReplicationFlags* RepFlags,
 6    const TArray<TObjectType*>& Slots
 7)
 8{
 9    static_assert(TIsDerivedFrom<TObjectType, UObject>::IsDerived,
10        "ReplicateArrayToChannel only works for UObject subclasses");
11
12    if (!Channel) return false;
13
14    bool bWroteAny = false;
15    for (TObjectType* Slot : Slots)
16    {
17        if (Slot && Slot->ReplicateToChannel(Channel, Bunch, RepFlags))
18        {
19            bWroteAny = true;
20        }
21    }
22    return bWroteAny;
23}

This simplifies array replication and ensures consistent replication logic across your UObjects. For a deep dive into UActorChannel::ReplicateSubobject, see the Unreal Engine API reference:

Conclusion

By following these detailed steps, we achieve efficient and flexible replication of UObjects. This method enables customizable inventory systems in Lunaris without unnecessary overhead, streamlining multiplayer functionality.

We hope this guide helps you replicate UObjects effectively in your Unreal Engine projects. Stay tuned for more insights and detailed guides on advanced Unreal Engine features!

For those looking to streamline their development pipeline beyond replication, consider these resources:

Stay tuned for more insights and detailed guides on advanced Unreal Engine features!

Here’s the full code below:

Header:

 1#pragma once
 2
 3#include "CoreMinimal.h"
 4#include "Engine/ActorChannel.h"
 5#include "UObject/Object.h"
 6#include "NetworkedUObject.generated.h"
 7
 8UCLASS()
 9class UNetworkedUObject : public UObject
10{
11    GENERATED_BODY()
12public:
13
14    virtual bool CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack) override;
15
16    virtual int32 GetFunctionCallspace(UFunction* Function, FFrame* Stack) override;
17
18    virtual bool IsSupportedForNetworking () const override { return true; };
19
20    /**
21     * Helper to replicate *this* subobject.
22     * Compiler will inline it, so zero overhead vs. calling ReplicateSubobject directly.
23     */
24    FORCEINLINE bool ReplicateToChannel(
25        UActorChannel*             Channel,
26        FOutBunch*                 Bunch,
27        const FReplicationFlags*   RepFlags
28    )
29    {
30        // `this` is a UObject*, so calls the correct overload.
31        return Channel && Channel->ReplicateSubobject(this, *Bunch, *RepFlags);
32    }
33
34    template<typename TObjectType>
35    static FORCEINLINE bool ReplicateArrayToChannel(
36        UActorChannel*                     Channel,
37        FOutBunch*                         Bunch,
38        const FReplicationFlags*           RepFlags,
39        const TArray<TObjectType*>&        Slots
40    )
41    {
42        static_assert(TIsDerivedFrom<TObjectType, UObject>::IsDerived,
43                      "ReplicateArrayToChannel only works for UObject subclasses");
44
45        if (!Channel) return false;
46
47        bool bWroteAny = false;
48        for (TObjectType* Slot : Slots)
49        {
50            if (Slot && Slot->ReplicateToChannel(Channel, Bunch, RepFlags))
51            {
52                bWroteAny = true;
53            }
54        }
55        return bWroteAny;
56    }
57};

And now the implementation:

 1#include "NetworkedUObject.h"
 2
 3#include "Engine/NetDriver.h"
 4#include "GameFramework/Actor.h"
 5
 6bool UNetworkedUObject::CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack)
 7{
 8    bool bCallRemoteFunction = UObject::CallRemoteFunction(Function, Parms, OutParms, Stack);
 9
10    AActor* OuterActor = GetTypedOuter<AActor>();
11
12    if(!IsValid(OuterActor)) return false;
13
14    UNetDriver* NetDriver = OuterActor->GetNetDriver();
15    if(!IsValid(NetDriver)) return false;
16
17    NetDriver->ProcessRemoteFunction(OuterActor,Function,Parms,OutParms,Stack,this);
18
19    return bCallRemoteFunction;
20}
21
22int32 UNetworkedUObject::GetFunctionCallspace(UFunction* Function, FFrame* Stack)
23{
24    UObject* OuterActor = GetTypedOuter<AActor>();
25
26    return (OuterActor ? OuterActor->GetFunctionCallspace(Function, Stack) : FunctionCallspace::Local);
27}