Unreal Engine Asset Management Summary

[USparkle Column] If you have special skills, love to "do some research", are willing to share and learn from others' strengths, we look forward to your joining, let the sparks of wisdom collide and intertwine, and let the transfer of knowledge continue!

I. Introduction

When we open the game engine, the first thing we do should be to import or manually create some resources needed for development, which may be models (Mesh), textures (Texture) or scenes (Map). It seems natural that these resources exist in the engine. However, when the engine packages these resources and generates the game package body, how does it determine the reference relationship between each other and how does it manage the loading and release of these resources in the memory? We also How to manually manage memory assets and how to optimize memory and game package size. To answer these questions, we must first understand how the engine manages assets.

This article will introduce Unreal Engine's asset management, manual management of assets required for the game, resource loading and release, asset reference relationships, and settings for packaged game assets. The full text is approximately 12,000 words. Please correct me if there are any deficiencies.

2. What is an asset?

First of all, it is important to understand the concept of assets, which is the basis for solving the above problems (manual management of assets required by the game, loading and releasing of assets). So, what is an asset? Are stickers considered assets? Sort of. Is Mesh an asset? Sort of. Are blueprints considered assets? Sort of. In fact, the engine has already told us through the file suffix: files with the suffix uasset are all assets in the eyes of Unreal Engine, but in addition to uasset, there is another file in the content folder: umap, which is also an asset, and is A "higher level" Primary Asset, the meaning of which will be explained later.

Using Tools
The following tool environments are used for code analysis:

  • Windows 10
  • UE 5.0.1
  • Visual Studio 2022

The essence of uasset
: First of all, uasset is represented on the disk as a file. When we reference a certain uasset asset in the engine, it will be loaded into the memory through the path, and each uasset will be consistent with the UPackage class object. One correspondence, and then when storing a UPackage object, all objects under the package will be stored in uasset. The relationship between them is the "Outer" object we often see in C++. Below are other UObjects associated in the UPackage of an empty Actor blueprint asset printed in editor mode, including the corresponding Class and CDO. , blueprint nodes, etc.:

What needs to be noted here is that in editor mode rather than runtime, because the corresponding "Outer" is different in different modes. The "Outer" of the blueprint UObject in the editor is UPackage, while in Runtime it is the "Outer" of Actor. Usually it is the Level where it is located.

So, what is "Outer"? In fact, just translate the meaning of the English word literally. One Object is included by another. For example, there are many Actors in Level and many Components in Actor. Here Level and Actor are the corresponding Outers. You can also get the outermost in C++. The "Outer" object.

Here we need to distinguish between Outer and Owner. The Outer and Owner of Component are generally Actors, and Actors generally do not have an Owner. Outer is the Level where it is located, and under the editor is UPackage.

The printing logic is as follows:

Teacher Dazhao has given detailed explanations and instructions for the process of loading uasset and filling in the relevant details of the path. The video link is placed at the end for reference.

As for the file suffix (uasset or umap), it is defined by the EPackageExtension enumeration. You can also customize the asset suffix to distinguish the type:

/**
 * Enum for the extensions that a package payload can be stored under.
 * Each extension can be used by only one EPackageSegment
 * EPackageSegment::Header segment has multiple possible extensions
 * Use LexToString to convert an EPackageExtension to the extension string to append to a file's basename
 * (e.g. LexToString(EPackageExtension::Asset) -> ".uasset"
 * Exceptions:
 * Unspecified -> <emptystring>
 *  Custom -> ".CustomExtension"; the actual string to use is stored as a separate field on an FPackagePath
 */
enum class EPackageExtension : uint8
{
// Header Segments
/**
* A PackageResourceManager will search for any of the other header extensions
* when receiving a PackagePath with Unspecified
*/
Unspecified=0,
/** A binary-format header that does not contain a UWorld or ULevel */
Asset,
/** A binary-format header that contains a UWorld or ULevel */
Map,
/**
* Used when the owner of an EPackageExtension has a specific extension that does not match
* one of the enumerated possibilies, e.g. a custom extension on a temp file
*/
Custom,
// Other Segments

};

Now, we know that the process of loading uasset is actually loading a series of Objects related to the Object. So, how does the engine manage all the uassets that need to be used? The answer is AssetManager.

3. AssetManager

When you click the run button, the engine will automatically run the logic of the current level. Without manual management, the engine will automatically load the current Level resources for you. This can reduce the developer's workload, but the loading process It seems to have become a black box. This is not something most developers want to face. Sometimes we need to manually manage the loading and releasing of resources, optimize memory, obtain resource loading progress, etc. The engine also provides the corresponding interface, which is AssetManager kind.

AssetManager is a singleton class. After inheriting a custom subclass in C++, it needs to be overridden in the engine settings:

AssetManager provides some resource loading and unloading, the main ones are:

/** Gets the FAssetData for a primary asset with the specified type/name, will only work for once that have been scanned for already. Returns true if it found a valid data */
virtual bool GetPrimaryAssetData(const FPrimaryAssetId& PrimaryAssetId, FAssetData& AssetData) const;

/** Gets list of all FAssetData for a primary asset type, returns true if any were found */
virtual bool GetPrimaryAssetDataList(FPrimaryAssetType PrimaryAssetType, TArray<FAssetData>& AssetDataList) const;

/** Gets the in-memory UObject for a primary asset id, returning nullptr if it's not in memory. Will return blueprint class for blueprint assets. This works even if the asset wasn't loaded explicitly */
virtual UObject* GetPrimaryAssetObject(const FPrimaryAssetId& PrimaryAssetId) const;
virtual TSharedPtr<FStreamableHandle> LoadPrimaryAssets(const TArray<FPrimaryAssetId>& AssetsToLoad, const TArray<FName>& LoadBundles = TArray<FName>(), FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority);

/** Single asset wrapper */
virtual TSharedPtr<FStreamableHandle> LoadPrimaryAsset(const FPrimaryAssetId& AssetToLoad, const TArray<FName>& LoadBundles = TArray<FName>(), FStreamableDelegate DelegateToCall = FStreamableDelegate(), TAsyncLoadPriority Priority = FStreamableManager::DefaultAsyncLoadPriority);
virtual int32 UnloadPrimaryAssets(const TArray<FPrimaryAssetId>& AssetsToUnload);

/** Single asset wrapper */
virtual int32 UnloadPrimaryAsset(const FPrimaryAssetId& AssetToUnload);

Methods exposed into Blueprints:

Three important types appear many times here: PrimaryAsset, PrimaryAssetType and PrimaryAssetId.

  • PrimaryAsset: Main asset, which will be introduced next.
  • PrimaryAssetType: Primary asset type, label for classifying primary assets.
  • FPrimaryAssetId: Primary asset ID, used to uniquely identify the primary asset and maintain PrimaryAssetType and PrimaryAssetName information. How to obtain it in the blueprint:

struct FPrimaryAssetId
{
/** An FName describing the logical type of this object, usually the name of a base UClass. For example, any Blueprint derived from APawn will have a Primary Asset Type of "Pawn".
"PrimaryAssetType:PrimaryAssetName" should form a unique name across your project. */
FPrimaryAssetType PrimaryAssetType;
/** An FName describing this asset. This is usually the short name of the object, but could be a full asset path for things like maps, or objects with GetPrimaryId() overridden.
"PrimaryAssetType:PrimaryAssetName" should form a unique name across your project. */
FName PrimaryAssetName;
}
/**
 * A primary asset type, represented as an FName internally and implicitly convertible back and forth
 * This exists so the blueprint API can understand it's not a normal FName
 */
struct FPrimaryAssetType
{
/** Convert from FName */
FPrimaryAssetType() {}
FPrimaryAssetType(FName InName) : Name(InName) {}
FPrimaryAssetType(EName InName) : Name(FName(InName)) {}
FPrimaryAssetType(const WIDECHAR* InName) : Name(FName(InName)) {}
FPrimaryAssetType(const ANSICHAR* InName) : Name(FName(InName)) {}

/** Convert to FName */
operator FName&() { return Name; }
operator const FName&() const { return Name; }

/** Returns internal Name explicitly, not normally needed */
FName GetName() const
{
return Name;
}

private:
friend struct Z_Construct_UScriptStruct_FPrimaryAssetType_Statics;

/** The FName representing this type */
FName Name;
};

AssetManager provides many methods for loading and unloading main resources and AssetBundle-related functions. So what is a main asset?


A packaged game of Primary and Secondary Assets starts running. If you do not manually specify a "Default Map", the program will not know which scene to start, and it will not know which resources to load. Once a Level is specified as The main scene, the program will load the scene and all referenced resources.

Here, Primary Asset is the Level to be specified, and Secondary Assets are other referenced assets included in the Level, including Mesh, Texture, Audio, etc.

The asset management system in Unreal Engine divides all assets into two types: Primary Assets and Secondary Assets. Primary Assets can be manipulated directly by AssetManager via their primary PrimaryAssetId, which is obtained by calling GetPrimaryAssetId. Secondary Assets are not handled directly by the asset manager, but are loaded automatically by the engine in response to being referenced or used by the primary asset. (Hope the explanation is clear)

The distinction between Primary and Secondary Assets
Although we roughly understand the classification, how do we make the distinction when it comes to specific assets? In fact, there is a very simple way, which is to move the mouse under the corresponding asset, and the asset details will be displayed:

Primary Asset information:

Information about Secondary Assets:

The difference between them is that Primary Asset will be marked with PrimaryAssetType and PrimaryAssetName information.

Blueprints that inherit C++ classes become Primary Assets.
To designate assets from a specific UObject class as Primary Assets, override GetPrimaryAssetId to return a valid FPrimaryAssetId structure, such as:

virtual FPrimaryAssetId GetPrimaryAssetId() const override;

FPrimaryAssetId AMyActor::GetPrimaryAssetId() const
{
UPackage* Package = GetOutermost();

if (!Package->HasAnyPackageFlags(PKG_PlayInEditor))
{
return FPrimaryAssetId(UAssetManager::PrimaryAssetLabelType, Package->GetFName());
}

return FPrimaryAssetId();
}

What is the use of changing it to PrimaryAsset? Of course, its loading is managed through AssetManager.

So far, UWorld is the only primary asset of the engine by default, and the corresponding C++ has also rewritten its GetPrimaryAssetId method:

I just talked about how to convert blueprint assets into main assets, so how should other types such as Mesh or Texture be converted into main assets? The answer is to use Data Asset.

4. Data Asset

The way to create Data Asset is very simple, as shown below:

However, you need to choose a base class that inherits UDataAsset in advance. Of course, if you want to use the main asset, you have to inherit UPrimaryDataAsset, add the corresponding asset reference to the class, and this base class needs to override the GetPrimaryAssetId() method and specify a custom PrimaryAssetType (You can refer to the implementation of map).

It should be noted that the above method of creating a Data Asset actually creates an instance of the selected base class:

After the corresponding Data Asset is created, you can call the method in AssetManager to obtain and load the resource. You can choose to load synchronously or asynchronously:

Wait a minute, do you feel like you've overlooked something? That's right. A very important setting is ignored: telling the engine to specify the location of the PrimaryAsset.

Because as mentioned before, if you don't tell the engine that you need to load PrimaryAsset, such as Default Map, the engine will not load it unless you reference it somewhere. So, where to set it? Look at the picture below:

Required information:

  • Primary Asset Type: PrimaryAssetType specified in the Data Asset base class
  • Asset Base Class: Specified Data Asset base class
  • Has Blueprint Class: Refers to whether this type has a blueprint asset (Data Asset is not a blueprint), which is generally found in C++ overloaded methods.
  • Directories: the folder where the Data Asset is located
  • Rules: I will talk about it later when I talk about packaging and chunking.

The other options can be basically understood by looking at the Tip, so I won’t go into details.

After making the settings, you can use AssetManager to load and unload resources.

Note:
Although AssetManager provides a method to unload a Primary Asset, only when it is ensured that the Primary Asset has no references will it be recycled by garbage collection at the appropriate time.

五、Data Table,Curve Table,Data Registry

In fact, these three brothers have little to do with Data Asset. Their main function is to store data and then load it at runtime. If you want to talk about it, you may have to write a separate article on other topics, so I won’t introduce it here.

So far, the basics I want to talk about have been covered, and the next step is the focus of this article.

6. Asynchronous loading

As mentioned above, when the engine starts to load a certain Level, it loads the corresponding assets and Classes through layer-by-layer references. If a certain Level is associated with PlayerCharacter (basically inevitable), then PlayerCharacter references many other Classes and resources, that means that the engine will take a long time to load these assets, and it is very likely that most of the associated assets will not be used at the moment. The loading speed will be slow and consume a lot of memory. Obviously this is not what we want. Yes, how to solve this kind of memory-related and reference-related problems? In addition to the AssetManager mentioned above for asset management, another thing needs to be involved: asynchronous loading of resources.

Why asynchronous loading of resources can solve the above problems? For example: PlayerCharacter references classes that are not currently needed due to unavoidable reasons. This class or blueprint carries a large number of assets such as Texture that occupy memory resources. If soft references are not used, Or loading these resources asynchronously means that as long as PlayerCharacter exists, the occupied space will not be released (because garbage collection will only reclaim space when there is no reference), and if you use soft references, asynchronously when needed Loading assets by loading prevents all referenced assets from being loaded into memory at once. So what situations are considered to exist references in blueprints? I have summarized the following situations:

The following ways count as cited:

1. Create a variable of a certain type. Regardless of whether it is empty or not, this class and the classes and assets it refers to will be referenced. This is the most direct reference method.

2. CastTo (forced transfer node)
to the corresponding class will also reference the class. It sounds a bit difficult to understand the reference, but the more difficult thing is still to come.

3.Get All Actors/Widgets

The performance of this node is poor. Generally, arrays can be used if they can be stored in arrays, but there is still no way to avoid the reference relationship.
You said it's not possible to use functions to pass parameters? Sorry, it's not possible either.

4. Function parameters and parameter classes will also be regarded as having references.

But the most disgusting thing is this one:

5. Abandoned node variables that are not connected to the blueprint. Because some variables were accidentally deleted and then not connected, the compilation can pass, but there are references to the abandoned variables, so do not think that as long as the blueprint is Once the node is disconnected, it's all over.

For example: a reference to BP_Actor:

The reference to BP_Character comes from here:

So, how to minimize the references between classes, please continue reading.

Dereferencing Dereferencing
is a complex process, which requires a good project structure as the basis. At the same time, when writing the blueprint, we must handle the use of variables and various nodes. We do not need to overdo dereferencing and destroy the project structure or Complicating the implementation of simple logic will result in more losses than gains. There is an appropriate "degree" that needs to be measured based on the project.

Several ways of dereferencing are provided:
1. Soft references are reasonable

But you need to compare the following situations:
Actor base class variables:

Texture2D variables:

Blueprint class variables:

From the prompt, we can draw the conclusion: using C++ as the class as the type of the variable will be regarded as a soft reference, and if the variable is a blueprint class, it will still be directly loaded and referenced. This is why the blueprint class variable in the Reference Viewer will be Reasons for treating it as a hard reference:

In fact, it is generally the case that large assets (Texture, Mesh) and the like are set to Soft Object, and it does not make much sense to set blueprint class variables to Soft Object. The essence of a soft reference is the stored corresponding path class name, which is loaded asynchronously when needed, but think about one thing: when we load the soft reference object and force it into the corresponding type or save it as a variable, soft reference Does the function still exist? Obviously it doesn't make any sense. If you change it to Texture, it won't hurt. If you want to break away from the reference relationship between classes, you might as well try the following two methods.

2. Multiple inheritance and interfaces
If Soft Object is mainly used to handle reference relationships of assets, then inheritance is the way to handle dereferences between classes. Sometimes we will see such a design: a base class blueprint contains most of what it needs, but the assets referenced are all empty, and its subclasses do not have too many overloaded methods and extensions. Assets referenced by subclasses have corresponding resources (hard references or soft references). What are the benefits of doing so? In fact, the advantage is that it avoids the reference between subclasses caused by the CastTo node and only retains the reference relationship between parent classes. This is a better way of dereferencing in my opinion.

3. Delegation
Delegation is simply an artifact of dereferencing. When we hope that there will be no reference relationship between parent classes, then delegation is an excellent choice. Just make time notifications. As for how to use delegation, I don’t know much about it. Having said that, it’s very simple.

In fact, the above methods should be able to meet many situations. If there are other methods, please comment and add.

7. Package reference

The engine loads the required resources starting from the main asset by reference. This is true in the editor, and the same is true for packaged projects. You specify the main asset and traverse all used resources based on the reference relationship. However, it should be noted that when we use the above In the soft reference method, the engine will think that you have not used the asset and will not package it into the game package, or if the required resource does not have a reference anywhere, but the resource needs to be packaged When entering the project, you need to manually add the path to the corresponding asset in the engine settings and tell the engine that the assets in this folder also need to be packaged into the game. Of course, you can also choose which assets should not be packaged into the game:

But the main point I want to say is how to segment the header. Why divide the package into pieces? The reason is very simple. In order to facilitate subsequent updates and maintenance, developers do not want to update the entire game package just because they have made a few changes later. Players also do not want to download the game again just because the developers have corrected a few bugs. Then the solution is to divide the package body into chunks, and then develop DLC. The same principle applies, and the chunking should not be too many. Too many chunks means smaller compression and larger package body, while too few chunks means that the update will be very slow. Trouble, it’s important to make the trade-offs.

PAK chunking
After the game is packaged, the data will be sealed into PAK files, like this:

If you want to segment the main asset, you need to use PrimaryAssetlabel, create a Data Asset and inherit PrimaryAssetlabel.

This is what it looks like after opening:

Parameter description:
Priority: Priority, the larger the value, the higher the management rights for the asset. In other words, if an asset meets two PrimaryAssetlabel conditions at the same time, the asset will be assigned to the one with the larger Priority value.

Chunk ID: The ID of the current chunk, corresponding to the PAK file name, -1 defaults to pakchunk0.

Ccook Rule: introduced below.

Others are relatively simple, that is, select the assets under the corresponding path or specify the assets to be managed by this PrimaryAssetlabel.

After setting up, it is very likely that the same resources have been introduced into different Chunks. So how do we determine which block the asset belongs to and the size of each block, which will be introduced later. Let’s talk about Cook first.

CookWhat
is Cook?

Simply put, Cook is the process of proposing "useless" assets. For example, in the daily development process, we need to debug, and some functions are only available in the editor. The assets will also carry functions that have nothing to do with the official release version or Regarding related content, the process of Cook is to eliminate this type of stuff that we don’t need, and then only keep the parts related to the game. This process is the best Cook.

After understanding the concept of Cook, let’s explain the part about Cook when introducing the parameters above:

  • Unknown: Equivalent to automatic Cook. If there is a reference to this resource, it will be cooked. If there is no reference to this resource, it will not be cooked.
  • Never Cook: Never Cook, if there is a reference to it, an error will be reported
  • Development Cook: Cook will be done if there is a reference under Development packaging conditions.
  • Development Always Cook: Always Cook under Development packaging conditions
  • Always Cook: Always Cook under Development or Ship packaging conditions

Summary: As long as there is a reference to an asset, Cook operations should be performed.

Comparison of Cook’s assets before and after:

Asset size in image engine

Size after Cook

From the comparison, we can see that the change in asset size before and after Cook will generally decrease because the editor-related parts have been removed.

8. Visualization Tools

Some tools were mentioned above, here is a summary:

The reference relationship of an asset in the Reference Viewer
can display Soft Reference and Soft Reference, as well as the corresponding main asset for dereference:

The Audit Asset
asset segmentation tool is used to check the "relatively accurate" size of a certain or segmented asset. It does not represent the final packaged size. If you want to check the size of your own segments, remember to cook the content in advance:

After Cook completes Refresh, there will be more Windows options:

Create a few more and package them. Pay attention to how the assets are divided to prevent the same ones from being placed in two Chunks:

Size Map
is used to check the occupied size of assets and optimize a large asset through it:

The memory command
Memreport will save the current memory usage and can also be used for debugging.

9. Summary

So far, it should be explained clearly how the engine packages and finds the required assets, how the assets are referenced, how to manually manage the loading and unloading of the main assets, and the optimization and debugging of the memory. There are still many details, including UE5 and For new development modes such as Game Feature, as well as specific parameters, please see the official documentation. I will add more when I have time.

Reference
Asset Management

[English live broadcast] Asset Manager Explained | Asset Manager Explained (official subtitles)_bilibili_bilibili

[Chinese Live Broadcast] Issue 33 | UE4 Asset Management Basics 1 | Epic Dazhao_bilibili_bilibili

Package project

Cooking and Chunking


This is the 1461st article of Youhu Technology. Thank you to the author Xue Liuxing for contributing. Welcome to forward and share, please do not reproduce without the author's authorization. If you have any unique insights or findings, please feel free to contact us and discuss it together.

Author's homepage: https://www.zhihu.com/people/xueliuxing

Thank you again Xue Meteor for sharing. If you have any unique insights or findings, please feel free to contact us and discuss it together.

Guess you like

Origin blog.csdn.net/UWA4D/article/details/132901294