Nested subobject replication

I am working on an inventory system and currently have a working implementation but I want to see if it is possible to refactor it in a way that will make it more flexible to work with.
Right now, I have items represented by UObjects so they can have functionality like OnEquip, OnDrop, OnUse, etc.
Inventories store a list of UItem and UItem inherits from UNetworkedObject (below) so they can be replicated:

UCLASS()
class SURVIVALGAME_API UNetworkedObject : public UObject
{
	GENERATED_BODY()

protected:
	// 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)
	AActor* GetOwningActor() const
	{
		return GetTypedOuter<AActor>();
	}
	
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override
	{
		Super::GetLifetimeReplicatedProps(OutLifetimeProps);
		
		// 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;
	}
};

And then UItem contains properties that can change at runtime, like amount, damage, etc. Here is the base UItem, which can be inherited from to add more data:

UCLASS(EditInlineNew, DefaultToInstanced)
class SURVIVALGAME_API UItem : public UNetworkedObject
{
	GENERATED_BODY()

public:
	UPROPERTY()
	FOnItemUpdated OnItemUpdated;
	
	UPROPERTY(EditAnywhere, Replicated, SaveGame)
	TObjectPtr<UItemDefinition> ItemDefinition;

	UPROPERTY(EditAnywhere, Replicated, ReplicatedUsing = OnRep_Amount, BlueprintReadOnly, SaveGame)
	int32 Amount = 1;

	UPROPERTY(EditAnywhere, Replicated, ReplicatedUsing = OnRep_InventoryPosition, BlueprintReadOnly, SaveGame)
	int32 InventoryPosition = -1;

public:	
	virtual bool TryDrop(const FTransform& InTransform);
	
	virtual bool CanStackWith(UItem* OtherItem);
	
	/**
	 * @brief Attempts to stack another item into this one
	 * @param OtherItem The item to stack into this one
	 */
	virtual void CombineWith(UItem* OtherItem);
	
	virtual EItemDragResult OnItemDragged(UItem* DraggedItem);

private:
	UFUNCTION()
	void OnRep_Amount();
	
	UFUNCTION()
	void OnRep_InventoryPosition();
	
protected:
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};

As it stands, I store the inventory position of the item straight inside each item. This works, but I would rather not have the items be responsible for keeping track of where they are in an inventory.
I would prefer it if I could have inventories store an array of UInventorySlot (or similar) instead, which would handle the inventory position of items and allow for more granular functionality to be added to which items can be in which slots etc.
I have tried an approach like this, where UInventorySlot looks like this:

class UInventorySlot : public UNetworkedObject
{
	UItem* Item; // can be null if the slot is empty

    void SetItem(UItem* Item);
    bool CanAcceptItem(Uitem* Item);
};

Then the inventory (an ActorComponent) stores an array of these slots based on the inventory’s size.
The trouble comes when trying to replicate. In the InventoryComponent I do something like this:

bool UInventoryComponent::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
	bool bWroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);

	bWroteSomething |= Channel->ReplicateSubobjectList(InventorySlots, *Bunch, *RepFlags);

	return bWroteSomething;
}

But that will only replicate the UInventorySlots and their properties, and since ReplicateSubobjects is not included for UObjects I cannot override it in UInventorySlot to simply replicate the slot’s item as a subobject.

TLDR: Is there a way to replicate these nested UObjects? Alternatively, could there be a different way to set this up that wouldn’t require nested UObjects?
Would love to hear any thoughts.

Great post.

What is the purpose of UInventorySlot? It seems unnecessary to me, and removing / moving the functionality to InventoryComponent it might fix the problem.

Also, have to make sure, did you remember GetLifetimeReplicatedProps in your InventoryComponent?

This was meant to be more of a question than a post but I’ve figured it out for the most part and it will serve others well if they run into the same problems.

UInventorySlot doesn’t exist yet. It’s a part I want to add to the system, and I think I got it working by adding an identical ReplicateSubobjects function to UNetworkedObject, then manually calling that inside of UInventoryComponent::ReplicateSubobjects; the biggest problem becomes making sure each UNetworkedObject has its owner set to an Actor, which can be tedious, but it isn’t a big problem.

The purpose of UInventorySlot’s addition was to remove the UItem’s responsibility of keeping track of its position in each inventory, which doesn’t really make sense when the UItem isn’t in an inventory (e.g. laying on the ground as a dropped item). Instead, the UInventorySlot keeps track of its position, wherever it is, then can optionally contain an item. This also means individual slots can have functionality attached to them like CanAccept(UItem* Item).

I’m currently looking into making a tetris-like inventory system, (like Dead Matter or Diablo) which might not even follow this structure, but I suppose people could use a system like this for a more Minecraft/Vintage Story-like fixed grid inventory.

So for nested subobject replication, you have to do something like this:

bool UInventoryComponent::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
	bool bWroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);

	bWroteSomething |= Channel->ReplicateSubobjectList(Slots, *Bunch, *RepFlags);
	for (auto Slot : Slots)
	{
		if(Slot)
		{
			if(Slot->GetOuter() != GetOwner())
				Slot->Rename(nullptr, GetOwner());
			bWroteSomething |= Slot->ReplicateSubobjects(Channel, Bunch, RepFlags);
		}
	}

	return bWroteSomething;
}

Where UInventorySlot has this function manually added, not overridden:

bool UInventorySlot::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
	bool bWroteSomething = false;

	bWroteSomething |= Channel->ReplicateSubobject(Item, *Bunch, *RepFlags);

	return bWroteSomething;
}

Also make sure everything is added to GetLifetimeReplicatedProps correctly and make sure each UItem and UInventorySlot is owned by an Actor, I use this to have UInventorySlots which are added in editor work when starting the game:

void UInventoryComponent::BeginPlay()
{
	Super::BeginPlay();

	for (auto Slot : Slots)
	{
		Slot->Rename(nullptr, GetOwner());
	}
}

But if you create slots at runtime you would just make sure the owner is set then.
As for items, the owner should change if it gets dropped (dropped item actor owns it then) or transferred to another inventory.
(stream of thought isn’t ending) That brings up another issue that might be more difficult to solve: if you want items to contain inventories, you would need to recursively set the owner to an Actor which would get annoying to handle, one of the reasons I made UInventoryComponent a component in the first place.