Unreal 5 Asset Manager Thoughts and Tests

Example Asset Manager Project

Motivation

There’s a few good resources around on the Asset Manager, but most of them skim over bundles and loading, and most of them don’t mention neat patterns for reuse, so it’s easy to look at the Asset Manager and wonder what it’s actually for when thinking about your project. This page is a collection of notes along with a sample project exploring some useful usage patterns the Asset Manager facilitates.

Sample Project

A sample project is available here with all the code from this post in there. The Asset Manager is enabled by default if you’re porting to your own project, so no need to go hunting in the plugins list.

Asset Manager settings

The upstream doc for the Asset Manager is brief, but covers the basics of the system. Initially, the Asset Manager has two Primary Asset Types configured: Map and PrimaryAssetLabel. By configuring the search directories, this allows available Maps to be listed with no additional configuration using the asset manager:

UAssetManager &Manager = UAssetManager::Get();
TArray<FSoftObjectPath> Maps;
Manager.GetPrimaryAssetPathList(FPrimaryAssetType(TEXT("Map")), Maps);

Each type to be managed is configured by adding an entry to the Primary Asset Types to Scan array in the Asset manager settings.

One of the types in the Example Project

Each primary asset is uniquely identified by its FPrimaryAssetId, which is composed of two FName: an asset type and an asset name. The Primary Asset Type property sets the type, in this case to "ExampleStaticData". This doesn’t need to match the name of the class below, though it’s generally good practice to do so.

The next option is the Asset Base Class, which is the class all Data Assets of this type must inherit from. The Asset Manager will use an Asset Registry query to find filter out any assets that don’t match this base class, unless Has Blueprint Classes has been checked. In this case, in my tests the Asset Base Class was ignored, and in its place the registry filtered on ten blueprint base classes when building the list of Primary Assets:

Effect of setting bHasBlueprintClasses on Registry Filter

Directories and Specific Assets are searched to populate the set of candidate assets, then passed to the Registry filter shown above. Finally, Rules is used to control cooking rules for all assets of this type. This can be overridden by both the global override options below, or by the asset itself as we’ll see with PrimaryAssetLabel.

Global Settings in the Example Project

The global settings for the most part do what they say on the tin. Directories can be excluded wholesale for things like test or debug assets using Directories to Exclude, and there’s some options for overriding cook rules: Primary Asset Rules, Custom Primary Asset Rules, and Only Cook Production Assets.

Should Manager Determine Type and Name is only needed for Primary Assets that don’t implement GetPrimaryAssetId(). I haven’t yet found a compelling reason not to implement GetPrimaryAssetId() so keep this off. Likewise Should Guess Type and Name in Editor is probably just going to cause you trouble when you start testing outside of the editor in packaged builds, so keep that off as well.

I haven’t used Should Acquire Mission Chunks on Load, it sounds like it might be for when chunks are dynamically made available such as with DLC. and Should Warn About Invalid Assets is ok to keep on.

PrimaryAssetLabel

Beyond Map, a second Primary Asset Type comes configured out of the box with UE5 called PrimaryAssetLabel. This serves as a useful demonstration of an Asset Type that isn’t directly linked to a single concept within a project, and instead functions as a higher level abstraction over many assets. They’re also a good demonstration of “Bundles” which are quite simply an FName mapped to a list of asset paths and associated with a Primary Asset. By default they’re stored and updated within the Asset Manager when a Primary Asset is saved.

Out of the box, a Data Asset of this type can build asset bundles from a number of sources:

  • the directory the Data Asset is in,
  • a list of blueprints,
  • a list of other assets,
  • a Collection

The blueprint and other asset lists will be added to an “Explicit” bundle, the directory assets added to a “Directory” bundle, and the collection added to a “Collection” bundle. The directory search is recursive, so any assets in subfolders will also be added.

Concretely, this means creating a PrimaryAssetLabel Data Asset in a directory with some other assets gives a convenient shortcut to load all assets within that directory via the following:

void LoadDirectory(UDataAsset* DataAsset)
{
  UAssetManager &Manager = UAssetManager::Get();
  FPrimaryAssetId PrimaryAssetId = Manager.GetPrimaryAssetIdForObject(DataAsset);
  TArray<FName> Bundles { "Directory" };
  Manager.LoadPrimaryAsset(PrimaryAssetId, Bundles, FStreamableDelegate::CreateUObject(this, &AMyActor::MyFunctionToCallOnLoad));
}

Note that while this example uses the DataAsset object, it’s also completely reasonable to use the FPrimaryAssetId directly, particularly in cases where the Data Assets themselves are left unloaded until needed.

The other notable property on PrimaryAssetLabel is one called “Rules” which allows the cook rules to be overridden for this asset. This is handy if you want to use Data Assets to add or remove things from cooking.

Additional Roles: Decoupling Data and Asset Metadata

By adding more Primary Asset Types, the utility of the Asset Manager can be increased to cover more than just loading or cooking a specified list or a directory of assets. There are two major roles that the Asset Manager and Data Assets can fill with some additional work: decoupling data from code so that blueprints aren’t required to serve both functions, and extending the PrimaryAssetLabel style of Data Asset to include metadata consumed from assets.

Creating a Data Asset

The simplest type of data asset is described in the official doc, and is what I will refer to in the sample and here as a Static Data Asset, since the data is assigned to the Data Asset within fields that are not changeable in the editor. These types of assets are perfectly suited to being consumed via Interface since the properties available are known ahead of time.

In the example project, UExampleStaticData is used to show this concept, with a mesh and a texture soft ref both added to the "MyBundle" bundle.

Data Management

Consume Data Asset via Interface

This strategy was proposed by Michael Allar on Twitter/Youtube and results in a nice system where different consumers of Data Assets share the same Data Asset and consume its parts in different ways, while producers need only push the relevant data asset to the consumer with no additional logic. This means you can have one Data Asset for something like an Item, which contains a Material to be used in a vendor’s shop interface, the stats used for a GameplayAbility and also the Static Mesh used when it’s dropped on the ground.

This strategy is shown in the Example Project, where two trigger boxes with different Data Assets supply consumer actors with their Data Asset when the player enters the trigger volume. Both a Blueprint Class and a Native Class are tested.

This is as simple as creating an interface with a Data Asset child class or PrimaryAssetId as a parameter and having any class that needs to consume the Data Asset implement that interface.

#pragma once

#include "CoreMinimal.h"
#include "ExampleStaticData.h"
#include "UObject/Interface.h"
#include "DataAssetConsumer.generated.h"

UINTERFACE(BlueprintType)
class UDataAssetConsumer : public UInterface
{
	GENERATED_BODY()
};

class ASSETMANAGERDEMO_API IDataAssetConsumer
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
	void ConsumeAsset(UExampleStaticData *StaticDataAsset);

        // This is another option, a little more flexible since the asset doesn't need to be loaded in order to be
        // passed around. I prefer the above since I haven't found a case where a producer should supply a consumer
        // an unloaded Asset, and in general Data Assets themselves have no hard refs and thus can be loaded Synchronously
	// UFUNCTION(BlueprintNativeEvent, BlueprintCallable)
	// void ConsumeAssetId(FPrimaryAssetId AssetId);
};

It’s possible to use a single interface for an entire project, and cast the UDataAsset to the specific type of asset required within the consumer, but I couldn’t think of a good example of a time when the producer doesn’t know what type of asset it’s going to supply to the consumer, so I think for now I’d make one interface per producer-consumer relationship.

Populate Data Asset via Interface

The other major role Data Assets be used for is to easily control when groups of assets are loaded. PrimaryAssetLabel provides a good starting point for this, but can be extended to provide even more utility both for loading and other general categorisation tasks. The sample project contains a Data Asset called ExampleProcessedData, along with a Data Asset DA_ExampleDataAsset in the AssetManagerDemo folder. This class shows how to not only load all the assets in a directory, but also filter them based on a simple regular expression, and store + save metadata about particular assets as well as the bundles for loading.

UExampleProcessedData firstly inherits from PrimaryDataAsset since it provides an implementation of GetPrimaryAssetId() that can be reused without modification. It then overrides PreSave and UpdateAssetBundleData in order to add an additional step during PreSave after the asset’s UPROPERTY have been parsed to populate bundles.

This additional step is based on the PreSave method in PrimaryAssetLabel, and searches a given directory for assets, then places them in a given bundle.

UCLASS()
class ASSETMANAGERDEMO_API UExampleProcessedData : public UPrimaryDataAsset
{
	GENERATED_BODY()
public:
	UExampleProcessedData();

        // Add metadata-specified UPROPERTY assets to bundles as usual
	virtual void PreSave(FObjectPreSaveContext ObjectSaveContext) override;

        // But also do our own processing here and add some customised bundles
	virtual void ProcessData();


#if WITH_EDITORONLY_DATA
	// This gets called during PostLoad and will wipe our customised bundles if left as default
	virtual void UpdateAssetBundleData() override {};
#endif

protected:
	UPROPERTY(EditAnywhere, Category = "Custom Processing")
	FName CustomAssetBundle;

	UPROPERTY(EditAnywhere, Category = "Custom Processing")
	FName CustomAssetBaseDirectory;

	UPROPERTY(EditAnywhere, Category = "Custom Processing")
	bool bCustomAssetRecurseDirectory;
};

PreSave’s implementation can then do the following, so that we get both the automatic bundles and have the chance to do our own processing and make our own bundles.

void UExampleProcessedData::PreSave(FObjectPreSaveContext ObjectPreSaveContext)
{
	Super::PreSave(ObjectPreSaveContext);
	// By default parse the metadata of bundles
	if (UAssetManager::IsValid())
	{
		AssetBundleData.Reset();
		UAssetManager::Get().InitializeAssetBundlesFromMetadata(this, AssetBundleData);

		// Do custom data processing
		ProcessData();

		if (UAssetManager::IsValid())
		{
			// Bundles may have changed, refresh
			UAssetManager::Get().RefreshAssetData(this);
		}

		// Update asset rules
		FPrimaryAssetId PrimaryAssetId = GetPrimaryAssetId();
		UAssetManager::Get().SetPrimaryAssetRules(PrimaryAssetId, Rules);
	}
}
void UExampleProcessedData::ProcessData()
{
	UAssetManager& Manager = UAssetManager::Get();
	IAssetRegistry& AssetRegistry = Manager.GetAssetRegistry();

	FARFilter RegistryFilter;
	RegistryFilter.PackagePaths.Add(CustomAssetBaseDirectory);
	RegistryFilter.bRecursivePaths = bCustomAssetRecurseDirectory;

	TArray<FAssetData> RegistryAssets;
	AssetRegistry.GetAssets(RegistryFilter, RegistryAssets);

	for (const FAssetData& AssetData : RegistryAssets)
	{
		FSoftObjectPath AssetRef = Manager.GetAssetPathForData(AssetData);

		if (!AssetRef.IsNull())
		{
			NewPaths.Add(AssetRef);
		}
	}

	AssetBundleData.SetBundleAssets(CustomAssetBundle, MoveTemp(NewPaths));

This is almost identical to the PrimaryAssetLabel implementation, but instead of grabbing the asset’s current directory, we can specify one of our own, control recursion and also the bundle to be used. Another good way to get more control is by exposing the RegistryFilter itself as a UPROPERTY on the Data Asset.

Where this gets more interesting is by modifying the inner loop and considering a case where an asset has some data on it that we wish to query. A use case I have for this is that I have different biomes in a procedural level generator, each with a FGameplayTagContainer storing tags that describe its properties. I can create a Data Asset that consumes all of my Biome assets within the editor to create a pool of choices for the level generator, and alongside each tileset I can store its Tags without needing to load the tileset itself.

Here is the updated ProcessData that grabs the Tags using an interface from any asset that matches a regex and stores them in a property.

void UExampleProcessedData::ProcessData()
{
	TaggedCustomAssets.Reset();

	UAssetManager& Manager = UAssetManager::Get();
	IAssetRegistry& AssetRegistry = Manager.GetAssetRegistry();

	FARFilter RegistryFilter;
	RegistryFilter.PackagePaths.Add(CustomAssetBaseDirectory);
	RegistryFilter.bRecursivePaths = bCustomAssetRecurseDirectory;

	TArray<FAssetData> RegistryAssets;
	//AssetRegistry.GetAssetsByPath(PackagePath, RegistryAssets, true);
	AssetRegistry.GetAssets(RegistryFilter, RegistryAssets);

	TArray<FSoftObjectPath> NewPaths;

	FRegexPattern Regex = FRegexPattern(InterfaceAssetMatchRegex);

	for (const FAssetData& AssetData : RegistryAssets)
	{
		FSoftObjectPath AssetRef = Manager.GetAssetPathForData(AssetData);

		if (!AssetRef.IsNull())
		{
			NewPaths.Add(AssetRef);

			FRegexMatcher Matcher(Regex, AssetData.ObjectPath.ToString());
			if (!InterfaceAssetMatchRegex.IsEmpty() && Matcher.FindNext())
			{
				UObject* AssetObj = AssetRef.TryLoad();
				UBlueprintGeneratedClass* AssetBP = Cast<UBlueprintGeneratedClass>(AssetObj);
				if (AssetBP)
				{
					auto CDO = AssetBP->GetDefaultObject<AActor>();
					if (CDO && CDO->Implements<UExampleInterface>())
					{
						FGameplayTagContainer Tags = IExampleInterface::Execute_GetTagContainer(CDO);
						if (!Tags.IsEmpty())
						{
							TaggedCustomAssets.Add(AssetRef, Tags);
						}
					}
				}
			}
		}
	}

	AssetBundleData.SetBundleAssets(CustomAssetBundle, MoveTemp(NewPaths));
}

The Asset Path will then be stored along with its FGameplayTagContainer for lookup at runtime, as well as within the bundle for loading.

Processed Data Asset Example

Asset Load Management

Finally, I wanted to double check assets are loaded and unloaded correctly. There is a DemoActor class with several functions that achieve this, and it’s preferable to run this test in standalone mode since the editor itself may hold references to objects that prevent them from being unloaded. A peer had mentioned to me some difficulty in getting a Data Asset to load without also bringing along all of its bundles, which is easily testable using these functions.

UCLASS(Blueprintable)
class ASSETMANAGERDEMO_API ADemoActor : public AActor
{
	GENERATED_BODY()

public:

	UFUNCTION(BlueprintCallable, Category = "Asset Manager Demo")
	void AsyncLoadBundle();

	UFUNCTION(BlueprintCallable, Category = "Asset Manager Demo")
	void PrintDataAssetState();

	UFUNCTION(BlueprintCallable, Category = "Asset Manager Demo")
	void LoadPrimaryAssetObject();
	UFUNCTION(BlueprintCallable, Category = "Asset Manager Demo")
	void UnloadPrimaryAssetObject();
	UFUNCTION(BlueprintCallable, Category = "Asset Manager Demo")
	void UnloadPrimaryAsset();

protected:
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Asset Manager Demo")
	FPrimaryAssetId PrimaryAssetId;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Asset Manager Demo")
	FName Bundle;
};
void ADemoActor::AsyncLoadBundle()
{
	UE_LOG(LogTemp, Verbose, TEXT("Load Primary Asset Bundles"));
	PrintDataAssetState();
	UAssetManager &Manager = UAssetManager::Get();
	TArray<FName> Bundles { Bundle };
	Manager.LoadPrimaryAsset(PrimaryAssetId, Bundles, FStreamableDelegate::CreateUObject(this, &ADemoActor::PrintDataAssetState));
}

void ADemoActor::UnloadPrimaryAsset()
{
	UE_LOG(LogTemp, Verbose, TEXT("Unload Primary Asset"));
	PrintDataAssetState();
	UAssetManager &Manager = UAssetManager::Get();
	Manager.UnloadPrimaryAsset(PrimaryAssetId);
	GEngine->ForceGarbageCollection(true);
	PrintDataAssetState();

}

void ADemoActor::LoadPrimaryAssetObject()
{
	UE_LOG(LogTemp, Verbose, TEXT("Load Primary Asset Object"));
	PrintDataAssetState();


	UAssetManager &Manager = UAssetManager::Get();
	auto Path = Manager.GetPrimaryAssetPath(PrimaryAssetId);

	FSoftObjectPtr Ptr(Path);
	UObject *Obj = Ptr.LoadSynchronous();
	if (!Obj)
		UE_LOG(LogTemp, Error, TEXT("Failed to load primary object %s using path %s"), *PrimaryAssetId.ToString(), *Path.ToString());

	PrintDataAssetState();
}

void ADemoActor::UnloadPrimaryAssetObject()
{
	UE_LOG(LogTemp, Verbose, TEXT("Unload Primary Asset Object"));
	PrintDataAssetState();
	UAssetManager &Manager = UAssetManager::Get();
	auto Path = Manager.GetPrimaryAssetPath(PrimaryAssetId);

	FStreamableManager StreamManager;
	StreamManager.Unload(Path);
	PrintDataAssetState();
}

void ADemoActor::PrintDataAssetState()
{
	UAssetManager &Manager = UAssetManager::Get();
	auto Path = Manager.GetPrimaryAssetPath(PrimaryAssetId);
	FSoftObjectPtr Ptr(Path);
	if (Ptr.IsValid())
		UE_LOG(LogTemp, Warning, TEXT("Loaded: Primary Asset object %s"), *PrimaryAssetId.ToString())
	else
		UE_LOG(LogTemp, Error, TEXT("Not Loaded: Primary Asset object %s"), *PrimaryAssetId.ToString())

	TArray<FAssetBundleEntry> Entries;
	Manager.GetAssetBundleEntries(PrimaryAssetId, Entries);
	for (auto &Entry : Entries)
	{
		for (auto &Asset : Entry.BundleAssets)
		{
			FSoftObjectPtr AssetPtr(Asset);
			if (AssetPtr.IsValid())
				UE_LOG(LogTemp, Warning, TEXT("Loaded: Asset %s in bundle %s"), *Asset.ToString(), *Bundle.ToString())
			else
				UE_LOG(LogTemp, Error, TEXT("Not Loaded: Asset %s in bundle %s"), *Asset.ToString(), *Bundle.ToString())
		}
	}
}

Test Harness

The Demo Actor with the code above is given a BeginPlay in blueprint to run a simple test that loads and unloads the given Primary Asset

Demo Actor Blueprint Graph

To check the results, run in standalone and then check AssetManagerDemo\Saved\Logs\AssetManagerDemo_2.log, which should have the following down the bottom once the test has run (potentially interleaved with other log output):

[2022.03.25-03.06.28:719][  0]LogWorld: Bringing up level for play took: 0.002268
[2022.03.25-03.06.28:726][  0]LogTemp: Error: Not Loaded: Primary Asset object ExampleProcessedData:DA_ExampleDataAsset
[2022.03.25-03.06.28:726][  0]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBWithIface.BP_ExampleActorBWithIface_C in bundle MyBundle
[2022.03.25-03.06.28:726][  0]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBNoIface.BP_ExampleActorBNoIface_C in bundle MyBundle
[2022.03.25-03.06.28:726][  0]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorAWithIface.BP_ExampleActorAWithIface_C in bundle MyBundle
[2022.03.25-03.06.28:726][  0]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorANoIface.BP_ExampleActorANoIface_C in bundle MyBundle

[2022.03.25-03.06.28:744][  0]LogTemp: Warning: Loaded: Primary Asset object ExampleProcessedData:DA_ExampleDataAsset
[2022.03.25-03.06.28:744][  0]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBWithIface.BP_ExampleActorBWithIface_C in bundle MyBundle
[2022.03.25-03.06.28:744][  0]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBNoIface.BP_ExampleActorBNoIface_C in bundle MyBundle
[2022.03.25-03.06.28:744][  0]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorAWithIface.BP_ExampleActorAWithIface_C in bundle MyBundle
[2022.03.25-03.06.28:744][  0]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorANoIface.BP_ExampleActorANoIface_C in bundle MyBundle

[2022.03.25-03.06.32:637][396]LogTemp: Warning: Loaded: Primary Asset object ExampleProcessedData:DA_ExampleDataAsset
[2022.03.25-03.06.32:637][396]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBWithIface.BP_ExampleActorBWithIface_C in bundle MyBundle
[2022.03.25-03.06.32:637][396]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBNoIface.BP_ExampleActorBNoIface_C in bundle MyBundle
[2022.03.25-03.06.32:637][396]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorAWithIface.BP_ExampleActorAWithIface_C in bundle MyBundle
[2022.03.25-03.06.32:637][396]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorANoIface.BP_ExampleActorANoIface_C in bundle MyBundle

[2022.03.25-03.06.32:659][399]LogTemp: Warning: Loaded: Primary Asset object ExampleProcessedData:DA_ExampleDataAsset
[2022.03.25-03.06.32:659][399]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBWithIface.BP_ExampleActorBWithIface_C in bundle MyBundle
[2022.03.25-03.06.32:659][399]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBNoIface.BP_ExampleActorBNoIface_C in bundle MyBundle
[2022.03.25-03.06.32:659][399]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorAWithIface.BP_ExampleActorAWithIface_C in bundle MyBundle
[2022.03.25-03.06.32:660][399]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorANoIface.BP_ExampleActorANoIface_C in bundle MyBundle

[2022.03.25-03.06.35:641][835]LogBlueprintUserMessages: [BP_DemoActor_C_1] Unload
[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Primary Asset object ExampleProcessedData:DA_ExampleDataAsset
[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBWithIface.BP_ExampleActorBWithIface_C in bundle MyBundle
[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBNoIface.BP_ExampleActorBNoIface_C in bundle MyBundle
[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorAWithIface.BP_ExampleActorAWithIface_C in bundle MyBundle
[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorANoIface.BP_ExampleActorANoIface_C in bundle MyBundle

[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Primary Asset object ExampleProcessedData:DA_ExampleDataAsset
[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBWithIface.BP_ExampleActorBWithIface_C in bundle MyBundle
[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBNoIface.BP_ExampleActorBNoIface_C in bundle MyBundle
[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorAWithIface.BP_ExampleActorAWithIface_C in bundle MyBundle
[2022.03.25-03.06.35:641][835]LogTemp: Warning: Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorANoIface.BP_ExampleActorANoIface_C in bundle MyBundle

[2022.03.25-03.06.38:643][278]LogBlueprintUserMessages: [BP_DemoActor_C_1] Finished
[2022.03.25-03.06.38:643][278]LogTemp: Error: Not Loaded: Primary Asset object ExampleProcessedData:DA_ExampleDataAsset
[2022.03.25-03.06.38:643][278]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBWithIface.BP_ExampleActorBWithIface_C in bundle MyBundle
[2022.03.25-03.06.38:643][278]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorBNoIface.BP_ExampleActorBNoIface_C in bundle MyBundle
[2022.03.25-03.06.38:643][278]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorAWithIface.BP_ExampleActorAWithIface_C in bundle MyBundle
[2022.03.25-03.06.38:643][278]LogTemp: Error: Not Loaded: Asset /Game/AssetManagerDemo/ProcessedDataExampleAssets/BP_ExampleActorANoIface.BP_ExampleActorANoIface_C in bundle MyBundle

We can see that initially nothing was loaded, then the Data Asset was loaded without its bundles, allowing us to query the TagContainers and available assets, then later once we loaded the bundles all the assets were pointing to live Objects, and then after an Unload and forcing Garbage Collection everything is unloaded again. Note: don’t force GC at runtime unless you really know what you’re doing, it’s only used here as part of testing.

Future Work

Since we’ve cleanly separated the code from the data, we could theoretically serialise all the data in the game to text much more easily than in the previous situation where there might be blueprint graphs. There’s a plugin here that might help with this, the core of which is Open Source.