
Content:
Hello!
Today I will talk about how to handle assets on the
Unreal Engine 4 so that it would not be excruciatingly painful for the aimlessly busy memory and moans of players during the loading of your game.
One of the unobvious features of the engine is that for all objects that are somehow affected through the reference system, so-called
Class Default Object (CDO) is stored in memory. Moreover, for the full functioning of objects, all the resources mentioned in them — meshes, textures, shaders, and others — are loaded into memory.
')
As a result, in such a system, it is necessary to closely monitor how the tree of connections of your game objects is “turned around” in memory. It is easy to give an example when introducing the simplest condition from the discharge — if the player is currently controlling the apple — the Buy More Apples Right Now! Button will be shown to him — will pull half of the textures of the entire interface behind him, even if the user plays only a pear character .
Why? The scheme is very simple:
- HUD checks for which class the player is, thereby loading the Apple class into memory (and all that is mentioned in the Apple);
- If the check was successful, the widget widget is created (it is mentioned directly -> it loads immediately);
- Buy by clicking on should open the Premium Shop window;
- Premium Shop , depending on certain conditions, is able to show the screen Clothes for a Character , which uses 146 icons of clothes and 20 models of different stones and fruit barrels for each class.
The tree will continue to unfold up to all its leaves, and in this way, seemingly completely harmless checks and references to other classes (even at the level of Cast!) - you will have in your memory entire groups of objects that the player will never need in this moment of gameplay.

At some point during development this will become critical for your game, but not immediately (the memory thresholds of even modern mobile devices are very high). At the same time, design errors of this kind are very difficult and unpleasant to correct.
I want to give a few practical solutions that I use all the time myself, which can serve as an example of resolving such situations, and can be easily expanded to meet the needs of your project.
Step 1. Using special pointers to assets
In order to interrupt the vicious practice of loading the entire dependency tree into memory, the gentlemen from Epic Games gave us the opportunity to use two clever types of links to assets, this
TAssetPtr and
TAssetSubclassOf (the only difference between them is that the
classset A, only children of it, which is convenient when class A is abstract).
The peculiarity of using these types is that they do not load resources into memory automatically, only they store references to them. Thus, the resources fall into the assembled project (which did not happen, for example, when the library of characters is stored as an array of text references to assets), but loading into memory occurs only when the developer tells about it.
Step 2. Loading resources into memory on demand
To do this, we need such a thing as
FStreamableManager . In more detail, I will talk about this below in the framework of the examples, so far it is enough to say that the load of assets can be both asynchronous and synchronous, thereby completely replacing the "normal" links to assets.
Examples
The main purpose of the article is to give practical answers to the questions “Who is to blame?” (Direct links to assets) and “What should I do?” (Download them through
TAssetPtr ), so I will not repeat what you can read in the
official documentation engine , and give examples of the implementation of such approaches in practice.
Example 1. Choosing a character
In many games, be it DOTA 2 or World of Tanks - there is an opportunity to watch a character outside of battle. Click on the carousel - and now a new model is displayed on the screen. If there are direct links to all available models, then, as we already know, all of them will fall into memory even at the loading stage. Just imagine - all one hundred and twelve characters pillboxes and immediately in memory! :)
Data structure
To make it easier to upload characters, we will create a sign in which we can get a link to its asset by the character ID.
USTRUCT(Blueprintable) struct FMyActorTableRow : public FTableRowBase { GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) FString AssetId; UPROPERTY(EditAnywhere, BlueprintReadWrite) TAssetSubclassOf<AActor> ActorClass; FMyActorTableRow() : AssetId(TEXT("")), ActorClass(nullptr) { } };
Note that I used the
FTableRowBase class as the parent for our data structure. This approach allows us to create a table for easy editing right in blueprints:


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

After receiving the link to the asset character, 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<AActor> 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<UClass>(StaticLoadObject(UClass::StaticClass(), nullptr, *(Reference.ToString()))); if (ActorClass != nullptr) { // SpawnedActor = WorldContextObject->GetWorld()->SpawnActor<AActor>(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 boot process happens here:
FStreamableManager& AssetLoader = UMyGameSingleton::Get().AssetLoader; FStringAssetReference Reference = AssetPtr.ToStringReference(); AssetLoader.RequestAsyncLoad(Reference, FStreamableDelegate::CreateStatic(&UMyAssetLibrary::OnAsyncSpawnActorComplete, WorldContextObject, Reference, SpawnTransform, Callback));
We use
FStreamableManager to load an asset that is transferred via
TAssetPtr into memory. After the asset is
loaded , the
UMyAssetLibrary :: OnAsyncSpawnActorComplete function will be called, in which we will try to create an instance of the class, and if everything is OK, we will attempt to spawn the ector to the world.
Asynchronous execution of operations implies notification of their execution_ = B8, so at the end we trigger the blueprint event:
Callback.ExecuteIfBound(SpawnedActor != nullptr, Reference, SpawnedActor);
The management of what is happening in blueprints will look like this:


Actually, everything. Using this approach, you can spawn ektor asynchronously, minimally loading the memory of the game.
Example 2. Interface Screens
Remember the example of the button
Need More Yablok , and how she pulled the load to the memory of other screens, which the player does not even see at the moment?
It is not always possible to avoid this by 100%, but the most critical dependence between the windows of the interface is their discovery (creation) on some event. In our case, the button does not know anything about the window that it generates, besides what window itself will need to be shown to the user when clicked.
We will use the knowledge obtained earlier and create an interface screen table:
USTRUCT(Blueprintable) struct FMyWidgetTableRow : public FTableRowBase { GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) TAssetSubclassOf<UUserWidget> WidgetClass; FMyWidgetTableRow() : WidgetClass(nullptr) { } };
It will look like this:

Creating an interface is different from spurs ekorov, so we create an additional function to create widgets from asynchronously loaded assets:
UUserWidget* UMyAssetLibrary::SyncCreateWidget(UObject* WorldContextObject, TAssetSubclassOf<UUserWidget> 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<UClass>(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<UUserWidget>(World, WidgetType); } else { UserWidget = CreateWidget<UUserWidget>(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 check for the validity of 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 difficult business game developers, so such cases will not be superfluous to provide.
Secondly , the widgets do not spawn into the world, the
CreateWidget function is used for them:
UserWidget = CreateWidget<UUserWidget>(OwningPlayer, WidgetType);
Thirdly , if, in the case of an ector, he was born in the world and became part of it, then the widget remains the usual suspended “naked” pointer, which the Enryl garbage collector would happily hunt for. To give it a chance, we enable it to protect it from being devoured by the GC on the current frame:
UserWidget->SetFlags(RF_StrongRefOnFrame);
Thus, if no one takes the baton to himself (the window is not shown to the user, but only created), then the garbage collector will delete it.
And the
fourth , for sweetness - we load the widget synchronously, within one tick:
AssetLoader.SynchronousLoad(Reference);
As practice shows, this is great even for mobile phones, while it is easier to handle the synchronous function - you do not need to start additional download events and in any way handle them. Of course, with this practice, you do not need to do all the long-term 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 a lot of data structures using
TAssetPtr , but you don’t want to create a class for each one in 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 reference to a specific asset type. For example, for texture atlases I use the following structure:
USTRUCT(Blueprintable) struct FMyMaterialInstanceAsset { GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) TAssetPtr<UMaterialInstanceConstant> MaterialInstance; FMyMaterialInstanceAsset() : MaterialInstance(nullptr) { } };
Now you can use the
FMyMaterialInstanceAsset type in
blueprints , and on the basis of it create your own custom data structures that will be used in the tables:

In all other respects, working with this type of data will not differ from the above.
Conclusion
Using
asset references through
TAssetPtr can
greatly reduce the memory consumption of your game and speed up loading time significantly. I have tried to give the most practical examples of using this approach, and I hope they will be useful to you.
The full source code for all examples is available
here .
Comments and questions are welcome.