Replicated UObject subobject lifetime appears bound to the original UActorChannel that created it

Hello! We’re implementing an inventory system where items are replicated UObjects (subobjects) and can be moved between different owning actors/inventories at runtime (e.g. container → player inventory → dropped body → another player). Replication itself works after transfer (properties update, references are valid), but client-side lifetime of the UObject seems tied to the actor channel that originally created it. When that original actor is destroyed or streamed out, the client destroys/GCs the item UObject even if it is currently being replicated by a different actor.

This causes frequent item invalidation on clients in streaming/destroy scenarios.

What we found in engine behavior :

On the client, the initial replication path adds created subobjects to UActorChannel::CreateSubObjects. When the actor channel is torn down (destroy/stream-out), it iterates that list and calls PreDestroyFromReplication() and then MarkAsGarbage() on those subobjects.

So even if the same UObject is now being replicated from another actor/channel, it still gets destroyed when the original channel closes. This looks like an engine assumption that subobject lifetime is bound to the channel that first created it.

Our current workaround (client-side migration)

When an item is transferred, we manually remove the UObject from any old channels’ CreateSubObjects and add it to the new channel, in FastArraySerializer post-rep callbacks.

UWorld* World = ParentContainer->ParentManager->GetWorld();
if (!World) return;
 
UNetDriver* NetDriver = World->GetNetDriver();
if (!NetDriver || !NetDriver->ServerConnection) return;
 
AActor* NewOwner = ParentContainer->ParentManager->GetOwner();
UActorChannel* NewChannel = NetDriver->ServerConnection->FindActorChannelRef(NewOwner);
 
// Remove from all other channels
for (UChannel* Channel : NetDriver->ServerConnection->Channels)
{
    UActorChannel* ActorChannel = Cast<UActorChannel>(Channel);
    if (ActorChannel && ActorChannel != NewChannel)
    {
        ActorChannel->CreateSubObjects.Remove(ItemSlot.Item);
    }
}
 
// Add to new owner's channel
if (NewChannel)
{
    NewChannel->CreateSubObjects.AddUnique(ItemSlot.Item);
}

However, CreateSubObjects is deprecated and flagged to become private (“Use GetCreatedSubObjects() instead”), and in 5.4+ the replacement is not usable for mutation (and/or private/const depending on version). So this workaround is not future-proof.

Our questions :

Is this behavior intended?

Is the engine design that replicated UObject subobjects are expected to live and die with the actor channel that originally created them, making “transferring the same subobject instance between different replicating actors” unsupported?

If transfer is supported, what is the correct API/pattern to migrate ownership without touching CreateSubObjects?

Is there an engine-supported way to:

detach a subobject from the old channel’s created-subobject lifetime tracking, and/or re-associate it with the new actor/channel so it won’t be destroyed when the old channel closes?

If transfer is not supported, what is the recommended approach for inventory items that must persist across actor destruction/streaming?

Any clarification on the intended lifetime model and the correct way to implement “moving items between inventories/owners” (especially with streaming) would be appreciated.

Thanks in advance.

[Attachment Removed]

Steps to Reproduce
Network: Dedicated server + 2 clients

Server spawns ProductionActor that creates an item UObject and replicates it as a subobject.

Client1 interacts and moves that same item into their own inventory (new owning/replicating actor for that item becomes the player / inventory owner).

Later, ProductionActor is destroyed or streamed out (level streaming unload).

Observed: On Client1, the item UObject is destroyed/garbage-collected and inventory reference becomes invalid.

Client2 arriving later may see the item fine depending on which actor first replicated it to them (their “initial replicator” differs), but the same invalidation occurs when that original replicator actor/channel goes away.

[Attachment Removed]

For NewObject<> in ProductionActor: what are you using as the Outer for the item UObject instance? I assume it’s ProductionActor?

Outer doesn’t matter here — that was my first guess too, so I tested a bunch of options. I tried World, ProductionActor, changing the owner to the transfer target via UObject::Rename, etc. The owner/outer is not the source of the issue; it doesn’t seem related to this behavior at all.

When a client enters relevancy range for an actor, if that actor has a registered UObject in its subobject list, the client adds it to that actor channel’s CreateSubObjects array locally on that client. Later, when that actor is destroyed or unloaded, the channel destroys everything in that list — and that’s the source of the issue.

There’s another workaround as well: create a new item object on each transfer between containers. Based on my testing, this also solves the issue, because it guarantees the object only ever exists under its initial replicator/channel.

This is basically what you’re doing, but the source of the problem isn’t the object’s ownership — it’s the actor channel’s CreatedSubObjects / CreateSubObjects array behavior I mentioned above. :grinning_face_with_smiling_eyes:

I can see how, if your first approach was to duplicate/copy the object, it could look like the issue is tied to ownership, because that workaround coincidentally fixes it by ensuring the object only ever exists under its initial replicator/channel.

Theoretically you could do this with the same item instance by using UObject::Rename, but instead of that, what I do is DuplicateObject

the item instead, to its new Outer, and mark the old item instance garbage. Reason being, I deliberately want the old item instance to be destroyed, as an extra layer of protection against dangling pointers in the old actor that could continue to reference the item in its new location.

[Attachment Removed]

Hi,

To answer your questions:

Is this behavior intended?

Yes, this behavior is intended.

Is the engine design that replicated UObject subobjects are expected to live and die with the actor channel that originally created them, making “transferring the same subobject instance between different replicating actors” unsupported?

Yes, this is by design, as the engine assumes a replicated subobject will only be owned by a single actor and won’t have its ownership changed, making functionality like this unsupported.

Is there an engine-supported way to detach a subobject from the old channel’s created-subobject lifetime tracking, and/or re-associate it with the new actor/channel so it won’t be destroyed when the old channel closes?

Like you noted here, access to the CreateSubObjects array is planned to be deprecated, so you’ll likely need to make engine modifications to be able to do something like this.

There was support recently added for renaming dynamically spawned actors (CL 31915797), but this was more intended to allow actors to change which streaming level is their outer. I don’t believe this affects the actor channel associated with the subobject.

If transfer is not supported, what is the recommended approach for inventory items that must persist across actor destruction/streaming?

I believe the most common workaround is simply to destroy the original subobject instance and create a new one on the target owner.

Another option that’s been suggested is to use structs instead of UObjects to represent items: [Content removed]

Thanks,

Alex

[Attachment Removed]

I’m not Epic, but may be able to provide some additional insight.

What are you using as the outer for the UObject item instance in NewObject<> in ProductionActor ? I assume the ProductionActor?

Fast array post rep functions aren’t called server side, so are you removing from the ProductionActor CreateSubObjects elsewhere? If not, it will be destroyed in UActorChannel::DestroyActorAndComponents

The way I handle item instance transfers in our UObject based inventory is by ensuring the item instance outer is always the inventory component or actor that owns the item instance. This means that any time the item instance is looted from a corpse, dropped, picked up, placed into a container, etc, it always gets an updated outer for its new owner.

Theoretically you could do this with the same item instance by using UObject::Rename, but instead of that, what I do is DuplicateObject

the item instead, to its new Outer, and mark the old item instance garbage. Reason being, I deliberately want the old item instance to be destroyed, as an extra layer of protection against dangling pointers in the old actor that could continue to reference the item in its new location.

The down side with this approach is that the item instance needs to fully replicate whenever it changes owner, but they aren’t that big, and this doesn’t happen that often, so it’s an issue I can live with. I think even a rename would force a full replication, but I’m not 100% sure on that.

I haven’t tried it, but it may be possible to scope the item instance outer to a long lived actor like the game state, and then just pass the pointer around to different owning actors who replicate it as subobjects, but there were too many unknowns to me at the time to experiment with that myself, a few years ago when I wrote this.

I’ll be watching this post further to see if there’s an even better approach to what I took, but I hope that provides some extra context or leads.

[Attachment Removed]