Replicating UObjects: Building a Flexible Inventory System

Last modified date

Project Orion

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, know 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);
		
		DOREPLIFETIME(AMyAwesomeActor, MyObject);
	}
	
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 the 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(UST_SlotComponent, 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<UInventoryComponent*> Slots;
};

Happy Coding!