In previous articles, we introduced the implementation mechanism of UE hot updates and the automation process of hot updates. Recently, we plan to continue writing a few articles to discuss the process and rules of resource package management in UE hot updates.
Of course, different types of projects will have different packaging strategies, and resource management does not have a universal best strategy. This article mainly introduces the engineering practice of splitting the base package in the hot update process, involving the modification of the engine to implement a common splitting method for Android/iOS. We hope to provide some useful ideas for projects in different businesses.
In practical engineering, we abandoned the default package splitting scheme provided by UE and implemented a flexible package splitting scheme based on the HotPatcher framework. For details, see the article: Resource Management: Reshaping UE’s Package Splitting Scheme.
When projects develop to the mid- to late-stages, there will be a large number of maps and art resources. For mobile games, the size of the package is also quite sensitive, and Android has a 2G package size limitation. Therefore, when a project reaches a certain stage, splitting the package becomes a necessary consideration, and a giant installation package is not conducive to promotion.
In the case of hot updates, splitting the base package needs to consider two factors:
- Reducing the package size without affecting gameplay
- Minimizing players’ download waiting time
The trade-offs and implementations of these two factors are more or less related to specific game businesses. Here, we will only split the base package from the implementation level; different businesses can split according to their own business rules.
UE’s resource management can split resources during packaging, using Asset Manager
or AssetPrimaryLabel
to categorize resources by type, directory, map, dependency analysis, etc. This is the so-called Chunk mechanism.
For the operation method of Chunk division, refer to UE’s documentation:
UE’s Chunk mechanism can turn a single giant pak that is created by default into several smaller pak files based on the specified splitting granularity (when Priority
is the same, there may be a situation where a resource exists in multiple chunks). If you take maps and their dependent resources as the chunk division mechanism, you can include the initial key maps in the base package and dynamically download the remaining resources during hot updates or runtime.
The key to splitting the base package lies in two points:
- Being able to split resources (chunks) based on custom classifications
- Being able to control the rules for packing resources into the base package
The first point can be achieved through the Chunk mechanism of Asset Manager. So, how can the second point be achieved?
Android
For the Android platform, because a package larger than 2GB will cause packaging failures, UE provides the ObbFilter
functionality by default to specify which files should be added to OBB (pak/mp4), etc.
The control method only requires adding configurations.
1 | # Config/DefaultEngine.ini |
The rules of ObbFilters starting with -
serve as exclusion rules, which filter out chunk1-3
pak from the base package and can be used for subsequent download processes.
It is also possible to specify a combination of Exclude
and Include
rules:
1 | +ObbFilters=-*.pak |
The first step ignores all pak files and then explicitly adds pakchunk0-*.pak
to the obb.
iOS
However, iOS does not provide this functionality. To achieve iOS filtering mechanisms similar to Android, I looked into the UE packaging code for iOS, and it can be implemented in the following way (modifying the iPhonePackager
code). The thought and implementation process is recorded as follows.
Note: The IPA packaging method I use is remote building. For details, see the previous article: UE4 Development Notes: Mac/iOS.
In the earlier sections, it was mentioned that UE provides file filtering rules for packaging into OBB for Android:
1 | # Config/DefaultEngine.ini |
However, UE does not provide a corresponding operation for iOS. By default, all pak files for iOS are packaged into the IPA.
To unify the base package rules of Android and iOS, I implemented a filtering rule functionality on iOS similar to Android. Here’s a simple introduction.
I used Mac for remote packaging. The process involves compiling code to generate IPA on Mac, pulling it back to Windows for cooking, generating Pak files, and finally unpackaging the original IPA, adding Pak and other files to form the final IPA.
My requirement is to specify custom filtering rules that can ignore certain files and not pack them into the IPA. This operation actually lies within the process of unpackaging and repackaging the IPA. After reviewing UE’s code, I found that this operation is performed through the iPhonePackager
standalone program, which means that I need to modify the code of this program.
After debugging and analysis, I found that the actual operation of repackaging the IPA occurs in the following function:
1 | Programs/IOS/iPhonePackager/CookTime.cs |
This function is located in the iPhonePackager
- CookTime
class.
1 | static public void RepackageIPAFromStub() |
The operation to perform is to intervene in this process and filter the list of files in PayloadFiles
based on our custom rules.
The process can be divided into the following steps:
- Read Filter configuration from the project
- Create the actual filter
- Use the filter to check whether files should be included in the IPA during the
RepackageIPAFromStub
file traversal process
This can be achieved with just a few dozen lines of code. First, an IniReader class needs to be added:
1 | using Tools.DotNETCommon; |
Then, create the filter in the RepackageIPAFromStub
function:
1 | FileFilter IpaPakFileFilter = new FileFilter(FileFilterType.Include); |
Here, the value of IPAFilters
is read from the project’s Config/DefaultEngine.ini
under [/Script/IOSRuntimeSettings.IOSRuntimeSettings], with rules similar to Android, allowing specification of both Exclude
and Include
rules, but all rules must be written in one line and separated by commas.
1 | [/Script/IOSRuntimeSettings.IOSRuntimeSettings] |
Finally, you also need to detect whether the files match our specified filtering rules during the iteration of Payload
files in RepackageIPAFromStub
:
1 | static public void RepackageIPAFromStub() |
Now, when packaging for iOS, files will be added according to the specified filtering rules, achieving consistent behavior with Android.
The log during the packaging process is as follows (the above code has been commented out):
1 | Saving IPA ... |
As can be seen, the filtering rules have taken effect. Additionally, this will not have any other adverse effects on the packaged bundle (of course, this may result in losing resources by default, and a download mechanism needs to be implemented, which can refer to my previous articles in the hot update series).
Note: Since iPhonePackager is a Program-type program that does not depend on the engine, it can be compiled and then copied for use with non-source versions of the engine.
Note: In non-remote builds, you cannot modify the iPhonePackager code when packaging the iOS package directly on Mac, as non-remote builds will not utilize it. To achieve the same effect, it is necessary to modify the process in IOSPlatform.Automation.cs
, adding the above code to the Package
function to implement the filtering behavior.
Furthermore, by default, UE should not compile the C# program’s project on Mac. You can modify the AutomationTool on Windows and then copy it to Mac.
Priority Packaging Fails
When splitting the base package, I wanted to minimize resource redundancy by controlling the PrimaryAssetLabel
‘s Priority
, but tests showed that it did not work (resources with both high and low priority were present in the same chunk):
Looking at the code in the engine, resource management is obtained via AssetManager::GetPackageManagers
:
1 | Engine\Source\Runtime\Engine\Private\AssetManager.cpp |
By default, the Priority
value is not utilized at all.
Thus, I need to modify this part of the engine’s implementation so that when retrieving the resource’s associated PrimaryAssetId, it captures the list of Labels with the maximum (or highest identical) Priority
. However, if labels are used at runtime, it becomes a bit counterintuitive. It is recommended that the Priority
function be effective only during packaging, and at runtime, loading a PrimaryAssetLabel
should not affect resource management. The engine launch parameters can be checked through FParse::Param
to detect this.
1 | FPrimaryAssetId AddPrimaryAssetId = PrimaryAssetId; |
By enabling -labelpriority
during cook time, resources will only exist in chunks with a higher Priority
value.
End
Through the above operations, you can realize consistent base package filtering rules for Android/iOS, packing the most critical resources into the base package. The remaining pak files can be extracted from Saved/StagedBuilds
after completing the base package, to be downloaded at runtime according to the startup of the hot update platform, or designed according to the project’s type and requirements for a runtime download scheme.