Optimizing Battle Breakers for chunked downloading
Chunk SetupA chunk is a collection of assets (game content) that can be independently deployed and downloaded. By default, a project will only have one chunk, known as Chunk 0, and all assets will be included in that chunk. After assigning assets to different chunks, Unreal Engine 4’s staging system will create a separate, independent .pak file out of each chunk, which can then be integrated into the appropriate platform-specific deployment system based on your game’s target platform. For Battle Breakers, they turn into files that are downloaded using the HttpChunkInstaller plugin. There is an older method of creating chunks that uses cooker callbacks, but starting with 4.17, the AssetManager is the easiest and most flexible way to do this. To take advantage of these features, a game needs to be set up to create chunks as described in the Packaging documentation.
Battle Breakers (called “World Explorers” in code) has been in development for some time, so the first step was converting it to use the Asset Manager. To do this, we went through and identified which types of game content should be Primary Assets, and set those up in the AssetManagerSettings. In the case of Battle Breakers, there are 28 different Primary Asset Types, but the most interesting pieces of content are maps and characters. Battle Breakers has several hundred characters with unique art, but most players only have access to a portion of those characters. Also, the designers wanted all of the onboarding/tutorial content to be in the initial download to avoid player friction. So, the chunking model that Battle Breakers needed was to have three different types of chunks: Onboarding content in Chunk 0, other maps and shared gameplay content in a set of manually-defined, higher-numbered chunks that are downloaded when the player finishes onboarding, and a set of auto-generated chunks containing the other characters, which are downloaded on demand. Here are some example lines from DefaultGame.ini:
[/Script/Engine.AssetManagerSettings] +PrimaryAssetTypesToScan=(PrimaryAssetType="Character",AssetBaseClass=/Script/WorldExplorers.WExpCharacterDefinition,bHasBlueprintClasses=False,bIsEditorOnly=False,Directories=((Path="/Game/Characters/Classes"), (Path="/Game/World/Map")),Rules=(Priority=1,CookRule=AlwaysCook)) +PrimaryAssetTypesToScan=(PrimaryAssetType="WExpChunkDefinition",AssetBaseClass=/Script/WorldExplorers.WExpChunkDefinition,bHasBlueprintClasses=False,bIsEditorOnly=False,Directories=((Path="/Game/Chunks")))To implement the manual chunks, we created a specific subclass of UPrimaryAssetLabel for Battle Breakers that knows whether the chunk is intended for onboarding, auto-download, or on-demand-downloadable content. Each label has a manually-defined ChunkId and Priority in the Rules section, and specifies an editor collection that can be modified by artists and designers (a new feature in 4.18). Any asset in the collection will be pulled into that chunk, unless it is managed by a higher priority Primary Asset/Label. To create the auto-generated character chunks, we added code to the Battle Breakers-specific subclass of UAssetManager that calls SetPrimaryAssetRules on each character to assign it to a chunk, starting with Chunk 1000. These chunk assignments are saved/loaded to a simple .csv file to make sure they stay constant over time. we also ended up needing to make a few game-specific hacks in the virtual function ShouldSetManager, which is called to determine which assets are managed by a specific Primary Asset ID. Each game will need its own rules for assigning content to chunks, depending on the needs of that specific game. The AssetManager is designed to support this by being subclassed and customized as needed.
Another tool that Battle Breakers used is ChunkDependencyInfo, which specifies parent/child relationships between chunks. To set this up, we added lines like this to DefaultEngine.ini:
[/Script/UnrealEd.ChunkDependencyInfo] +DependencyArray=(ChunkID=100,ParentChunkID=11) +DependencyArray=(ChunkID=101,ParentChunkID=11)
These lines specify that Chunk 100 and Chunk 101 are children of Chunk 11. This means that if any asset is included in Chunk 11, it will not be included in Chunk 100 or Chunk 101. By default, all chunks are children of Chunk 0, meaning that anything in the initial startup chunk will not be wastefully duplicated to other chunks. By correctly setting up the chunk hierarchy, we minimized the required disk size on the player’s device and avoided duplicating content.
Analyzing Chunk AssignmentsAfter initially setting up the chunks, we checked to see if they were producing the results we wanted. They were not. So, to help track down these issues, we added some new tracking tools to 4.19 to show us which assets were going into which chunks, and why. The first tool was the Asset Audit window, accessed from Windows --> Developer Tools --> Asset Audit in the editor. After opening the Asset Audit window, clicking the Add Chunks button will fill in the window with a summary of all chunks that exist in the game:
To inspect an individual chunk, right-click it and use either the Size Map or Reference Viewer to see what it includes. The Size Map shows an all-inclusive view where the contents are grouped based on association with a top-level asset (or Primary Asset ID) and scaled by disk size, while the Reference Viewer shows parent-child relationships between individual assets, with a few filters and options. As an example, we can use the Reference Viewer on Chunk_7 to see why it has 85MB of content:
To get here, we looked at the reference view for Chunk_7 and saw that Chunk_Onboarding was one of the references, so we double-clicked it to focus the reference viewer on it. The Green boxes are Primary Assets, while the grey boxes are actual, on-disk assets. A reference from a Primary Asset to a disk asset is called a Management Reference, and seeing them may require turning on the “Show Management References” option in the upper-left window. In this case, the white lines indicate that these are explicit management references, which means that an asset was explicitly included as part of the primary asset, in this case by the collection described above. If we turned off the Search Breadth Limit option and scrolled down, we would see pink lines representing implicit management references. These are assets that were automatically added to the management set because of recursive references from explicitly managed assets.
The other way to inspect the chunk assignments is to find an asset you are curious about in the content browser, right-click, select Reference Viewer, and then manually turn on Show Management References. This will show which Primary Assets manage the asset, if any:
From this example, we can see that the asset is implicitly managed by a WExpCharacterDisplay primary asset. If we double-click that primary asset, we would see it is assigned to Chunk 1167. This is due to the hard reference from the Display asset in the upper left. If we were to double-click that asset, we would see it is explicitly managed by the primary asset.
Another way to inspect chunk assignments is to add multiple real assets to the Asset Audit window, either by selecting them via the content browser, or by using one of the bulk add options in the Asset Audit window. In this example, we added all Texture2D assets to the Asset Audit window, and then sorted by size. We have also toggled off some Columns to make it easier to read:
In this example, most of the largest textures happen to be in Chunk 2. You could then right-click an individual texture and go to Reference Viewer to see why it’s in that chunk. The Asset Audit window also has some other useful information. The Memory size there is estimated from the ResourceSize, and the Total Usage is the number of Primary Assets that reference that texture, scaled by Primary Asset Priority.
Optimizing Disk SizeWe used the analysis tools to make sure that the chunks were set up how we wanted them, but several of the chunks were still too large. To fix this, we used the same tools to track down where all the space was going. These tools and techniques can actually be used even if you don’t have your game set up for chunking, and were significantly improved for 4.19. To start with we want to look at the actual cooked size on disk, so we’ll go back to the chunks view in the Asset Audit window.
By default, as shown in the screenshot above, this window will display Editor disk sizes, which can be quite different from the sizes of assets in an actual cooked build. But if you have cooked a build locally, that drop down will be populated with the platforms you have cooked content for, so you can see the actual size that will be deployed with your shipped game. Selecting Custom also makes it possible to load a specific DevelopmentAssetRegistry.bin file off of the hard drive. If the build process is set up to archive that file, it can be used to get real size numbers from production builds.
Once the desired platform is selected, right-click it and select Size Map to get an overview of where all the disk size is going. Here is an example of Chunk_0 from Battle Breakers, before we optimized it:
This view shows all of the assets inside the chunk, scaled by size on disk. Each colored box represents a different top level asset or Primary Asset ID. Zooming the the mouse wheel in or double-clicking gives you a better view of a specific group of assets, while zooming out or using the upper left arrow returns to the previous view. Right-clicking an asset allows bringing it to either the reference viewer or Asset Audit view to inspect in more detail. The Size Map view uses information about chunks and primary asset management references to filter the Size Map’s contents, so only assets that are actually in that chunk will be shown.
Because the Size Map assets are scaled based on size, it immediately pointed out several clear paths to optimization. A large bloom texture and some VR-specific assets are not needed for a mobile title, so we removed them from the build entirely. In other cases, we modified the editor collections to move a specific asset between chunks. These same techniques can also be used to optimize memory, as disk size is generally a good proxy for memory. Actually computing memory usage at runtime for a specific platform cannot be done at cook time, so if you switch the Size Map to memory size it uses a less accurate estimation based on the Resource Size.
Using these tools and game-specific optimizations, we were able to create a chunk setup for Battle Breakers that downloads faster for users and is able to be updated by designers when things change. These tools can be used to help organize and optimize any game.