Out-of-the-box, there are two primary types in Unreal Engine which have what we consider “first-class” support for the replication and networking system. These are Actors, and through them, Actor Components. For almost any conceivable situation, this is absolutely all we need. Both types support their own replicated properties, they can be safely created and destroyed at runtime on demand, and they support networked function calls, known as RPC’s.

One important difference, however, is the method of replication. The replication/RPC data is sent across the network via an ActorChannel – and as the name implies, these are only suitable for Actors. Channels can be created and destroyed at will (e.g, network relevancy, dormancy etc.) – but they are required to send network information from one connection to another.

ActorComponents and UObjects cannot create their own channels (or at least, the engine does not provide an implementation of this). This means that any object that needs to be created/destroyed via the network must piggyback off of an existing Actor and it’s channel. Thankfully, the engine provides such a solution, in the form of UObject::ReplicateSubobjects(). The Replication System will make calls to this function when considering an object for replication, and gives it a chance to append subobjects and their properties.

AActor provides a default implementation for replicating its components properties in ActorReplication.cpp. Components that are marked as replicated are maintained in a separate “ReplicatedComponents” list, which is then checked through during AActor::ReplicateSubobjects():

bool WroteSomething = false;

for (UActorComponent* ActorComp : ReplicatedComponents)
{
	if (ActorComp && ActorComp->GetIsReplicated())
	{
		WroteSomething |= ActorComp->ReplicateSubobjects(Channel, Bunch, RepFlags);
		WroteSomething |= Channel->ReplicateSubobject(ActorComp, *Bunch, *RepFlags);
	}
}

return WroteSomething;

We can borrow some of this implementation to add support for other custom UObject-derived types too. However, we also need to do some work on the Object side of things, as adding properties to the channel is not enough.

The Replicated UObject Class

Lets first create the base class for our new replicated UObject. The following is a suggested implementation, for a basic Actor Sub-Object. Some common useful helper functions have been added too, such as a getter for the UWorld and the AActor itself.

UCLASS()
class UMyAwesomeObject : public UObject
{
	GENERATED_BODY()
public:
	UMyAwesomeObject();
	
	// Allows the Object to get a valid UWorld from it's outer.
	virtual UWorld* GetWorld() const override
	{
		if (const UObject* MyOuter = GetOuter())
		{
			return MyOuter->GetWorld();
		}

		return nullptr;
	}
	
	UFUNCTION(BlueprintPure, Category = "My Object")
	AActor* GetOwningActor() const
	{
		return GetTypedOuter<AActor>();
	}
	
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override
	{
		// Add any Blueprint properties
		// This is not required if you do not want the class to be "Blueprintable"
		if (const UBlueprintGeneratedClass* BPClass = Cast<UBlueprintGeneratedClass>(GetClass()))
		{
			BPClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps);
		}
	}
	
	virtual bool IsSupportedForNetworking() const override
	{
		return true;
	}
	
	virtual int32 GetFunctionCallspace(UFunction* Function, FFrame* Stack) override
	{
		check(GetOuter() != nullptr);
		return GetOuter()->GetFunctionCallspace(Function, Stack);
	}
	
	// Call "Remote" (aka, RPC) functions through the actors NetDriver
	virtual bool CallRemoteFunction(UFunction* Function, void* Parms, struct FOutParmRec* OutParms, FFrame* Stack) override
	{
		check(!HasAnyFlags(RF_ClassDefaultObject));

		AActor* Owner = GetOwningActor();
		UNetDriver* NetDriver = Owner->GetNetDriver();
		if (NetDriver)
		{
			NetDriver->ProcessRemoteFunction(Owner, Function, Parms, OutParms, Stack, this);
			return true;
		}

		return false;
	}
	
	/*
	* Optional
	* Since this is a replicated object, typically only the Server should create and destroy these
	* Provide a custom destroy function to ensure these conditions are met.
	*/
	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "My Object")
	void Destroy()
	{
		if (!IsPendingKill())
		{
			checkf(GetOwningActor()->HasAuthority() == true, TEXT("Destroy:: Object does not have authority to destroy itself!"));
			
			OnDestroyed();
			MarkPendingKill();
		}
	}
	
protected:
	virtual void OnDestroyed()
	{
		// Notify Owner etc.
	}
};

That’s pretty much all there is too it, we have now created a UObject that when created with a valid ‘Actor’ as the outer, can replicate both native and Blueprint properties, as well as correctly route RPC’s through the parent actor.

Notice that we are still bound by the usual rules that apply to Multiplayer in UE4 – Replication of properties is as always from Server->Client, and if we want to reference these objects over the network – they must either be Default Subobjects, or created by the Server at runtime and allowed to replicate through the Actor Channel.

These objects will also, like components, share the Network Relevancy, Dormancy and Frequency properties of their owning Actor. There is no way around that – if you want an Object that is always network-relevant, it’s actor must be always relevant too. For most cases, using an Actor or a Component is still a preferred (and less engineered) method of getting data from one connection to another.

Adding to the Subobject List

Now that we have our Object, all we need to do is create one and tell the actor to replicate it. Some important points, the Outer of the object should *always* be the Actor – and the Actor needs to be able to gather a reference to it somehow. The following is a basic example for an actor that contains both a single object, and an array of objects:

UCLASS()
class AMyAwesomeActor : public AActor
{
	GENERATED_BODY()
public:
	AMyAwesomeActor(const FObjectInitializer& OI);
	
	virtual bool ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags) override
	{
		bool bWroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
		
		// Single Object
		bWroteSomething |= Channel->ReplicateSubobject(MyObject, *Bunch, *RepFlags);
		
		// Array of Objects
		bWroteSomething |= Channel->ReplicateSubobjectList(ArrayOfMyObject, *Bunch, *RepFlags);
		
		return bWroteSomething;
	}
	
	virtual void GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const override
	{
		Super::GetLifetimeReplicatedProps(OutLifetimeProps);
		
		// Replicating these properties is optional, but likely useful.
		DOREPLIFETIME(AMyAwesomeActor, MyObject);
		DOREPLIFETIME(AMyAwesomeActor, ArrayOfMyObject);
	}
	
protected:
	// Note: These properties do not neccesarily need to be replicated, but they likely would be in most cases.
	UPROPERTY(Replicated, BlueprintReadWrite, Category = "My Object")
	UMyAwesomeObject* MyObject;
	
	UPROPERTY(Replicated, BlueprintReadWrite, Category = "My Object")
	TArray<UMyAwesomeObject*> ArrayOfMyObject;
};

Like any network-relevant object, the lifespan of the object should be managed by the Server.

Note that the UPROPERTY() references to those objects (the array and the pointer) do not necccesarily need to be replicated, you could just as easily populate them in the same manner as AActor manages it’s internal list of ReplicatedComponents. Most of the time however, I suspect this will be wanted for most implementations.

Improving The System

We now have a reasonable system for replicating arbitrary Sub-Objects. Just like Components, these objects support all the usual UE4 networking features via the ActorChannel.

But there is one caveat to this implementation, and that is that we have to create a special Actor subclass to handle the replication of the Object. What can we do if we want to compartmentalise this into a common system that can be shared and used by any actor class? Imagine a scenario where you want to add an “Inventory” to an Actor, containing UObject-based Inventory “Slots”. Ultimately, this is the way the Hardpoint and Weapon system works in Project Orion.

Now it should be noted, that we still *always* want to ensure we create the UObjects using the Actor as the Outer. While ActorComponents can also provide an implementation for ReplicateSubobjects() – but importantly: subobjects are always created client-side using the Actor as the Outer.

I feel the need to put that in bold – because I learned the hard way that creating sub-objects with a non-actor outer can cause lots of obscure issues in regards to destruction and GC of the objects.

Slot-Driven Inventory System

With all of this in mind, the following is a nicer, more contained system for creating and replicating Subobjects, but in a way which allows us to support any existing actor class. The implementation below is a barebones similar example to my own slot-based inventory system that I put together for Project Orion.

The Inventory Slot

Some additional convenience properties for accessing the Inventory Component that manages us (note, there are other ways to reference the Inventory, but this is a convenient method).

UCLASS()
class UInventorySlot : public UMyAwesomeObject
{
	GENERATED_BODY()
public:
	/*
	* Accessor for the relevant inventory component
	*/
	UFUNCTION(BlueprintPure, Category = "Slot")
	UInventoryComponent* GetInventory() const
	{
		return ComponentOwnerPrivate;
	}
	
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override
	{
		Super::GetLifetimeReplicatedProps();
		
		DOREPLIFETIME_CONDITION(UInventorySlot, ComponentOwnerPrivate, COND_InitialOnly);
	}
	
private:
	friend class UInventoryComponent;

	UPROPERTY(Replicated)
	UInventoryComponent* ComponentOwnerPrivate;
};

The Inventory Component

UCLASS(meta = (BlueprintSpawnableComponent))
class UInventoryComponent : public UActorComponent
{
	GENERATED_BODY()
public:
	UInventoryComponent::UInventoryComponent(const FObjectInitializer& OI)
		: Super(OI)
	{
		// Component must be replicated to replicate sub-objects
		SetIsReplicatedByDefault(true);
	}
	
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override
	{
		Super::GetLifetimeReplicatedProps(OutLifetimeProps);

		DOREPLIFETIME(UInventoryComponent, Slots);
	}
	
	virtual bool ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags) override
	{
		bool bWroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);

		bWroteSomething |= Channel->ReplicateSubobjectList(Slots, *Bunch, *RepFlags);
		return bWroteSomething;
	}

	/*
	* Creates a new Inventory Slot of the given SlotClass
	*/
	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Slots", meta = (DeterminesOutputType = "SlotClass"))
	UInventorySlot* CreateSlot(const TSubclassOf<UInventorySlot> SlotClass)
	{
		if (SlotClass && !SlotClass->HasAnyClassFlags(CLASS_Abstract))
		{
			AActor* lOwner = GetOwner();
			
			checkf(lOwner != nullptr, TEXT("Invalid Inventory Owner"));
			checkf(lOwner->HasAuthority(), TEXT("Called without Authority!"));

			// Use the Actor as the Outer.
			UInventorySlot* NewSlot = NewObject<UInventorySlot>(lOwner, SlotClass);
			checkf(NewSlot != nullptr, TEXT("Unable to create inventory slot"));

			// Set immediately for replication
			NewSlot->ComponentOwnerPrivate = this;
			Slots.Add(NewSlot);

			return NewSlot;
		}
		
		return nullptr;
	}
	
	/*
	* Destroys a slot
	*/
	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Slots")
	bool DestroySlot(UInventorySlot* InSlot)
	{
		if (InSlot)
		{
			AActor* lOwner = GetOwner();

			checkf(lOwner != nullptr, TEXT("Invalid Inventory Owner"));
			checkf(lOwner->HasAuthority(), TEXT("Called without Authority!"));

			const int32 RemovedIndex = Slots.Remove(InSlot);
			if (RemovedIndex != INDEX_NONE)
			{
				InSlot->DestroySlot();
				return true;
			}
		}
		
		return false;
	}

	/*
	* Clears and destroys all Slots
	*/
	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Slots")
	void DestroyAllSlots()
	{
		AActor* lOwner = GetOwner();

		checkf(lOwner != nullptr, TEXT("DestroySlot:: Invalid Inventory Owner"));
		checkf(lOwner->HasAuthority(), TEXT("DestroySlot:: Called without Authority!"));

		for (UInventorySlot* SlotItr : Slots)
		{
			if (IsValid(SlotItr))
			{
				SlotItr->DestroySlot();
			}
		}

		Slots.Empty();
	}
	
protected:
	// Ensure we destroy all objects when components is destroyed/unregistered
	virtual void OnUnregister() override
	{
		const AActor* lOwner = GetOwner();
		if (lOwner && lOwner->HasAuthority())
		{
			DestroyAllSlots();
		}
		
		Super::OnUnregister()
	}
	
private:
	/*
	* Slots maintained by this component
	*/
	UPROPERTY(Replicated)
	TArray<UInventorySlot*> Slots;
};

Happy Coding!

Author James
Published
Views 19965
2

Comments (7)

  • Jay
    19/06/2021 at 10:44 PM Reply
    Is there a typo on the last code example line 119? Shouldn't it be TArray Slots?
    • 26/08/2021 at 2:00 PM Reply
      Yes, good catch. Apologies for that, have updated the article :)
  • Soren Biltoft Knudsen
    01/09/2021 at 5:54 PM Reply
    Hey Jambax, Can you make an article about your experience on dealing with calling RPC in actors that are not owned by a net connection or at least workarounds? :)
  • Sam
    10/10/2021 at 12:20 PM Reply
    Hi, This is honestly a super helpful guide! It's really helped get my head wrapped around UObject replication, so thanks for that! One thing that I'm currently struggling to figure out, is how to utilize the "UInventorySlot". I'm trying to figure it out in the context that players can pick up and drop weapons in the world. Would there be a "dummy" actor that would represent the weapon in the world that would have a reference to a derived UInventorySlot which holds weapon data such as ammo? Then when the weapon would be "picked up" in a sense, would that derived UInventorySlot weapon class be transferred to the UInventoryComponent of the Actor who picked up that weapon? Again, super helpful article, wish you all the best! - Sam.
  • Typo fixer
    11/04/2022 at 12:54 PM Reply
    Two typos I found in this article. They can be easily found by searching for "know as" so it should be "known as". The second one has a duplicate word "the the".
    • 14/05/2022 at 10:34 AM Reply
      Thanks - updated the post :)
  • 09/07/2022 at 1:03 PM Reply
    Hey James, thanks, very helpful guide! The big issue for me was that replication of reference via ReplicateSubobjects takes long time, so calling RPC won't work until it's replicated on the client. Luckily there is on_rep, so not a big deal :)

Leave a Reply