How to CreateDefaultSubobject with a blueprint derived class, without hardcoding an asset path?

Basically, I have a native class which handles some generic systems logic, and I want it to have a DefaultSubobject which is a blueprint-defined asset. However, I don’t want to hardcode any asset paths in c++ code, I want it to be completely editor-specified.

I’m running into a bit of a chicken-and-egg problem with the fact that CreateDefaultSubobject() has to be called in the C++ class’s constructor, which runs before the blueprint properties are assigned.

I’ve discovered the FObjectInitializer version of the class constructor, but this also requires the derived class’s class override implementation to be defined and specified in the derived class’s constructor in C++, which brings me back to the original problem.

The closest I’ve come to a working solution is to use Config properties, but the logic involved in setting that up is looking pretty hairy, given that the class’s constructor once again can’t access its own properties (although potentially, the non-CDO instances can access the CDO’s value?), and that assigning and saving that value also requires you to run the TryUpdateDefaultConfigFile() function on an instance of an object, which makes it harder to attach the config to some generic non-actor helper object.

UCLASS(Config = MyClassConfigs)
class AMyActor : public AActor
{
  public:
	AMyActor();

	UPROPERTY(GlobalConfig, EditAnywhere) //Config
	TSubclassOf<UMyBaseClassComponent> ComponentClass;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	UMyBaseClassComponent* mComponent;

	UFUNCTION(CallInEditor)
	void SaveConfig() {
		TryUpdateDefaultConfigFile();
	}
};

AMyActor::AMyActor() {
	if (!IsTemplate()) {
		UClass* myClass = GetMutableDefault<AMyActor >()->ComponentClass.Get();
		UObject* obj = CreateDefaultSubobject(FName("MyProp"), UMyBaseClassComponent::StaticClass(), myClass , true, false);
		mComponent= static_cast<UMyBaseClassComponent*>(obj);
	}
}

Is there a better way?

1 Like

In .h:

UPROPERTY(EditAnywhere, Category = "CategoryName")
TSubclassOf<AMyClassName> ObjectClass;

Then you can set any asset of the specified class from your library in the blueprint.

Thanks for the response @Tuerer !

Unfortunately if I just use a regular UPROPERTY, the blueprint value won’t be assigned yet by the time the constructor is run , which is the only place I’m allowed to use AddDefaultSubobject :sob:

I could use a PostInit function , create my class, and store it as a non-default-subobject variable, but then I don’t get to reap whatever benefits I would get from having it be a proper DefaultSubobject

I think if you are requiring this type to be set in a Blueprint class, by definition you cannot access the child (BP) class’s settings in the parent class constructor. The parent class cannot possibly reference data from the child class.

Thus it would seem your only path forward is via PostInitProperties?

The benefit of the FObjectInitializer is such that C++ child classes can override the type of an object instantiated by a parent class constructor, but AFAIK this is not available to Blueprints since you cannot modify the constructor of a blueprint class, and even if you could, the configuration of your Blueprint wouldn’t be available to the constructor in any case.

1 Like

Yes, you can’t use default subobject at runtime, but you can:

AMyClassName* MyObject = NewObject<AMyClassName>(this);
if (MyObject != nullptr)
{
    MyObject-> //Set what you need ;
    MyObject->RegisterComponent();
}
1 Like

For posterity: I didn’t want to give up on CreateDefaultSubobject, and decided I’d rather have 1 hardcoded asset reference than N hardcoded asset references, so this is what I did. It works but it’s really janky.

hvConfig.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "Logging/LogMacros.h"
#include "hvConfig.generated.h"

HVCOMMON_API DECLARE_LOG_CATEGORY_EXTERN(HvConfig, Log, All);

UCLASS(BlueprintType, ClassGroup = (Vacui))
class HVCOMMON_API UhvConfig : public UDataAsset
{
	GENERATED_BODY()

private:
	static TObjectPtr<UhvConfig> CachedConfig;
	static UhvConfig* GetSingleton();

public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	TMap<FSoftClassPath, FSoftClassPath> HvSubobjectClassOverrides;

	template<typename NativeUClass>
	TSoftClassPtr<NativeUClass> GetOverrideClassImpl() {
		static_assert(std::is_base_of<UObject, NativeUClass>::value, "NativeUClass must derive from UObject");
		UClass* nativeClass = NativeUClass::StaticClass();
		auto it = HvSubobjectClassOverrides.Find(nativeClass);
		if (!it) {
			UE_LOG(HvConfig, Display, TEXT("No override class registered for %s"), *nativeClass->GetName());
			return nativeClass;
		}
		UClass* overrideClass = it->TryLoadClass<NativeUClass>();
		if (!overrideClass) {
			UE_LOG(HvConfig, Fatal, TEXT("Failed to load override class %s"), *it->GetAssetPathString());
			return nativeClass;
		}
		UE_LOG(HvConfig, Display, TEXT("Found override class %s -> %s"), *nativeClass->GetName(), *it->GetAssetPathString());
		return overrideClass;
	}

	template<typename NativeUClass>
	static TSoftClassPtr<NativeUClass> GetOverrideClass() {
		return GetSingleton()->GetOverrideClassImpl<NativeUClass>();
	}
};

hvConfig.cpp

#include "hvConfig.h"

HVCOMMON_API DEFINE_LOG_CATEGORY(HvConfig);

// note: For non-Uobjects, you can also use TSharedPtr<>
TObjectPtr<UhvConfig> UhvConfig::CachedConfig = nullptr;

UhvConfig* UhvConfig::GetSingleton() {
	if (CachedConfig) {
		return CachedConfig.Get();
	}
	const TCHAR* configInstanceName = TEXT("hvConfig'/hvCore/hvConfig.hvConfig'");
	ConstructorHelpers::FObjectFinderOptional<UhvConfig> ConfigFinder(configInstanceName, LOAD_FindIfFail);
	UhvConfig* config = ConfigFinder.Get();
	if (!config) {
		UE_LOG(HvConfig, Error, TEXT("Failed to find config asset. Using a transitory one."));
		config = NewObject<UhvConfig>();
	}
	CachedConfig = config;
	return config;
}

myActor.cpp

AMyActor::AMyActor() {
	struct FConstructorStatics
	{
		TSoftClassPtr<UMyBaseComponent> DerivedClass;
		FConstructorStatics()
			: DerivedClass(UhvConfig::GetOverrideClass<UMyBaseComponent>())
		{}
	};
	static FConstructorStatics ConstructorStatics;

	UObject* subobj = CreateDefaultSubobject(FName("MyComponent"), UMyBaseComponent::StaticClass(), ConstructorStatics.DerivedClass.Get(), true, false);
	mMyComponent = static_cast<UMyBaseComponent*>(subobj);
}

Note: this code is inside a module named hvCommon, inside a plugin named hvCore. This is reflected in the HVCOMMON_API macro (which resolves to DLLIMPORT / DLLEXPORT), and the asset path for the singleton object hvConfig'/hvCore/hvConfig.hvConfig'