Runtime Asset Management
Runtime Asset Management
Prior to 4.16, UE4 has not provided much support for runtime loading/unloading of assets.
There were bits and pieces in StreamableManager, ObjectLibrary, and the Map streaming code,
but there were no examples or documentation. Our internal games have developed their own
systems for asset management, but those have their own problems. The goal of the Asset
Management system added in 4.16 is to provide the basic structure for runtime asset
management and allow individual games to customize things as needed.
Goals
● Improve the existing low level engine systems for async loading assets
● Create a system for managing assets that divides content into understandable chunks,
without losing the advantages of a loose package architecture
● Provide a distinction between “top level” assets that are manually loaded, and other
assets that are loaded automatically as needed
● Provide a centralized, easy to use interface for async loading assets and managing their
memory
● Provide tools to help package related assets into separately cooked/downloaded
bundles
● Provide tools to help audit disk and memory usage
Concepts
● Assets already exist, and are objects that are viewed in the Content Browser. Maps,
texture, sounds, blueprints, etc are assets
● The Asset Registry already exists and is a repository for useful information about
specific assets that is extracted at package save time
● Streamable Managers already exist and are native structs responsible for loading
objects and keeping them in memory
● Primary Assets are assets that can be manually loaded/unloaded based on changes in
game state. This includes maps and game-specific objects such as inventory items or
character classes.
● Secondary Assets are all other assets, such as textures, sounds, etc. These are loaded
automatically based on use by Primary Assets
● An Asset Bundle is a named list of Assets that can be loaded as a group at runtime
● The Asset Manager is a new, optional, global singleton that manages information about
primary assets and asset bundles that is useful at runtime.
Primary Assets
A Primary Asset is a UObject that returns something valid from GetPrimaryAssetId(). This
could be a PrimaryDataAsset/UObject subclass that is directly edited, a Blueprint class, or a
runtime-only UObject that represents other data sources.
● PrimaryAssetType: An FName describing the logical type of this object, usually the
name of a base UClass. For instance if we have blueprints AWeaponRangedGrenade_C
and AWeaponMeleeHammer_C that inherit from native class AWeapon, they would both
have the same primary asset type of “Weapon”
● PrimaryAssetName: 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
● PrimaryAssetType:PrimaryAssetName forms a unique pair across the entire game,
and violations of this will cause errors. This is the string you would use to identify an item
when talking to a persistent back end. For instance, Weapon:BattleAxe_Tier2 represents
the same object as /Game/Items/Weapons/Axes/BattleAxe_Tier2.BattleAxe_Tier2_C
● Type and Name are saved directly as AssetRegistry tags, so once a primary asset has
been saved to disk once you can search for it directly in the asset registry
● Maps inside /Game/Maps are set to be Primary Assets by default, everything else needs
to be set up for your specific game
● PrimaryAssetLabels also live at the engine level and are special Primary Assets that
are used to label other assets for chunking and cooking
Streamable Manager
FStreamableManager is a native structure that handles async loading objects and keeping
them in memory until they are not needed. There are multiple Streamable Managers that are
used for different use cases. This struct already existed, but has been enhanced to work with
Streamable Handles that keep the loaded objects in memory as long as they are needed:
Asset Manager
The AssetManager is a singleton UObject that provides operations for scanning for and loading
Primary Assets at runtime. It is meant to replace the functionality that ObjectLibraries currently
provide, and wraps a FStreamableManager to handle the actual async loading. The engine
asset manager provides basic management, but more complicated things like caching could be
implemented by game-specific subclasses. Basic operations for Asset Manager:
● Get(): Static function to return the active asset manager. Use IsValid() before if unsure
● ScanPathsForPrimaryAssets(Type, Paths, BaseClass): This functions scans the disk
(or cooked asset registry) and parses FAssetData for primary assets of a specific type
● GetPrimaryAssetPath(PrimaryAssetId): Converts Primary Asset to object path
● GetPrimaryAssetIdForPath(StringReference): Converts an object path into a
Type:Name pair if that path refers to a Primary Asset
● GetPrimaryAssetIdFromData(FAssetData): Figures out the Type:Name based on the
FAssetData returned from the Asset Registry
● GetPrimaryAssetData(PrimaryAssetId): Returns FAssetData for a Type:Name combo
● GetPrimaryAssetDataList(Type): Returns a list of all FAssetData for a
PrimaryAssetType
● GetPrimaryAssetObject(PrimaryAssetId): Returns the in memory UObject for a
Type:Name combo if loaded into memory
● LoadPrimaryAssets(AssetList, BundleState, Callback, Priority): Asynchronously
loads the list of primary assets and any assets referenced by BundleState. Returns
FStreamableRequest to allow polling or waiting, and calls callback when complete. This
can also be used to change the asset state for already loaded assets
● UnloadPrimaryAssets(AssetList): Drops hard GC references to the primary assets.
Objects may still be referenced via other systems
● ChangeBundleStateForPrimaryAssets(AssetList, Add, Remove): This operates on a
list of assets and can be used to change bundle state in a more complex way than Load
● GetPrimaryAssetsWithBundleState(): This runs a query to get the list of assets that
match the search criteria, useful to get a list to pass into ChangeBundleState
● Some generally useful utility functions for runtime asset management, such as redirector
support for PrimaryAssetIds
Asset Bundles
An Asset Bundle is a globally named explicit list of assets, associated with a Primary Asset. “All
Player buildable walls”, “First Generation Heroes” and “ArcBlade InGame” are good examples of
bundles:
On Disk Example
Most Primary Assets will have an on-disk representation because they are directly edited by
artists or designers. The easiest way to get this functionality up and running is by inheriting from
PrimaryDataAsset. For non-blueprint assets you can directly inherit from it and your class will
show up in the content browser when making a New Data Asset, for Blueprints you can inherit
from it and it will show up in the New Blueprint class list. Here’s an example of a blueprintable
data-only class from Fortnite:
Because this class inherits from PrimaryDataAsset, it automatically acquires a working version
of GetPrimaryAssetId using the asset’s short name and native class. If there was a
FortZoneTheme called Forest, it would have a primary asset id of FortZoneTheme:Forest. The
AssetBundles tag specifies that the asset reference will be automatically loaded as part of the
Menu bundle. Whenever a Fort Zone Theme is saved, the AssetBundleData member of
PrimaryDataAsset will be updated to include the TheaterMapTileClass asset reference.
Now that the native class is setup, designers can start adding instances of this class. To make
the parsing more efficient and for better organization, all Zone Themes should end up in the
same directory hierarchy. So, in our example the designers added the Zone Themes to sub
directories inside World in the content browser. So the next step is to Scan that directory to load
in the asset’s metadata. To do this from native you would add code like this:
Now that the primary asset is in the dictionary, we need to actually load it at runtime. Here’s
some example code:
This code calls some game-specific logic to figure out if the Menu state should be enabled, and
then passes in a desired list of BundleStates to the LoadPrimaryAsset function. This means that
if you’re in the menu the TheaterMapTileClass reference will get loaded for you, while if you’re
not it will only load the base ZoneTheme asset. The Load function returns a StreamableHandle
that can then be polled or otherwise managed
First, this code unloads any theaters that we previously created as dynamic assets. Then this
code figures out what the new game-global BundleState should be, finds any assets that were
in the old state, then tells those assets to transition to the new one. This allows transitioning all
assets at once between “menu” and “zone” states as an example
Game-Specific Configuration
It’s possible to use the manual method above to scan for Primary Assets, but if your game is
complicated enough to have multiple asset types you will want to use the
PrimaryAssetTypesToScan member of AssetManagerSettings. This is editable from the
[/Script/Engine.AssetManagerSettings] section of DefaultGame.ini, or via the Asset Manager
Project Settings tab. Here’s an example of the Fortnite Asset Manager settings (reformatted for
legibility, each + should start it’s own line):
[/Script/Engine.AssetManagerSettings]
!PrimaryAssetTypesToScan=ClearArray
+PrimaryAssetTypesToScan=(PrimaryAssetType="FortGameData",
AssetBaseClass=/Script/FortniteGame.FortGameData,
bHasBlueprintClasses=False, bIsEditorOnly=False,
SpecificAssets=("/Game/Balance/DefaultGameData.DefaultGameData"),
Rules=(Priority=1000,CookRule=AlwaysCook,ChunkId=0))
+PrimaryAssetTypesToScan=(PrimaryAssetType="Weapon",
AssetBaseClass=/Script/FortniteGame.FortWeaponItemDefinition,
bHasBlueprintClasses=False, bIsEditorOnly=False,
Directories=((Path="/Game/Items/Weapons"),(Path="/Game/Abilities/Player")),
Rules=(Priority=2,CookRule=AlwaysCook,ChunkId=0))
+PrimaryAssetTypesToScan=(PrimaryAssetType="Map",
AssetBaseClass=/Script/Engine.World,
bHasBlueprintClasses=False,bIsEditorOnly=True,
Directories=((Path="/Game/Maps")))
+PrimaryAssetTypesToScan=(PrimaryAssetType="FortZoneTheme",
AssetBaseClass=/Script/FortniteGame.FortZoneTheme,
bHasBlueprintClasses=True, bIsEditorOnly=False,
Directories=((Path="/Game/World/ZoneThemes")),
Rules=(Priority=0))
+PrimaryAssetTypesToScan=(PrimaryAssetType="PrimaryAssetLabel",
AssetBaseClass=/Script/Engine.PrimaryAssetLabel,
bHasBlueprintClasses=False, bIsEditorOnly=False,
Directories=((Path="/Game/Labels")))
This example first clears the values initialized in BaseGame.ini, then adds information for an
always loaded GameData object, Weapons, Maps, Zone Themes and Labels. Here’s what
some of those fields mean:
Once your INI is set up, there’s a good chance you’ll want to override some virtual functions in
your game specific subclass of AssetManager. Here are a few good places to start:
● StartInitialLoading gets called when the asset manager is initially created early in
Engine init, and this is where it does the scanning specified in the ManagerSettings. This
is a good time to do extra scanning
● FinishInitialLoading gets called at the end of Engine init right before the actual game
starts. This is a good time to finalize any loading
● PostInitialAssetScan gets called either from FinishInitialLoading in game, or when the
async asset registry scan finishes in the editor
● ApplyPrimaryAssetLabels gets called whenever the editor needs to set up the
Management database for cooking or the audit tools described below. It load and applies
PrimaryAssetLabels and is a good place to do game-specific chunk overrides
● ModifyCook is called by the cooker, and by default will schedule loading all AlwaysCook
assets, but you can modify it to do what you want
● PreBeginPIE is called right before PIE starts, it is a good time to preload expensive
assets you didn’t want to load on editor startup
Asset Audit UI
Once you’ve set up your game to scan Primary Assets, you can use the Asset Audit UI to
inspect your primary and secondary assets and audit them for memory use, chunking, or
general type-specific metadata. This window is a specialized version of the content browser
that is designed for showing assets in a list format, and with extra information useful in auditing.
There are 3 ways to get to the window:
Once you have the window open, you can add assets to the audit window with the buttons, and
select a platform using the drop down in the upper right. That drop down will be populated with
any local cooked asset registries that are available to the editor. Here's an example of using the
window to audit textures in Shooter Game on PS4, which is a Bottom-Up way of auditing:
To get to this view I cleared assets, clicked "Add Asset Class", selected Texture2d, then sorted
by Total Usage. Total Usage is only available in this window, and the higher the number, the
more Primary Assets use the texture. It's not a strict count, as it weights by Priority. In this
example T_Caustics_2 is the most commonly used texture, as 11 separate maps use it.
To get here I cleared assets, hit "Add Primary Asset Type", selected Map, then sorted by Disk
Kb. Disk Kb is a count of how much space is used by this Primary Asset as well as anything
else it Manages, where Exclusive Disk Kb is just for the specific asset. So in this case Sanctuary
and Highrise are both about the same exclusive size on disk at 20 Mb, but Sanctuary is
managing more textures and meshes so takes a total of 373 Mb on disk. Management can be
shared by types that share the same priority, so a good amount of that 373 Mb is also used by
Highrise. If you set up a type like “SharedAssets” with a higher priority, the Disk Size listed here
would only include assets not managed by SharedAssets.
The best example of how to do this is in the ShooterGame sample, which was modified to use
Asset Manager for Chunking. The chunking setup for ShooterGame is simple, where it wants
the Sanctuary map to be in chunk 1, Highrise to be in chunk 2, and everything else in the default
Chunk 0. Because Maps are now Primary Assets, this is very easy to setup. The first approach
is to use Rule Overrides. These allow setting the priority/chunk settings for a specific primary
asset. Here’s how this would work for Shooter Game:
[/Script/Engine.AssetManagerSettings]
+PrimaryAssetRules=(PrimaryAssetId="Map:/Game/Maps/Sanctuary",
Rules=(Priority=-1,ChunkId=1,CookRule=Unknown))
+PrimaryAssetRules=(PrimaryAssetId="Map:/Game/Maps/Highrise",
Rules=(Priority=-1,ChunkId=2,CookRule=Unknown))
+PrimaryAssetRules=(PrimaryAssetId="Map:/Game/Maps/ShooterEntry",
Rules=(Priority=-1,ChunkId=0,CookRule=AlwaysCook))
This sets specific maps to be in specific chunks, and all of their references will automatically be
added to those chunks as well, with the special rule that anything in the default chunk 0 is only
added to that chunk. This isn’t how chunking is actually set up in ShooterGame, though:
Instead of using ini overrides, you can also set up the chunking rules using
PrimaryAssetLabels. Labels are special assets that aren’t generally directly loaded at runtime,
but can be used to collect assets into functional groups using the Asset Bundle system
described above. Each Label has it’s own Rules that are applied to each of the labelled assets,
so they are great for setting chunking. Here’s the Highrise label from ShooterGame:
In this case the Highrise map is explicitly added to the Label’s ExplicitAsset list, which means
that the map /Game/Maps/Highrise.Highrise is managed by “PrimaryAssetLabel:HighriseLabel”
in addition to being managed by “Map:/Game/Maps/Highrise”. When it goes to decide the
Chunking/Cook rules, it follows this hierarchy. Because there’s no Chunk set for
Map:/Game/Maps/Highrise it uses the ChunkId from the label, which is set to 2. This causes all
of the assets in Highrise to end up in Chunk 2. If you want to inspect the results of the
Management dependency resolution, you can use the Reference viewer, with show
Management References enabled:
This shows the relationship described above, but if you were to double click to center on
Map:/Game/Maps/Highrise it would show it as being managed by
PrimaryAssetLabel:HighriseLabel, resulting in the “inheritance” mentioned above. Setting up
chunking is still a fairly complicated process, but by using the asset manager you can easily try
out different management scenarios, and then load up the Asset Audit window and look at the
Chunk column to see where it will end up when it is eventually cooked. Also, all of the
techniques described here can also be used to set the NeverCook or DevelopmentCook flag,
which can be very useful to for instance stop the released game from cooking things in the
Developers folder.