By default, when packaging a project in UE, BuildCookRun is invoked to execute a series of processes such as Compile/Cook/Pak/Stage. In UE, only the assets involved in Cook will be packaged; however, it often includes many unexpected assets, which can be perplexing. What resources does the engine depend on? And how should we manage the resources involved in packaging in UE?
This article starts with analyzing the rules for resource Cook during UE packaging, studying which resources will actually be Cooked, which is beneficial for resource management. Based on this understanding, a custom Cook process can be implemented, distributing Cook tasks to different processes or even machines to achieve parallelization and accelerate the UE build process.
In addition to resources like uasset, there are many Non-Asset files during packaging, such as ini, Shader Library, AssetRegistry, or script files added to the project, etc. These have been introduced in a previous article UE Hot Update: Demand Analysis and Solution Design. UE’s collection of these resources does not occur during the Cook phase (Shader Library and AssetRegistry are generated during the Cook phase), which will not be discussed further in this article, but a dedicated article will be written later on.
The resources involved in Cook in UE can be logically divided into the following categories:
- Key resources configured in project settings, such as StartupMap, GameMode, GameInstance, DefaultTouchInterface, etc., which are essential resources.
- Several UI directories packaged by default.
- Resources loaded via code during engine startup.
- Cook resources configured in project settings (including resources marked with
Directory to Always Cook
,PrimaryAssetLabel
, etc.). - Resources passed to CookOnTheFlyServer via
FGameDelegates::Get().GetCookModificationDelegate()
. - If no specific resources are specified under certain conditions, resources in project and plugin directories are analyzed.
- Localized resources (UE supports different resources for different cultures, though it is not commonly used).
The resource analysis process in UE is very complex and scattered across various places, and it contains various condition checks. Analyzing the project’s dependent resources accurately is quite inconvenient. This is why it is challenging to ascertain precisely what resources are packaged by UE.
Based on these pain points, I plan to implement a concise and standardized resource collection and packaging process based on HotPatcher. Essentially, the goal is to package the necessary resources that the engine and application need at runtime. This fundamental need leads to the complexity of resource configuration and detection in UE, but we can simplify it for unified analysis and management.
CookCommandlet
The engine provides UCookCommandlet to implement resource Cook, and during the packaging process, it is invoked by UAT. The default Cook command is as follows:
1 | D:/UnrealEngine/Engine/Engine/Binaries/Win64/UE4Editor-Cmd.exe |
It executes to the main function of UCookCommandlet
:
1 | Source\Editor\UnrealEd\Private\Commandlets\CookCommandlet.cpp |
After some parameter checks, it passes the execution flow to CookByTheBook
, creates CookOnTheFlyServer
, and calls StartCookByTheBook
.
All resources during the engine’s packaging are Cooked in CookOnTheFlyServer
, generating Shader and AssetRegistry. It can be said that CookOnTheFlyServer
is the process in which UE packages assets in a common format in the editor, serializing them to platform format.
Resource Analysis
The thought process behind resource loading during UE packaging is as follows: first, find the local uasset files, convert the paths to PackageName, load them; while loading, dependent resources are also loaded and cooked together.
StartupPackages
In CookOnTheFlyServer.cpp
, the resources already loaded into memory are added to CookByTheBookOptions->StartupPackages
:
CookOnTheFlyServer
will subsequently add them to the Cook list and handle redirectors.
AllMaps
If no maps are specified via the command line, AllMaps
will be added to MapIniSections
:
It is a section in DefaultEditor.ini
:
1 | DefaultEngine.ini |
Before starting, a global compilation of GlobalShader
occurs:
Resources are obtained via GRedirectCollector
:
UI
By default, UE adds directories under ContentDirectories
in BaseEditor.ini
to the Cook list: Engine/Config/BaseEditor.ini#L271
The default configuration in the engine is as follows, and other directories can be added by modifying DefaultEditor.ini
:
1 | BaseEditor.ini |
Resources in these directories will be packaged: CookOnTheFlyServer.cpp#L5519
1 | //@todo SLATE: This is a hack to ensure all slate referenced assets get cooked. |
If you do not want the resources under the /Game/UI
directory to be included during default packaging, you can override this in the project configuration file DefaultEditor.ini
:
1 | [UI] |
Directory to Always Cook
Project Settings
-Directory to Always Cook
DirectoriesToAlwaysCook
Maps
AlwaysCookMaps CookOnTheFlyServer.cpp#L5223
MapToCook CookOnTheFlyServer.cpp#L5253
1 | DefaultGame.ini |
Never Cook CookOnTheFlyServer.cpp#L5317
AllMaps CookOnTheFlyServer.cpp#L5266
Cultures
Here it does not refer to multiple languages but to different cultures, allowing for the use of different resources. Asset Localization
Code for obtaining culture resources: CookOnTheFlyServer.cpp#L6714
Resources are read from project settings:
It will traverse all RootPaths, such as /Engine
, /Game
, and root directories of plugins:
For example, resources under the following directory, and will recursively include subdirectories:
1 | /Game/L10N/en/ |
DefaultTouchInterface
DefaultTouchInterface is a configurable virtual joystick class in the engine. It may not be depended on by other resources, but it still needs to be packaged; hence it’s individually retrieved during Cook:
1 | FConfigFile InputIni; |
GetCookModificationDelegate
Binding agents can be passed to CookCommandlet for the files to be Cooked:
1 | // allow the game to fill out the asset registry, as well as get a list of objects to always cook |
Note that the passed resources need to be the absolute paths of uasset files, not resource paths like /Game/xxx
.
AssetManager
Accessing PrimaryAssetTypeInfo
through UAssetManager::Get().ModifyCook
(configured in project settings, PrimaryAssetLabelId, etc.).
The following two are the ones automatically added in new projects; ModifyCook
scans all PrimaryAssetId resources and adds specified resources to PackageToCook. Note that only these resources are added.
If no resources are explicitly specified in the command line, and no resources are obtained from FGameDelegates::Get().GetCookModificationDelegate()
, or UAssetManager::Get().Modify
, all resources from plugins and the project will be added:
The execution criteria are:
AlwaysCookMaps
inDefaultEditor.ini
is empty,AllMaps
is empty.List of maps to include in a packaged build
in project settings is empty.DirectoriesToAlwaysCook
in project settings is empty.- Resources obtained from
FGameDelegates::Get().GetCookModificationDelegate()
are empty. - Resources obtained from
UAssetManager::Get().ModifyCook
are empty.
In this case, it will fetch /Engine
, /Game
, and all enabled plugin umap, uasset through NormalizePackageNames
:
1 | // If no packages were explicitly added by command line or game callback, add all maps |
But with the default filters, it will exclude resources from the engine directory (/Engine
) and from localization directories (/*/L10N/
).
This essentially includes all uasset and umap from projects and plugins, excluding all resources under the L10N
directory.
The core function for collecting packaged content is located in UCookOnTheFlyServer::CollectFilesToCook
: CookOnTheFlyServer.cpp#L5200.
Maps, GameMode, GameInstance set in platform settings: CookOnTheFlyServer.cpp#L5450
DefaultTouchInterface
fromInputIni
: CookOnTheFlyServer.cpp#L5499
1 | DefaultInput.ini |
SkipEditorContent
You can configure this in project settings to ignore Editor-related resources during Cook:
In CookOnTheFlyServer
, it will ignore resources from /Engine/Editor*
and /Editor/VREditor*
:
1 | // don't save Editor resources from the Engine if the target doesn't have editoronly data |
Dependency Loading During Cook
Although the above lists the resources that will be included during the engine’s packaging, they are not everything since they are still individual resources or directories and do not include the dependency relationships of the resources. Therefore, UE will also perform a substantial dependency analysis during its Cook process.
Consider the following two questions:
- If an Actor with a C++ implementation is placed in a map and its constructor code loads a certain resource, how will it be packaged?
1 | AMyActor::AMyActor() |
Placing it in the scene will not generate any dependencies:
- How do we package resources that are not referenced in AssetRegistry dependency relationships? For example,
AnimSequence
configurations such asBoneCompressionSettings
andCurveCompressionSettings
do not appear in the dependency relationships and cannot be found when retrieving dependencies of the animation sequence from the AssetRegistry.
However, they are recorded in the uasset’s ImportTable:
From the Cooked uasset, it can also be seen:
Given this, why can’t we directly scan dependencies from the ImportTable of the assets?
This is because accessing the ImportTable requires actually loading the resource, which can be very resource-intensive and time-consuming with a large number of resources. Accessing dependency relationships from the AssetRegistry does not require loading resources into memory, making it much quicker.
So, how can we solve these issues? We need to start with the default implementation mechanism of UE. It’s essential to clarify two points:
- The engine creates CDO upon startup and executes the class constructor.
- UE’s resources will load their dependent resources when they are loaded.
Based on this thought, UE has implemented a solution during Cook to listen for all created UObjects and add them to the Cook list to ensure resources loaded in C++ constructors and dependent resources loaded through the ImportTable are also cooked.
1 | struct FPackageTracker : public FUObjectArray::FUObjectCreateListener, public FUObjectArray::FUObjectDeleteListener |
Thus, following the same thought process, we only need to use the AssetRegistry
to get the dependencies of the resources, store a rough list of resources, and then during Cook, listen to the creation of UObjects. If they are not in the scanned resource list, we will add them to the Cook queue, thereby achieving a complete resource packaging process.
The comparison between the Cook results without importing the ImportTable (left being the default UE Cook results, right being the custom Cook results):
The comparison of Cook results after tracking the ImportTable (left being the default UE Cook results):
As can be seen, the resources Cooked after tracking the ImportTable are consistent with the resources Cooked by UE by default.
Notes
If you have registered some configurable options in the engine, utilizing FSoftObjectPath
and marking storage as config
on the UObject will imply that the marked resources will be included in the Cook process, which should be avoided. It is advisable to create resource types similar to PrimaryAssetLabel
for resource management marking.
Summary
This article has analyzed the engine’s default resource packaging process, transforming UE’s resource analysis from a black box into a deterministic process. By understanding the analysis results, we can obtain a rough list of packaged resources through dependency analysis and implement complete resource packaging by tracking UObject
creation during Cook, thereby ensuring consistency with the default Cook resources in UE.
Analyzing UE’s resource packaging process allows us to replace the default Cook process in UE and realize multi-process, even cross-machine Cook task distribution, significantly enhancing UE’s packaging efficiency. In the future, I will implement a MultiCook mechanism for HotPatcher.