DoNoCreateDefaultSubobject ignored by existing blueprint classes

Hello,

We’ve noticed an issue related to CreateOptionalDefaultSubobject and blueprint derived classes, when we decide to skip creating a subobject if blueprint derived classes already exist.

We’ve initially encountered the issue with AAIController and its PathFollowingComponent, but here is a barebone example:

Let’s say we’re optionally creating a component in a base class, and (for now) keep it in a child class.

// .h

UCLASS(BlueprintType)

class UMyComponent : public UActorComponent

{

GENERATED_BODY()

};

UCLASS()

class AFoo : public AActor

{

GENERATED_BODY()

public:

AFoo(const FObjectInitializer& ObjectInitializer);

protected:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly)

TObjectPtr<UMyComponent> MyComp;

};

UCLASS(Blueprintable)

class AFoo_Child : public AFoo

{

GENERATED_BODY()

public:

AFoo_Child(const FObjectInitializer& ObjectInitializer);

};

// .cpp

AFoo::AFoo(const FObjectInitializer& ObjectInitializer)

: Super(ObjectInitializer)

{

MyComp = CreateOptionalDefaultSubobject<UMyComponent>(TEXT(“MyComp”));

}

AFoo_Child::AFoo_Child(const FObjectInitializer& ObjectInitializer)

: Super(ObjectInitializer)

{

}

Then, we make a blueprint version of that child class.

(see bp_child.png)

Later, we might decide to skip creating this component in the native child class. In code, we’re allowed to do so because the subobject is optional, we just need to explicitly skip it.

AFoo_Child::AFoo_Child(const FObjectInitializer& ObjectInitializer)

: Super(ObjectInitializer

.DoNoCreateDefaultSubobject(TEXT(“MyComp”))

{

}

The component will be properly skipped in code. However, it will still be present in the blueprint derived class.

Our understanding of the feature would be that the component in Blueprint should also be skipped, as the parent removed it

(and blueprints are only able to modify components that their parent exposed).

We’ve tracked down the issue to FBlueprintCompilationManagerImpl::ReinstanceBatch, Step 3. Copy defaults from old CDO.

NewCDO correctly does not have a valid component, but OldCDO’s component is valid (most likely coming from its saved data, which doesn’t check for subobjects being ignored when it’s deserialized).

This value is then transferred to NewCDO, leaving the blueprint instance with a valid component.

As this happens outside of the object construction flow, it doesn’t seem possible to access the object initializer, to skip subobjects that were ignored.

We did however manage to work around the issue by overriding Serialize in our native child class, and manually clearing the component’s property.

void AFoo_Child::Serialize(FArchive& Ar)

{

Super::Serialize(Ar);

if (Ar.IsLoading())

{

MyComp = nullptr;

}

}

This leads to “nullptr” being transferred to the new CDO, no matter what serialized data used to be there.

Opening the blueprint leaves a non-editable entry (see bp_child_invalid.png) but after compiling it, the component is properly removed. (see bp_child_fixed.png)

We can then remove our Serialize override.

1) Would you consider the current behavior to be expected, meaning it would be on us to make sure we get rid of the component if an earlier version had one?

2) Is our workaround safe or could it cause issues?

3) Do you know if there would be a way to handle it directly at the engine-level? (if we decide to skip creating a component, making sure it’s gone in derived blueprint classes)

Steps to Reproduce

  1. Create a new C++ actor class, with an optional component (CreateOptionalDefaultSubobject)
  2. Create a child of this actor class in C++ and a blueprint that inherits from it
  3. In the C++ child actor class, decide to not create the component anymore (DoNotCreateDefaultSubobject)
  4. The blueprint class will still have the component

Hi [mention removed]​,

I’ve been testing the setup you posted and, in my vanilla project, the component is removed correctly both in C++ and in the Blueprint. I also verified this using Unreal Engine 5.6.

Based on the engine function you referenced (BlueprintCompilationManagerImpl::ReinstanceBatch), this kind of issue could indeed come from Hot Reloading. When I tested with Hot Reload, the component was not removed correctly, which matches the behavior you described.

However, when performing a full editor rebuild/restart, the component does get removed correctly from the Blueprint. Because of this, I don’t think the behavior you are seeing is expected. Just to clarify, does this issue also happen when doing a full rebuild of the project, or only when using Hot Reload? From my tests, it only happens with Hot Reload, and it definitely looks like an Epic-side issue.

I will also test the most recent dev branch to confirm whether this has already been fixed. I wasn’t able to find any related bug reports on: https://issues.unrealengine.com/.

I’ll update soon, but in the meantime, please let us know if you’re seeing this issue with Hot Reload, full rebuild, or both.

Additionally, there is a great community post written by Zhi (Epic staff) about Investigating Blueprint Data Loss. This guide explains how Blueprint default data is stored, how it interacts with C++ changes, how CDOs are recreated during recompilation, and why outdated Blueprint data can sometimes persist. It seems quite relevant to this issue.

Guide: https://dev.epicgames.com/community/learning/tutorials/oEn6/investigating-blueprint-data-loss-issues-in-unreal-engine

Best,

Joan

Hi [mention removed]​,

Thanks a lot for the link to the community post, it will help when debugging blueprint serialization issues.

I can confirm the issue happens without hot-reloading.

I’ve linked a repro project in vanilla Unreal Engine 5.6(.1), from EGS, with the same repro steps:

- Create a child blueprint class with an optional component from a native parent

- DoNoCreate that component

- Notice that it’s still present in the blueprint class (it only goes away after explicitly clearing the property in Serialize and re-compiling the blueprint)

In the repro project, you can change FIX_STEP (DNCDSubobject.cpp) to test the different steps in order (it also already contains a child blueprint class for the tests).

I’ll check with the community post, to try to see where the issue might come from.

Hi [mention removed]​ ,

With your repro project we could reproduce the bug easily. Your serialize solution should totally be a good work around to handle this case and clear the component. It looks like the bug is generated under certain conditions so we are going to look a bit deeper to see the issue. We will also make a bug report so that Epic devs can fix this, but for now setting the component to nullptr is totally valid. Will post again in this thread the bug tracker once we have it.

Thanks for reporting this!

Best,

Joan

Thanks for looking into it!

Hi [mention removed]​,

I have filed an internal bug report so that the engine developers who own this system can start working on it.

Here is the bug tracking number: UE-355998. The link should become available once the devs make it public.

Please let us know if the workaround causes any issues at any point.

Best,

Joan