Say you have a base UItem
class, with a growing number of derived item blueprints. You plan to generate items dynamically in your game, perhaps as quest rewards or loot box contents. How can you get a list of all item classes without having to maintain it manually as new item types (C++ classes and/or blueprints) are added during development?
This is a question I've seen come up a number of times, and it's surprising the engine doesn't provide an easier way to accomplish it. There are various approaches, but quite a few pitfalls too. The method outlined below takes care to avoid all the issues I'm aware of, and should work in both editor and runtime code.
Assume we have an input called Base
of type UClass*
, and an output array called Subclasses
. The code is written with Subclasses
as an array of TAssetSubclassOf
so that we can gather the results without having to load them all. It could just as well be an array of UClass*
, TSubclassOf
, or FStringAssetReference
however.
First, we'll deal with native (C++) subclasses. To do so, we use the TObjectIterator
, which can iterate through all in-memory instances of UObjects of a given type. In our case, we're interested in objects of the type UClass
. Native UClasses will always exist in memory so long as their module is loaded.
for(TObjectIterator< UClass > ClassIt; ClassIt; ++ClassIt) { UClass* Class = *ClassIt; // Only interested in native C++ classes if(!Class->IsNative()) { continue; } // Ignore deprecated if(Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists)) { continue; } #if WITH_EDITOR // Ignore skeleton classes (semi-compiled versions that only exist in-editor) if(FKismetEditorUtilities::IsClassABlueprintSkeleton(Class)) { continue; } #endif // Check this class is a subclass of Base if(!Class->IsChildOf(Base)) { continue; } // Add this class Subclasses.Add(Class); }
We do a few checks to ensure we have a native, genuine UClass
, then finally check to see if it's derived from our given base class.
Finding blueprint classes is more involved, primarily because blueprint classes are not always loaded into memory (That's why we can't rely on the TObjectIterator
method). We're going to use the asset registry module to do most of the work.
The UE4 asset registry maintains basic information on all assets in a project, which can be accessed without needing to load the asset into memory. It's available in editor and runtime builds.
// Load the asset registry module FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< FAssetRegistryModule >(FName("AssetRegistry")); IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); // The asset registry is populated asynchronously at startup, so there's no guarantee it has finished. // This simple approach just runs a synchronous scan on the entire content directory. // Better solutions would be to specify only the path to where the relevant blueprints are, // or to register a callback with the asset registry to be notified of when it's finished populating. TArray< FString > ContentPaths; ContentPaths.Add(TEXT("/Game")); AssetRegistry.ScanPathsSynchronous(ContentPaths); FName BaseClassName = Base->GetFName(); // Use the asset registry to get the set of all class names deriving from Base TSet< FName > DerivedNames; { TArray< FName > BaseNames; BaseNames.Add(BaseClassName); TSet< FName > Excluded; AssetRegistry.GetDerivedClassNames(BaseNames, Excluded, DerivedNames); }
Having got a reference to the asset registry, we first ask it for a list of class names deriving from our base class.
FARFilter Filter; Filter.ClassNames.Add(UBlueprint::StaticClass()->GetFName()); Filter.bRecursiveClasses = true; if(!Path.IsEmpty()) { Filter.PackagePaths.Add(*Path); } Filter.bRecursivePaths = true; TArray< FAssetData > AssetList; AssetRegistry.GetAssets(Filter, AssetList);
The FARFilter
class lets you specify what kind of assets you want to retrieve from the registry. You can filter by class, path and various other values. Be aware though that 'class' in this instance refers to the type of asset and is distinct from the class associated with a blueprint asset. For blueprint assets, the filter class is UBlueprint
for all regular blueprints, UWidgetBlueprint
for UMG widget blueprints, etc. Unfortunately, there's no way to just ask the asset registry directly for asset data on all blueprints deriving from a particular class. So instead, we just grab data for all blueprints (optionally filtered by some path), which we'll then filter further by checking the name against our set of derived class names.
// Iterate over retrieved blueprint assets for(auto const& Asset : AssetList) { // Get the the class this blueprint generates (this is stored as a full path) if(auto GeneratedClassPathPtr = Asset.TagsAndValues.Find(TEXT("GeneratedClass"))) { // Convert path to just the name part const FString ClassObjectPath = FPackageName::ExportTextPathToObjectPath(*GeneratedClassPathPtr); const FString ClassName = FPackageName::ObjectPathToObjectName(ClassObjectPath); // Check if this class is in the derived set if(!DerivedNames.Contains(*ClassName)) { continue; } // Store using the path to the generated class Subclasses.Add(TAssetSubclassOf< UObject >(FStringAssetReference(ClassObjectPath))); } }
We use the TagsAndValues
member of the asset data and look for an entry called 'GeneratedClass'. This is one of the extra bits of data that gets added to the registry for blueprint assets, allowing us to do some inheritance-based checks without needing to load the asset. It stores a full path to the blueprint class.
One easy mistake to make when trying to dynamically find and load blueprint classes is to search for the blueprint itself. Blueprint objects themselves don't make it into a packaged build, but their associated classes (objects of type UBlueprintGeneratedClass
) do. Attempting to load a UBlueprint
in a packaged build will always fail.
Despite this, it seems that the asset data for blueprints is retained, which is crucial to the above code working properly in a packaged build. Attempting to call Asset.GetAsset()
in the loop above would return nullptr
in a packaged build, but since we are able to get the information we need to find the generated class, this doesn't matter.
It's important to note that if using the above code in a runtime (non-editor) module, you will need to ensure that all blueprints you might want to access will get processed during packaging. One way to do so is to designate a folder within your content tree and mark it as always cooked (Project Settings | Packaging | Additional Asset Directories to Cook), and put all the blueprints you intend to gather in this fashion inside it.
The alternative approach is to only use the above code within the editor, and store the results in a UPROPERTY
array of class asset references on an object which you know will be packaged.