
Asynchronous (and not so) data loading in Unreal Engine 4

Content:
- Step 1. Using special asset pointers
- Step 2. Loading resources into memory on demand
- Examples
- Conclusion
Hello!
Today I’ll talk about how to handle assets on Unreal Engine 4 so that it does not excruciatingly hurt the aimlessly occupied memory and moaning players during the loading of your game.
One of the unobvious features of the engine is that for all objects affected through the link system, the so-called Class Default Object (CDO) is stored in memory . Moreover, for the full functioning of objects, all the resources mentioned in them are loaded into the memory - meshes, textures, shaders, and others.
As a result, in such a system it is necessary to closely monitor how the tree of connections of your game objects in memory is “expanded”. It is easy to give an example when introducing the simplest condition from the category — if the player is currently controlling the apple, he will be shown the button “Buy More Apples Right Now!” - it will pull the loading of half the textures of the entire interface, even if the user plays only a pear character .
Why? The scheme is extremely simple:
- HUD checks what class the player is, thus loading into memory the Apple class (and everything that is mentioned in Apple);
- If the check was successful, a Buy-Apples widget is created (it is mentioned directly -> it loads immediately);
- Buy apples by pressing should open the Premium Store window ;
- The Premium Store , depending on certain conditions, can display the Clothes For Character screen , which uses 146 clothing icons and 20 models of different seeds and barrels of fruit for each class.
The tree will continue to expand up to all its leaves, and in this way, it would seem, completely harmless checks and references to other classes (even at the Cast level!) - you will have in your memory whole groups of objects that the player will never need in this moment of gameplay.

At some point during development, it will become critical for your game, but not immediately (the memory thresholds are very high even in modern mobile devices). Moreover, design errors of this kind are very difficult and unpleasant to fix.
I want to give some practical solutions that I always use myself, and which can serve as an example of resolving such situations, and can be easily expanded for the needs of your project.
Step 1. Using special asset pointers
To interrupt the vicious practice of loading the entire dependency tree into memory, the gentlemen from Epic Games provided us with the ability to use two tricky types of asset references, TAssetPtr and TAssetSubclassOf (the only difference between them is that in TAssetSubclassOf
The peculiarity of using these types is that they do not automatically load resources into memory, they only store references to them. Thus, the resources fall into the assembled project (which did not happen, for example, when storing the character library in the form of an array of text links to assets), but loading into memory occurs only when the developer says so.
Step 2. Loading resources into memory on demand
To do this, we need such a thing as FStreamableManager . I will discuss this in more detail below as part of the examples, for now it’s enough to say that loading assets can be either asynchronous or synchronous, thereby completely replacing the “normal” links to assets.
Examples
The main goal of the article is to give practical answers to the questions “Who is to blame?” (Direct links to assets) and “What to do?” (Download them via TAssetPtr ), so I won’t repeat what you can read in the official documentation engine , and give examples of the implementation of such approaches in practice.
Example 1. Character selection
In many games, whether it be DOTA 2 or World of Tanks - there is the opportunity to see the character outside the battle. Click on the carousel - and now on the screen displays a new model. If there are direct links to all available models, then, as we already know, all of them will go into memory at the loading stage. Just imagine - all one hundred and twelve Dota characters and immediately in memory! :)
Data structure
To make it convenient to load characters, we will create a plate in which we can get a link to his asset by the character’s identifier.
/**
* Example #1. Table for dynamic actor creation (not defined in advance)
*/
USTRUCT(Blueprintable)
struct FMyActorTableRow : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString AssetId;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TAssetSubclassOf ActorClass;
FMyActorTableRow() :
AssetId(TEXT("")),
ActorClass(nullptr)
{
}
};
Notice that I used the FTableRowBase class as the parent for our data structure. This approach allows us to create a table for easy editing directly in blueprints:


For a note - you may ask, why AssetId , if there is a certain Row Name ? I use an additional key for end-to-end identification of entities within the game, the naming rules of which differ from those restrictions that are imposed on the Row Name by the authors of the engine, although this is not necessary.
Download Assets
The functionality for working with tables in blunts is not rich, but it is enough:

After receiving a link to the character’s asset, the Spawn Actor (Async) node is used . This is a custom node, the following code was written for it:
void UMyAssetLibrary::AsyncSpawnActor(UObject* WorldContextObject, TAssetSubclassOf AssetPtr, FTransform SpawnTransform, const FMyAsyncSpawnActorDelegate& Callback)
{
// Асинхронно загружаем ассет в память
FStreamableManager& AssetLoader = UMyGameSingleton::Get().AssetLoader;
FStringAssetReference Reference = AssetPtr.ToStringReference();
AssetLoader.RequestAsyncLoad(Reference, FStreamableDelegate::CreateStatic(&UMyAssetLibrary::OnAsyncSpawnActorComplete, WorldContextObject, Reference, SpawnTransform, Callback));
}
void UMyAssetLibrary::OnAsyncSpawnActorComplete(UObject* WorldContextObject, FStringAssetReference Reference, FTransform SpawnTransform, FMyAsyncSpawnActorDelegate Callback)
{
AActor* SpawnedActor = nullptr;
// Ассет теперь должен быть в памяти, пытаемся загрузить объект класса
UClass* ActorClass = Cast(StaticLoadObject(UClass::StaticClass(), nullptr, *(Reference.ToString())));
if (ActorClass != nullptr)
{
// Спавним эктора в мир
SpawnedActor = WorldContextObject->GetWorld()->SpawnActor(ActorClass, SpawnTransform);
}
else
{
UE_LOG(LogMyAssetLibrary, Warning, TEXT("UMyAssetLibrary::OnAsyncSpawnActorComplete -- Failed to load object: $"), *Reference.ToString());
}
// Вызываем событие о спавне в блюпринты
Callback.ExecuteIfBound(SpawnedActor != nullptr, Reference, SpawnedActor);
}
The main magic of the download process is here:
FStreamableManager& AssetLoader = UMyGameSingleton::Get().AssetLoader;
FStringAssetReference Reference = AssetPtr.ToStringReference();
AssetLoader.RequestAsyncLoad(Reference, FStreamableDelegate::CreateStatic(&UMyAssetLibrary::OnAsyncSpawnActorComplete, WorldContextObject, Reference, SpawnTransform, Callback));
We use the FStreamableManager to load assets transferred through TAssetPtr into memory . After loading the asset, the UMyAssetLibrary :: OnAsyncSpawnActorComplete function will be called , in which we will already try to create an instance of the class, and if everything is OK, we will try to spawn the ector into the world.
Asynchronous execution of operations involves notification of their execution_ = B8, so at the end we trigger a blunt event:
Callback.ExecuteIfBound(SpawnedActor != nullptr, Reference, SpawnedActor);
Controlling what happens in the blueprints will look like this:


Actually, that's it. Using this approach, you can spawn the ector asynchronously, minimally loading the game’s memory.
Example 2. Interface screens
Remember the example of the NeedMoreApple button , and how did it pull the loading of other screens into the memory that the player does not even see at the moment?
It is not always possible to avoid this at 100%, but the most critical relationship between the windows of the interface is their opening (creation) by any event. In our case, the button does not know anything about the window that it generates, in addition to which window itself will need to be shown to the user when clicked.
We will use the knowledge gained earlier and create a table of interface screens:
USTRUCT(Blueprintable)
struct FMyWidgetTableRow : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TAssetSubclassOf WidgetClass;
FMyWidgetTableRow() :
WidgetClass(nullptr)
{
}
};
It will look like this:

Creating an interface is different from the spawn of ectors, so we will create an additional function for creating widgets from asynchronously loaded assets:
UUserWidget* UMyAssetLibrary::SyncCreateWidget(UObject* WorldContextObject, TAssetSubclassOf Asset, APlayerController* OwningPlayer)
{
// Check we're trying to load not null asset
if (Asset.IsNull())
{
FString InstigatorName = (WorldContextObject != nullptr) ? WorldContextObject->GetFullName() : TEXT("Unknown");
UE_LOG(LogMyAssetLibrary, Warning, TEXT("UMyAssetLibrary::SyncCreateWidget -- Asset ptr is null for: %s"), *InstigatorName);
return nullptr;
}
// Load asset into memory first (sync)
FStreamableManager& AssetLoader = UMyGameSingleton::Get().AssetLoader;
FStringAssetReference Reference = Asset.ToStringReference();
AssetLoader.SynchronousLoad(Reference);
// Now load object and check that it has desired class
UClass* WidgetType = Cast(StaticLoadObject(UClass::StaticClass(), NULL, *(Reference.ToString())));
if (WidgetType == nullptr)
{
return nullptr;
}
// Create widget from loaded object
UUserWidget* UserWidget = nullptr;
if (OwningPlayer == nullptr)
{
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject);
UserWidget = CreateWidget(World, WidgetType);
}
else
{
UserWidget = CreateWidget(OwningPlayer, WidgetType);
}
// Be sure that it won't be killed by GC on this frame
if (UserWidget)
{
UserWidget->SetFlags(RF_StrongRefOnFrame);
}
return UserWidget;
}
There are a few things worth paying attention to.
The first is that we added a validation check on the asset passed to us by reference:
// Check we're trying to load not null asset
if (Asset.IsNull())
{
FString InstigatorName = (WorldContextObject != nullptr) ? WorldContextObject->GetFullName() : TEXT("Unknown");
UE_LOG(LogMyAssetLibrary, Warning, TEXT("UMyAssetLibrary::SyncCreateWidget -- Asset ptr is null for: %s"), *InstigatorName);
return nullptr;
}
Everything can be in our hard work of game developers, so it will not be superfluous to foresee such cases.
Second , widgets will not spawn into the world, the CreateWidget function is used for them : Third , if in the case of an ector it was born in the world and became part of it, then the widget remains the usual suspended “bare” pointer, which the Anryl garbage collector would gladly hunt for. To give him a chance, we enable him protection from devouring by the GC to the current frame: Thus, if no one takes the baton on himself (the window is not shown to the user, but just created), the garbage collector will delete it. And the fourth , for dessert - we load the widget synchronously, within one tick:
UserWidget = CreateWidget(OwningPlayer, WidgetType);
UserWidget->SetFlags(RF_StrongRefOnFrame);
AssetLoader.SynchronousLoad(Reference);
As practice shows, this is great even for mobile phones, and it’s easier to handle the synchronous function - you do not need to start additional loading events and somehow process them. Of course, with this practice, you don’t have to do all the lengthy operations in the widget’s Construct — if necessary, let it appear for the player at the beginning, and then write “download” until all 100500 player items and character models are loaded onto the screen.
Example 3. Data tables without code
What if you need to create many data structures using TAssetPtr , but you don’t want to create a class in each code and inherit from FTableRowBase ? Blueprints do not have this type of data, so you can’t do without code at all, but you can create a proxy class with a link to a specific type of assets. For example, for texture atlases, I use this structure:
USTRUCT(Blueprintable)
struct FMyMaterialInstanceAsset
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TAssetPtr MaterialInstance;
FMyMaterialInstanceAsset() :
MaterialInstance(nullptr)
{
}
};
Now you can use the FMyMaterialInstanceAsset type in blueprints , and use it to create your own custom data structures that will be used in tables:

In all other respects, working with this data type will not differ from the above.
Conclusion
Using asset links through TAssetPtr can cool down your game’s memory usage and speed up loading times. I tried to give the most practical examples of using this approach, and I hope they will be useful to you.
The full source code of all examples is available here .
Comments and questions are welcome.