Asset Manager for Data Assets & Async Loading

What is Asset Manager?

The Asset Manager in Unreal Engine lets you manage your content with more control over loading/unloading and even loading only parts of an asset when set up correctly (by using soft-references to this ‘secondary content’ inside your ‘Primary Assets’ such as an Actor Class soft reference inside your Weapon DataAsset)

I recommend reading the official documentation page as I’ll try not to repeat too much of what is already explained there. Instead I’ll use this article to be more example-driven and from my personal perspective and use-cases.

Your project must define certain classes as Primary Assets (these may often be derived from PrimaryDataAsset but can derive from any UObject). These are the assets you will manage and the system will load/unload any referenced content (also known as ‘secondary assets’) such as meshes and textures. You can turn these ‘secondary assets’ (Everything is considered a Secondary Asset by default) into Primary Assets by overriding GetPrimaryAssetId() from UObject.h:

/**
* Returns an Type:Name pair representing the PrimaryAssetId for this object.
* Assets that need to be globally referenced at runtime should return a valid Identifier.
* If this is valid, the object can be referenced by identifier using the AssetManager 
*/
virtual FPrimaryAssetId GetPrimaryAssetId() const;

An example of a PrimaryAsset is an AI configuration asset that holds info about a specific monster along with which Actor to spawn for this AI, some attributes, abilities, and perhaps some UI stuff like name and icon.

Here is an example of a PrimaryAsset with MonsterData from my ActionRoguelike on GitHub. The use-case is a basic configuration for an AI to be spawned into the world. The actions are its abilities to be granted.

UCLASS()
class ACTIONROGUELIKE_API USMonsterData : public UPrimaryDataAsset
{
 GENERATED_BODY()
public:

 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Spawn Info")
 TSubclassOf<AActor> MonsterClass;
 
 /* Actions/buffs to grant this Monster */
 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Spawn Info")
 TArray<TSubclassOf<USAction>> Actions;

 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
 UTexture2D* Icon;

 FPrimaryAssetId GetPrimaryAssetId() const override
 {
 return FPrimaryAssetId("Monsters", GetFName());
 }
};

Another example of a Primary Asset is a Weapon DataAsset that holds variables and soft references such as the weapon Actor Class, damage type, Icon texture, UI Name, Rarity, etc.

If you are looking for a hands-on look, I recommend checking out my Action Roguelike project on GitHub. It contains some async loading examples using Asset Manager. ActionRPG by Epic uses Asset Manager too (but with blocking loads), still useful to see more use-cases on Primary Assets.

Primary Assets

The Asset Manager in Unreal Engine works through Primary Assets that it loads and unloads per our request. It’s similar to soft references in that regard except we use FPrimaryAssetId (a struct with Type and Name) to point to specific assets we wish to load.

You can either use UPrimaryDataAsset or override GetPrimaryAssetId() in any UObject derived class as mentioned earlier to turn it into a Primary Asset. They look very similar code-wise in the MonsterData example earlier.

Data Asset Examples of ‘Mutations’ in WARPSQUAD.
Data Asset Examples of ‘Ship Configurations’ in WARPSQUAD.

UPrimaryDataAsset (UDataAsset)

DataAsset class already set up to support Asset Manager. These assets will purely hold data and no functional logic. You can include Actor classes to spawn, Abilities to grant, UI names, Icons, etc.

You can think of it as descriptors, to describe the AI minion (hitpoints, abilities to grant, actor class to spawn, behavior tree to use) rather than its actual logic and brains.

FPrimaryAssetId & FPrimaryAssetType

Primary Asset Id & Type are both glorified FNames and categorize/identify the assets. This is how you will point to specific assets that you want to load, and is similar to soft references you may be used to.

For example, my ships are of type ShipConfig and one of the Ids that point to a specific data asset could look like ShipConfig:MyPirateShip. (the Id combines the Type:Name) You won’t be manually typing each Id, instead you can override the GetPrimaryAssetId on your asset in C++ to handle how you want Ids to be generated/handled. You may just want to return the name of your asset file.

Below is an example implementation of setting up the Id for a DataAsset.

FPrimaryAssetId ULZItemData::GetPrimaryAssetId() const
{
    return FPrimaryAssetId(ItemType, GetFName());
}

Asynchronous Loading

This aspect is what I could find the least information on when diving into Asset Manager. So I’d like to share some code examples (also available on GitHub) on how to async load your assets.

C++ Async Loading Example

Loading in C++ works by creating a Delegate with your own set of parameters you wish to pass along with it. In the example below I pass in the loaded Id and a vector spawn location.

// Get the Asset Manager from anywhere
if (UAssetManager* Manager = UAssetManager::GetIfValid())
{
    // Monster Id taken from a DataTable
    FPrimaryAssetId MonsterId = SelectedMonsterRow->MonsterId;

    // Optional "bundles" like "UI"
    TArray<FName> Bundles;

    // Locations array from omitted part of code (see github)
    FVector SpawnLocation = Locations[0]; 

    // Delegate with parameters we need once the asset had been loaded such as the Id we loaded and the location to spawn at. Will call function 'OnMonsterLoaded' once it's complete.
    FStreamableDelegate Delegate = FStreamableDelegate::CreateUObject(this, &ASGameModeBase::OnMonsterLoaded, MonsterId, SpawnLocation);
    
    // The actual async load request
    Manager->LoadPrimaryAsset(MonsterId, Bundles, Delegate);
}

The OnMonsterLoaded Function once load has completed:

void ASGameModeBase::OnMonsterLoaded(FPrimaryAssetId LoadedId, FVector SpawnLocation)
{
    UAssetManager* Manager = UAssetManager::GetIfValid();
    if (Manager)
    {
        USMonsterData* MonsterData = Cast<USMonsterData>(Manager->GetPrimaryAssetObject(LoadedId));

        if (MonsterData)
        {
            AActor* NewBot = GetWorld()->SpawnActor<AActor>(MonsterData->MonsterClass, SpawnLocation, FRotator::ZeroRotator);
        }
    }
}

Example taken from open-source Action Roguelike.

Blueprint Async Loading Example

Async loading is a bit easier in Blueprint as there is a neat little node available.

The downside of async loading in Blueprint is that we can’t pass in additional parameters in our own Delegate as easily as we did in C++ example above where we pass in the FVector for spawn location. You can pass in variables from other pins after the load has completed, but I’m unsure about how these variable values are ‘captured’ and so should be used with caution as they may have changed since you started the load request a few frames ago.

Asset Bundles

Asset Bundles can be used to categorize the soft references inside your PrimaryAsset to a specific type or use-case. eg. in-game or menu. Sometimes you only need a small part of an asset to be loaded (eg. when viewing a weapon purely in UI without the Mesh rendered anywhere). You can mark up those variables with meta = (Asset Bundles = “UI”) (can be any name you decide) and during the async load request, you may specify to only load 1 or more specific bundles instead of the entire asset when no bundles are specified.

UPROPERTY(…, meta = (AssetBundles = "UI"))
TSoftObjectPtr Icon;

/* Optional Action/Ability assigned to Item. Can be used to grant abilities while this item is active/equipped or to simply run item specific functions */
UPROPERTY(…, meta = (AssetBundles = "Actions"))
TArray> ActionClasses;

/* Optional "Weapon" actor and/or world representation of this object if dropped or equipped by a player */
UPROPERTY(…, meta = (AssetBundles = "Actor"))
TSoftClassPtr ActorClass;

Asset Manager Configuration

After configuring your Asset Manager it will automatically discover new PrimaryAssets when added. You setup this configuration in the Project Settings > Asset Manager.

Example Configuration from WARPSQUAD.

What about Streamable Manager?

Asset Manager wraps around the FStreamableManager, which is still a manager you can use for non PrimaryAssets. It’s not a complete replacement, Asset Manager is just solving a specific problem and management.

Auditing Assets

Auditing Assets gives you more insight into how your Primary Assets are setup and used.

right-click on an asset in the content browser lets you “Audit Assets…”. This gives you some insight into the total size associated with an asset, how often it’s used, Ids, Type, etc. Use the buttons at the top to easily filter based on certain criteria.

Audit Assets Window

References

15 Responses

  1. Hello. How to automatically unload UPrimaryDataAsset ? I try different ways, but even if there is no reference to the loaded PrimaryDataAsset , and CG is called, the asset is loaded in memory (node “Get Object from Primary Asset Id” returns a valid reference).

    • Unloading is possible only if you call the “Unload Primary Asset” node, but this node requires a specific PrimaryAsset to be specified. I don’t know how to unload all unused PrimaryAssets from memory.

    • I think by design you are supposed to hold onto the handles you load so you can explicitly unload it. Otherwise Asset Manager will keep it in memory indefinitely as you’ve discovered. This does make it more work to manage, but without that you might also just skip this system entirely and use the basic soft references instead which don’t share this behavior and will GC “more easily”.

  2. Hi, Tom. Great tutorial, thanks. But I have one issue with loading primary asset. I’m building my architecture around simple c++ classes where it’s possible. I’m currently trying to load asset from my factory (simple c++ class) and get the exception “You cannot use UFunction delegates with non UObject classes”. How can I avoid this constraints ?
    Thank you!

  3. – “After configuring your Asset Manager it will automatically discover new PrimaryAssets when added. You setup this configuration in the Project Settings > Asset Manager.”

    but what does it do ? what’s the purpose of this ?

    – why dealing with GetPrimaryAssetId() ? is it to avoid crazy generated hashed id’s ?

    • The asset manager needs to discover these ‘assets’ so it knows what’s available (eg. when assigning these assets by ID in blueprint) and so it knows what to load as all IDs will be known to it.

      That get ID function gives control over what the ID looks like so you’ll indeed end up with something readable instead of some random hash value.

  4. Thanks for this Tom really helpful, I was looking how to do a synchronous primary asset load for a few light weight UI items where Async was adding too much complication for not enough return but I still wanted to be able to leverage the meta data tagging for bundles to just load the UI components from the primary asset.

    Looking at the code in the Epic ActrionRPG Synchronous load would actually load the whole primary asset every time I think which is not what I want.

    I came up with this which seem to work and might help others if you need to do Sync rather than Async nicer than how the Epic example is, but I couldn’t find anything much documented and had to rely on looking at the code so might not be 100% best way to do it.

    Using your Async example the AssetManager LoadPrimaryAsset call also returns a TSharedPtr SH = the LoadPrimaryAsset call with the delegate etc.

    so with that you can check if it’s valid
    if (SH.IsValid())
    {
    SH->WaitUntilComplete();
    /* grab whatever the delegate call returned */
    }
    else
    {
    /*it’s in memory already or it couldn’t resolve the asset ID */
    {

    Looking at WaitUntilComplete in the source code it does just that if the asset is already in memory (or it couldn’t resolve it) then the TSharedPtr returns not valid or you already have a reference as it’s loaded you can reference. If not it keeps setting a timer until it’s loaded (or just hangs forever if something bad happens, to combat that you can also add a timeout to the function call to be on safe side)

    the other trick I’ve been doing is you can try and get a reference to the object in memory first then only try and load it if it’s not there something like below will return the object if it’s loaded nullptr if not but wont try and load it, then if it’s a big asset you can decide how to handle loading it.

    LoadedItem = Cast(AssetManager.GetPrimaryAssetObject(inventoryItemId));

    as always happy to be corrected but it seems to work!

    • can’t post code properly the shared pointer is or type FStreamableHandle which is why you can then call WaitUntilComplete on

  5. Thanks a lot, Tom! This is very informative as usual.
    Could you please elaborate a little on how you can “pass in variables from other pins after the load has completed” when async loading in blueprint?

    • That was a bit tricky for formulate without a screenshot – I meant grabbing data from other variables (pins). Either storing it as a variable before calling the async function (a bit ugly) or simply connecting any variable from the graph that is called before we make the async load request.

  6. Hello, this is a fantastic explanation of how to load specific assets and such at runtime thank you! I did want to ask if this method of loading would also work for loading/showing sub levels at runtime without having to place the sub level beforehand?

    • You can already load sub-levels without adding them beforehand, there is a blueprint node for that available (the exact name slips my mind right now).

      That said, you can still use these Ids on levels too and load assets that way (altho I think you still call that existing level streaming function and not load it via asset manager – you just use the primary Id to know which level you intend to load by converting it to the traditional soft reference) – that’s what I do currently as it’s nice to pass around these level names by primary Id instead of FNames.

Leave a comment on this post!