Blog

When I’m playing an online multiplayer game, I dislike the delay between picking up an item and the server confirming that you’ve picked it up. Like the online supply box in classic Monster Hunter; I press the button to put the item in my inventory, which locks my input. Then the server checks that I was indeed the one who picked it up, and finally confirms it with my console, which unlocks my input and puts the item in my inventory. Picking up each item can take a second or two, and while it’s definitely a minor inconvenience, it’s noticeable.

So it got me thinking about how it’s implemented, and if any one way is faster than the others. This train of thought led me to wanting to try my hand at a similar problem; to implement a couple of networked inventory components, and compare them. This blog post will detail how that went down, and the performance test I conducted at the end. (If you only want to know about the performance comparison between RPC and built-in replication, here you go.)

How I implemented it

My original plan was for the code to be as DRY as possible; one private Inventory class, which the two inventory components would use internally. This Inventory class could use a TMap, allowing a simple mapping from item IDs to the quantity of the item in the inventory. Simple, right?

The first component I implemented, RPCBasedInventoryComponent, works exactly like this. It keeps an Inventory member, and updates it via RPC calls. For instance, here’s the declaration and definition of Server_ModifyInventory:

// .h
UFUNCTION(Server, Reliable, Category = "Networked Inventory")
	void Server_ModifyInventory(const TArray<FInventoryEntry>& inventoryChanges);

// .cpp
void URPCBasedInventoryComponent::Server_ModifyInventory_Implementation(const TArray<FInventoryEntry>& inventoryChanges)
{
	ensure(GetOwner()->GetLocalRole() == ROLE_Authority);
	checkCode(
		for (const FInventoryEntry& entry : inventoryChanges)
		{
			if (entry.Quantity == 0)
			{
				UE_LOG(LogTemp, Warning, TEXT("Adding zero quantity of item %s."));
			}
		}
	);

	TTuple<EChangeGroupStatus, TArray<EChangeStatus>> pair = Inventory->ModifyGroupOfEntries(inventoryChanges);

	if (pair.Key != EChangeGroupStatus::AllSuccessful)
	{
		UE_LOG(LogTemp, Warning, TEXT("Not all inventory changes successful. Some lost."));
	}

	Client_ModifyInventory(inventoryChanges);
}

As you can see, after some checks, it just passes the changes to its internal Inventory property, before calling a client RPC to make sure that the client also makes the appropriate inventory changes. Getting this RPC-based component working truthfully didn’t take very much time; the operations to define and implement are fairly straightforward!

However, implementing the ReplicationBasedInventoryComponent took a while. Unfortunately, at least right now, TMap replication isn’t part of Unreal Engine 4. So, I made a few modifications; I created a new ArrayInventory class, which internally used a TArray rather than a TMap, but also kept an internal map of item IDs to array positions, which was updated via the RepNotify mechanism. That way, you get O(1) access, but also can replicate the whole inventory in a relatively straightforward manner!

But sadly, even after fixing that issue, there was another problem. You see, Unreal Engine 4 doesn’t support arbitrary replication trees of non-actor UObjects. Even if I were to add the private ArrayInventory object to the actor component, that inventory wouldn’t be able to replicate its internal TArray. I tried for a few days, but I personally couldn’t find a way around this problem. (If someone who reads this does though, please let me know!) So I looked for another solution.

By making the ArrayInventory inherit from AActor, I would be able to replicate it just fine. However, I didn’t like this solution very much. According to the UE4 documentation, an Actor is “any object that can be placed into a level”. I don’t think an inventory alone fits this description, since it’s more of a concept. Call it splitting hairs if you like, but I think that code should be as straightforward and obvious as possible; and going against existing UE4 conventions sounds like misunderstandings waiting to happen.

Another solution I briefly considered was making the ArrayInventory inherit from UObject, and moving it into the parent actor; i.e. the one which owns the ReplicationBasedInventoryComponent. The component itself could just get a reference or pointer to the ArrayInventory on its parent, which could be replicated just fine as it’s a direct variable of an actor.

Again, I didn’t like this solution. Actor components should be as encapsulated and isolated as possible; and requiring the parent actor itself to have a specific variable UObject on it means that the component can’t be used on any old actor anymore. It would muddy the waters too much, and so I didn’t follow this approach any further.

The solution I actually went with involved moving the ArrayInventory logic and variables out of their own object, and into the ReplicationBasedInventoryComponent. This made the code less DRY, of course - but it also meant that replication behaved nicely, the component was self-contained, and UE4 conventions were upheld. This wasn’t the ideal solution, but it was a lot closer to it than the others I considered.

And so, the final version of the ReplicationBasedInventoryComponent doesn’t pass inventory commands down to a sub-object, but handles them all itself. Fortunately, I’d already implemented the ArrayInventory, so picking this approach didn’t mean much extra work other than hammering the Ctrl+C and Ctrl+V keys. Here’s a little peek at the code for modifying the entry for a single item:

// .h
UFUNCTION(Category = "Networked Inventory")
	EChangeStatus ModifyEntry(const FInventoryEntry& entryChange);

// .cpp
EChangeStatus UReplicationInventoryComponent::ModifyEntry(const FInventoryEntry& entryChange)
{
	if (!Contains(entryChange.ItemCode))
	{
		InventoryArray.Emplace(entryChange.ItemCode, 0);
		LookupCache.Add(entryChange.ItemCode, InventoryArray.Num() - 1);
	}
	int32& quantityRef = InventoryArray[LookupCache[entryChange.ItemCode]].Quantity;
	quantityRef += entryChange.Quantity;

	check(quantityRef == InventoryArray[LookupCache[entryChange.ItemCode]].Quantity);

	if (quantityRef <= 0)
	{
		UE_LOG(LogTemp, Log, TEXT("Non-positive quantity for item %s. Quantity: %i. Removing..."), *entryChange.ItemCode.ToString(), entryChange.Quantity);

		ERemovalStatus status = RemoveItem(entryChange.ItemCode);
		if (status != ERemovalStatus::Success)
		{
			return EChangeStatus::CouldNotMakeChange;
		}
	}

	return EChangeStatus::Success;
}

Performance Comparison

So, having created one RPC-based inventory component, and another using variable replication, I needed some way to test them with respect to network activity. Thankfully, Unreal Engine 4 comes with the netprofile tool, which is for (you guessed it) network profiling! But what’s an appropriate test?

I made an actor which does nothing but add endlessly adds random elements to the inventory of the actor that collides with it, once per second. For the test, I got actor with an inventory to collide with this object, and left it alone for 20 minutes. The total number of unique entries should be close to 1200, which is a huge inventory size for many games, so this test should stress the system. So, let’s take a look at the results.

Here’s the profile for the RPC component:

RPC Inventory Component Netprofile Graph.

And here’s the profile for the replication component:

Replication Inventory Component Netprofile Graph.

Hmm. I was expecting that as the size of the inventory increases, variable replication gets more and more expensive. By looking at the “All RPCs” section of the network profiler, we can see that in total, the Client_ModifyInventory() RPC call uses 35.6 KB of memory bandwidth (for the RPC component):

RPC Inventory Component Netprofile All RPCs.

Similarly, by looking at the “All Properties” section of the network profiler, we can see that the InventoryArray property for the replication component only uses 26.8 KB of memory bandwidth in total - significantly less than the RPC call:

Replication Inventory Component Netprofile All Properties.

What?

For inventories that both grew to have over 1000 unique entries each, the fact that in UE4, replicating an enormous array each second is still cheaper than a few RPC calls involving much smaller arrays is absolutely wild. At least to a newcomer.

It looks like the average size in bits of the RPC call is 2-3 times larger as well; presumably this is the part that’s responsible for the big memory usage difference. All the same, these findings weren’t at all what I was expecting, and it was incredibly interesting experimenting with it!

And there you have it - Unreal Engine 4’s replication system is optimised as heck, so much so that you can replicate arrays with over 1000 elements with no problem. Have fun!