Actor Component sub-object replication is completely broken with Inline UObjects.

Actor Components have an inbuilt ability to replicate sub-objects and have done since forever. This feature doesn’t seem to be used very often, but it’s extremely useful for creating self-contained systems.

I’ve been building an inventory system - and it works(ed) pretty much flawlessly, until actors start being destroyed and recreated by the Net Relevancy system. It’s exposed a pretty gigantic flaw with the system. The following is a brief summary of the issue, and how it’s being caused.

Inventory Component
Contains a replicated array of ‘UInventorySlots’. The slots are EditInlineNew, and blueprintable so that they can add custom properties.



class UInventoryComponent : public UActorComponent

UPROPERTY(EditAnywhere, Instanced, BlueprintReadOnly, Category = "Slots", ReplicatedUsing = "OnRep_Slots")
TArray<UST_InventorySlot*> Slots;


Note the ‘Instanced’ flag on the array of slots. This is especially important because I need the slots to be able to be created inline. This allows an actor to have an ‘Inventory’, then specify whatever ‘Slots’ it likes for that inventory like so:

Slots.JPG

Another side-effect of this is that the slot is created with the Inventory Component as it’s ‘Outer’ - this is good and what I want to happen.

The slots are replicated by the inventory component the same way you would replicate any other sub-object and as described by the documentation, like so:



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

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

void UST_InventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(UST_InventoryComponent, Slots);
}


Now - this works absolutely flawlessly. I can spawn two clients with a pawn that has an inventory, and the default slots and corresponding items are also created as they should be.


Unfortunately however, as soon as that pawn goes out of relevancy range of a player and then returns (i.e, all objects are destroyed and re-created from replication) - the Actor Channel causes a crash. The sub-objects are recreated but are created inside the OLD inventory component and actor, which is pending kill. As soon as GC runs, the game crashes. It’s pretty obvious that this is a bug to me.

Another issue is that sub-objects which are created dynamically and not in that array by default - are actually created on the client using the Actor as the outer and NOT the component. This is caused by DataChannel.cpp line 3140 - where it creates the new object using the actor channels’ actor as the outer of the new object. It should be using the component that replicated it as the outer.

So basically, sub-object replication in the actor component is pretty useless while this is the case. This feels like the perfect application for it.

There seem to be a lot of other issues when using EditInlineNew and creating Instances of UObjects directly inline, such as struct UPROPERTY’s not showing in details panels etc. This area really could use some attention…

I’ve taken a look at the gameplay abilities system which seems to use Sub-Object Replication for attribute sets - but it spawns the sets dynamically using the actor as the outer at runtime. While this system obviously works, it has a lot of drawbacks:

  • I could only have one inventory component per-actor, as the slots need to know which inventory component they belong too and without the component as the outer, that’s extremely difficult and cumbersome.

  • It means that for every single inventory slot that my units have, I have to create a bespoke class just for that slot - or I have to replicate all of it’s properties. Both versions of that suck.

I really want to keep the Inlined slots, and I really need to be able to support network relevancy. There must be a workaround or a fix for this, surely?

Back after trying every workaround I can think of, even changing the UObject Outers at runtime with UObject::Rename, this still doesn’t really seem to work without an issue occurring somewhere.

My only workaround now is to forget the EditInlineNew system (which seems to create a lot of inherent issues on it’s own anyway), and force the end-user to create subclasses for every slot. Not really the end result I was looking for and considerably less ‘Clean’, but it seems that Instanced / EditInlineNew UObjects just do not play well with sub-object replication.

It would be really great if replicated UObjects could exist outside an Actor at some point - and if all the issues with EditInlineNew UObjects could be ironed out.

I have a similar setup for a modular scripting system, can confirm that the new objects are applied to the PendingKill original actors instead of the new one when relevancy is gained again shortly after losing it.

If the original actor has already finished GC by the time that relevancy is gained back, then everything appears to be working as expected.

Have you reported it yet? At the very least getting them not applied to the IsPendingKill actor would solve most of the issues.

I’ll also note that Instanced Inline objects can’t be edited from child classes when they are in parent classes, that one is a real kicker to clean and efficient workflow. Have a Default spawned object on a character that I cant edit the instanced objects on unless I use the very base parent class as a base.

Did you ever report this? I found multiple issues related to the same thing in the bug tracker and one fix slated for 4.22, but nothing specifically about using the stale actor context on replication.

If you didn’t i’ll mock up a reproduction to submit.

I did not I’m afraid, I changed so much in my frustration of trying to get something working!

I sent a couple of bug reports out today regarding this and another instanced uobject issue.

Hopefully it helps having a clean reproduction project in hand.

I heard back from them and it looks like it is an implementation mistake on our part, understandable I would hope since this kind of thing doesn’t have any sort of documentation.
Instanced InlineNew Object properties do not have any sort of self clean up for when things like relevancy destruction happens and the like which is what is causing the problem and why I was noticing it not happen after GC had swept through.

I’ll send you in PM a bit of code I mocked up real quick that appears to clean up the issue.

Apologies for replying to such an old thread, but I’ve had this exact issue and managed to solve it by digging into the engine code.

The code path for replication calls FNetGUIDCache::GetObjectFromNetGUID during serialization of the actor that entered relevancy range.

This function looks up the NetGUID for the UObject(s) in questions, and these NetGUIDs will be identical as the last time they were replicated. FNetGUIDCache::ObjectLookup is the cache for the lookup, and it contains weak pointers to the cached objects.

When your actor component gets destroyed due to going out of replication range, it only drops its references to those UObjects, and they will be “valid” until a garbage collector destroys them. This means that when FNetGUIDCache looks up those NetGUIDs, they will return “valid” UObjects which are really no longer valid because they belong to a destroyed Actor and Component.

All you have to do to fix this issue is to do the same thing Actors do for Components (remember - Components themselves are just replicated UObjects that belong to Actors. Actors). When Actors are destroyed, they mark their owned UObjects as PendingKill.

So, in your component, override OnComponentDestroyed method, and in it go through your UObjects and call object->MarkPendingKill();

Hope this helps others who find themselves in this situation!

3 Likes