How to Network Replicate UObjects in Unreal Engine

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?
  2. 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:

UCLASS()
class UNetworkedUObject : public UObject
{
	GENERATED_BODY()
}

Enabling UObject Replication

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

virtual 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:

// Declaration:
virtual bool CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack) override;

// Implementation:
bool UNetworkedUObject::CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack)
{
	bool bCallRemoteFunction = UObject::CallRemoteFunction(Function, Parms, OutParms, Stack);

	// Find the actor that owns this UObject
	// We need this to get it's net driver
	AActor* OuterActor = GetTypedOuter<AActor>();
	if (!IsValid(OuterActor)) return false;

	// Retrieve the NetDriver, which handles network replication
	UNetDriver* NetDriver = OuterActor->GetNetDriver();
	if (!IsValid(NetDriver)) return false;

	// Call the function on remote
	NetDriver->ProcessRemoteFunction(OuterActor, Function, Parms, OutParms, Stack, this);

	return bCallRemoteFunction;
}

Detailed Explanation

  • NetDriver: The 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, 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 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:

// Header:
virtual int32 GetFunctionCallspace(UFunction* Function, FFrame* Stack) override;

// Cpp:
int32 UNetworkedUObject::GetFunctionCallspace(UFunction* Function, FFrame* Stack)
{
	UObject* OuterActor = GetTypedOuter<AActor>();
	return (OuterActor ? OuterActor->GetFunctionCallspace(Function, Stack) : FunctionCallspace::Local);
}

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, 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.

Replicating the UObject (Optional but Recommended)

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

FORCEINLINE bool ReplicateToChannel(
    UActorChannel* Channel,
    FOutBunch*     Bunch,
    const FReplicationFlags* RepFlags
)
{
    return Channel && Channel->ReplicateSubobject(this, *Bunch, *RepFlags);
}

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 is convenient, you must still call ReplicateSubobject from the owning actor’s ReplicateSubobjects function. In practice, you override:

virtual bool ReplicateSubobjects(
    UActorChannel* Channel,
    FOutBunch*     Bunch,
    const FReplicationFlags* RepFlags
) override
{
    Super::ReplicateSubobjects(Channel, Bunch, RepFlags);

    MyUObject->ReplicateToChannel(Channel, Bunch, RepFlags);
    // or
    Channel->ReplicateSubobject(MyUObject, *Bunch, *RepFlags);
}

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

Replicating Arrays of UObjects (Optional but Recommended)

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

template<typename TObjectType>
static FORCEINLINE bool ReplicateArrayToChannel(
	UActorChannel* Channel,
	FOutBunch* Bunch,
	const FReplicationFlags* RepFlags,
	const TArray<TObjectType*>& Slots
)
{
	static_assert(TIsDerivedFrom<TObjectType, UObject>::IsDerived,
		"ReplicateArrayToChannel only works for UObject subclasses");

	if (!Channel) return false;

	bool bWroteAny = false;
	for (TObjectType* Slot : Slots)
	{
		if (Slot && Slot->ReplicateToChannel(Channel, Bunch, RepFlags))
		{
			bWroteAny = true;
		}
	}
	return bWroteAny;
}

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:

#pragma once

#include "CoreMinimal.h"
#include "Engine/ActorChannel.h"
#include "UObject/Object.h"
#include "NetworkedUObject.generated.h"

UCLASS()
class UNetworkedUObject : public UObject
{
	GENERATED_BODY()
public:

	virtual bool CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack) override;

	virtual int32 GetFunctionCallspace(UFunction* Function, FFrame* Stack) override;
	
	virtual bool IsSupportedForNetworking () const override { return true; };

	/** 
	 * Helper to replicate *this* subobject. 
	 * Compiler will inline it, so zero overhead vs. calling ReplicateSubobject directly. 
	 */
	FORCEINLINE bool ReplicateToChannel(
		UActorChannel*             Channel,
		FOutBunch*                 Bunch,
		const FReplicationFlags*   RepFlags
	)
	{
		// `this` is a UObject*, so calls the correct overload.
		return Channel && Channel->ReplicateSubobject(this, *Bunch, *RepFlags);
	}

	template<typename TObjectType>
	static FORCEINLINE bool ReplicateArrayToChannel(
		UActorChannel*                     Channel,
		FOutBunch*                         Bunch,
		const FReplicationFlags*           RepFlags,
		const TArray<TObjectType*>&        Slots
	)
	{
		static_assert(TIsDerivedFrom<TObjectType, UObject>::IsDerived,
					  "ReplicateArrayToChannel only works for UObject subclasses");

		if (!Channel) return false;

		bool bWroteAny = false;
		for (TObjectType* Slot : Slots)
		{
			if (Slot && Slot->ReplicateToChannel(Channel, Bunch, RepFlags))
			{
				bWroteAny = true;
			}
		}
		return bWroteAny;
	}
};

And now the implementation:

#include "NetworkedUObject.h"

#include "Engine/NetDriver.h"
#include "GameFramework/Actor.h"

bool UNetworkedUObject::CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack)
{
	bool bCallRemoteFunction = UObject::CallRemoteFunction(Function, Parms, OutParms, Stack);
	
	
	AActor* OuterActor = GetTypedOuter<AActor>();
	
	if(!IsValid(OuterActor)) return false;

	UNetDriver* NetDriver = OuterActor->GetNetDriver();
	if(!IsValid(NetDriver)) return false;

	NetDriver->ProcessRemoteFunction(OuterActor,Function,Parms,OutParms,Stack,this);

	return bCallRemoteFunction;
}

int32 UNetworkedUObject::GetFunctionCallspace(UFunction* Function, FFrame* Stack)
{
	UObject* OuterActor = GetTypedOuter<AActor>();

	return (OuterActor ? OuterActor->GetFunctionCallspace(Function, Stack) : FunctionCallspace::Local);
}

Leave a Reply