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)