Hello,
Thank you for your time.
USTRUCT()
struct FObjectRef
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
TObjectPtr<UObject> Object = nullptr;
};
UCLASS(Config=Game, DefaultConfig)
class UMySettings : public UObject
{
GENERATED_BODY()
public:
// UHT error:
// "Not allowed to use 'config' with object variables"
//
// UPROPERTY(Config, EditAnywhere)
// TObjectPtr<UObject> DirectObject = nullptr;
UPROPERTY(Config, EditAnywhere)
FObjectRef WrappedObject;
};
A direct UPROPERTY(Config) on TObjectPtr<UObject> is rejected by UHT, but UPROPERTY(Config) on a USTRUCT containing an inner UPROPERTY() TObjectPtr<UObject> compiles and appears to work through config import/export.
Is this nested struct case considered a supported pattern, or should object references in config-backed settings use only TSoftObjectPtr, FSoftObjectPath, or TSubclassOf?
Thank you.
Kiril
Steps to Reproduce
1. Create a `UObject`-based settings class with `Config=Game, DefaultConfig`.
2. Add a direct `UPROPERTY(Config) TObjectPtr<UObject>` field and run UHT/build.
3. Observe that UHT rejects the direct object property with `Not allowed to use ‘config’ with object variables`.
4. Replace the direct property with `UPROPERTY(Config)` on a `USTRUCT` that contains an inner `UPROPERTY() TObjectPtr<UObject>`, and compare the result.
Hi,
The USTRUCT wrapper is a loophole in the system. This works because UHT doesn’t recurse the USTRUCT struct properties to check if they are allowed. When the USTRUCT is serialized, it will serialize the UOBJECT path as a string, On load, ImportText parses that path string and calls StaticFindObject / StaticLoadObject to resolve it back to a live UObject*. This should work for asset references (things that live in a package and can be found by path). For anything else, the saved path will fail to resolve on load and silently come back as nullptr. If your intent is to store an asset reference in config, the correct and explicit way is:
UPROPERTY(Config, EditAnywhere)
FSoftObjectPath WrappedObject; // or TSoftObjectPtr<UMyType>
Regards,
Patrick
Hi,
If your UObject* is always a referenced object guaranteed to be a package asset. I think it will be safe in your current engine version. If you upgrade the engine in the future, who knows. Currently, your UObject* is probably loaded synchronously. If you are fine loading synchronously, so I put an AI generated sample showing how to do this (It mimics the TinyFont/TinyFontName UPROPERTY from Engine.h). The ideal solution would be to load them asynchronously (with UAssetManager::GetStreamableManager().RequestAsyncLoad(…)) before you need them, but that might be more work for little gain. I also asked the AI to generate an asynchronous version that I attached to this post.
UCLASS(Config=Game, DefaultConfig)
class UMySettings : public UObject
{
GENERATED_BODY()
public:
// What goes in the .ini, user-editable
UPROPERTY(Config, EditAnywhere, meta=(AllowedClasses="/Script/Engine.Texture2D"))
FSoftObjectPath MyTexturePath;
// Resolved live pointer, kept alive by GC, not collected as long as UMySettings is alive
// Not config, not editable, purely runtime
UPROPERTY()
TObjectPtr<UTexture2D> MyTexture;
// Clean accessor, just a pointer read, zero cost
UTexture2D* GetMyTexture() const { return MyTexture; }
// Called once after the config is loaded and asset loading is safe
void InitializeResolvedReferences()
{
if (!MyTexturePath.IsNull())
{
MyTexture = Cast<UTexture2D>(MyTexturePath.TryLoad());
}
}
};
// --- Initialization ---
// Call this from UGameInstance::Init(), a subsystem's Initialize(),
// or a module's StartupModule(), wherever asset loading is safe.
void UMyGameInstance::Init()
{
Super::Init();
GetDefault<UMySettings>()->InitializeResolvedReferences();
}
// --- Usage ---
void USomeClass::DoSomething()
{
UTexture2D* Texture = GetDefault<UMySettings>()->GetMyTexture();
if (Texture)
{
// use it
}
}
Regard,
Patrick
config_softref_example.txt(7.78 KB)
Do options 2. It’s basically the snippet I shared (I put it back below). ***FSoftObjectPath MyTexturePath;***represents the soft reference, TObjectPtr<UTexture2D> MyTexture; represents the cached pointer. Just resolve the reference synchronously when you want. It doesn’t have to be MyGameInstance::Init(), it can be when the module containing the code setting class loads. To keep everything in memory, just ensure to keep your setting object stay alive if it’s not in the DefaultConfig. (DefaultConfig should keep it the setting object alive and all its UPROPERTIES so your cached pointer)
UCLASS(Config=Game, DefaultConfig)
class UMySettings : public UObject
{
GENERATED_BODY()
public:
// What goes in the .ini, user-editable
UPROPERTY(Config, EditAnywhere, meta=(AllowedClasses="/Script/Engine.Texture2D"))
FSoftObjectPath MyTexturePath;
// Resolved live pointer, kept alive by GC, not collected as long as UMySettings is alive
// Not config, not editable, purely runtime
UPROPERTY()
TObjectPtr<UTexture2D> MyTexture;
....
}
Regards,
Patrick
Thanks for the clarification.
You mentioned that this works for asset references. I want to clarify whether this should still be treated as an unsupported loophole in all cases, or whether it is effectively safe as long as the referenced object is guaranteed to be a package asset.
I also want to ask about the recommended approach for a related case. Suppose a settings object stores a soft object reference, but the referenced asset is expected to remain loaded for the entire application lifetime.
We are currently considering four options:
1. Keep using a TObjectPtr inside a USTRUCT wrapper.
2. Resolve the soft reference on demand in a getter and cache the result in a mutable field.
3. Resolve all soft references during settings object initialization and store the resolved pointers on the CDO.
4. Create a separate subsystem that resolves these references during startup and keeps them alive afterwards.
Which of these approaches would you consider the most correct from an engine-architecture and maintenance perspective?
Thanks.
Hi Patrick,
Thank you for the response, but the provided sample does not fit our setup. Our settings class lives in a separate module, so UGameInstance::Init() is not a viable initialization point.
The assets in question are lightweight — curve assets, data tables with soft references, and similar. They must be loaded synchronously at startup and kept alive for the entire application lifetime. Async loading is not needed here.
Could you share your opinion on the four approaches we listed in the previous message? Specifically, which is closest to how Epic’s teams handle this internally?
Thank you.