Blueprint class can only override class of inherited native component based on actual default component class from parent, not declared type

The description of CreateDefaultSubobject<TReturnType, TClassToConstructByDefault> implies that child classes should be able to override the subobject type with any child of TReturnType. However, blueprint classes seem to be limited to subclasses of TClassToConstructByDefault. This may just be a UI issue with how FBlueprintComponentDetails::CustomizeDetails populates the list of available classes.

Steps to Reproduce
Declare an actor class in C++.

Add a component property:

UPROPERTY(VisibleAnywhere, BlueprintReadOnly)

UMeshComponent* Mesh;

In the constructor, initialize it as follows:

Mesh = CreateDefaultSubobject<UMeshComponent, UStaticMeshComponent>(“Mesh”);

Note that although we create a UStaticMeshComponent, the property type and return type are UMeshComponent

Launch the editor and create a derived blueprint class

Select the Mesh component and attempt to change the component class via the dropdown. Note that only subclasses of UStaticMeshComponent specifically can be selected, not siblings of it under UMeshComponent (such as UGeometryCollectionComponent)

Hi James,

After investigating the engine source code, I believe that this behavior is actually by design - a limitation of the blueprint system and not only a UI issue. Even if the class picker allowed selecting a class derived from TReturnType and not TClassToConstructByDefault, the operation triggered by the selection would fail in function USubobjectDataSubsystem::ChangeSubobjectClass():

[Engine\Source\Editor\SubobjectDataInterface\Private\SubobjectDataSubsystem.cpp] [:1356] USubobjectDataSubsystem::ChangeSubobjectClass() [.....] [:1383] if (!NewClass->IsChildOf(BaseClass)) [:1384] { [:1385] return false; [:1386] }The check performed in the code snippet above would cause the operation to fail because “BaseClass” in this case would still correspond to TClassToConstructByDefault and not TReturnType. This is because each level of a Blueprint Class Hierarchy needs an actual instance (not just a class specification) of a Template Component that represents the default class and its property values at that hierarchy level (a concept similar to CDOs, but within the context of each blueprint class). When a Blueprint is used to extend a C++ class, it creates that template component instance using TClassToConstructByDefault, and that instance must be derived from that class for the system to work. I haven’t dug deep enough to find out the reason behind this requirement, but the code does seem to expect it in more than one location.

Note that overriding a Default Subobject Class is also restricted in C++, albeit a little more loosely. The first override of a Default Subobject Class is indeed restricted to subclasses of TReturnType and not TClassToConstructByDefault. However, any further overrides are restricted to subclasses of the previous override. For example, consider the following hierarchy:

`UCLASS()
class AActorParent : public AActor
{
GENERATED_BODY()
public:
AActorParent(const FObjectInitializer& ObjectInitializer)
{
MyMesh = CreateDefaultSubobject<UMeshComponent, UStaticMeshComponent>(“MyMeshComponent”);
RootComponent = MyMesh;
}
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = “true”))
TObjectPtr MyMesh;
};

UCLASS()
class AActorChild : public AActorParent
{
GENERATED_BODY()
public:
AActorChild(const FObjectInitializer& ObjectInitializer)
: AActorParent(ObjectInitializer.SetDefaultSubobjectClass(“MyMeshComponent”))
{}
};

UCLASS()
class AActorGrandChild : public AActorChild
{
GENERATED_BODY()
public:
AActorGrandChild(const FObjectInitializer& ObjectInitializer)
: AActorChild(ObjectInitializer.SetDefaultSubobjectClass(“MyMeshComponent”))
{}
};`Attempting to instantiate AActorChild will work without any problems, but spawning a AActorGrandChild will result in the following runtime error (from [Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectGlobals.cpp:4226]):

LogUObjectGlobals: Error: Class /Script/Paper2D.PaperSpriteComponent is not a legal override for component MyMeshComponent because it does not derive from Class /Script/Engine.SkeletalMeshComponent. Will use Class /Script/Engine.SkeletalMeshComponent when constructing component.Interestingly, I also found that there are only 3 places in the entire engine codebase that call CreateDefaultSubobject<TReturnType, TClassToConstructByDefault> with different types: the experimental plugin classes AWheeledVehiclePawn and UWebAPIDeveloperSettings, and the well-known class ADefaultPawn. ADefaultPawn calls CreateDefaultSubobject<UPawnMovementComponent, UFloatingPawnMovement>, and attempting to change the class of that component on a derived blueprint will only show subclasses of UFloatingPawnMovement.

I hope this information is helpful. If this limitation is impacting you, I suggest trying to organize your actor and component hierarchies in a way that the C++ class does not need to use a TClassToConstructByDefault different from TReturnType. If this does not seem possible in your use case, maybe you can provide some more information about it so that we can try to find a workaround.

Best regards,

Vitor

Hi Vitor, thanks for the thorough investigation. This is just a prototype so I’ll probably just work around it for now. It’s unfortunate the engine doesn’t support this - it kind of makes sense, but theoretically you’d think an instance of a subclass could serve as a template component for a superclass. Good to know the details of what you can and can’t do in any case.

Thanks, James