There are a few key factors that go into deciding how many bundles to generate. First, it's important to note that you control how many bundles you have both by how large your groups are, and by the groups' build settings. "Pack Together" for example, creates one bundle per group, while "Pack Separately" creates many. See schema build settings for more information.
Once you know how to control bundle layout, the decision of how to set these up will be game specific. Here are key pieces of data to help make that decision:
Dangers of too many bundles:
Dangers of too few bundles:
Addressables provides three different options for bundle compression: Uncompressed, LZ4, and LZMA. Generally speaking, LZ4 should be used for local content, and LZMA for remote, but more details are outlined below as there can be exceptions to this.
You can set the compression option using the Advanced settings on each group. Compression does not affect in-memory size of your loaded content.
CacheInitializationSettings
. See Initialization Objects for more information about setting this up.Note that the hardware characteristics of a platform can mean that uncompressed bundles are not always the fastest to load. The maximum speed of loading uncompressed bundles is gated by IO speed, while the speed of loading LZ4-compressed bundles can be gated by either IO speed or CPU, depending on hardware. On most platforms, loading LZ4-compressed bundles is CPU bound, and loading uncompressed bundles will be faster. On platforms that have low IO speeds and high CPU speeds, LZ4 loading can be faster. It is always a good practice to run performance analysis to validate whether your game fits the common patterns, or needs some unique tweaking.
More information on Unity's compression selection is available in the Asset Bundle documentation.
Currently there are two optimizations available.
After every new Addressable content build, we produce an addressables_content_state.bin file, which is saved to the folder path defined in the Addressable Assets Settings value "Content State build Path" appended with /. A new content build here is defined as a content build that is not part of the content update workflow. If this value is empty, the default location will be the Assets/AddressableAssetsData/<Platform>/
folder of your Unity project.
This file is critical to our content update workflow. If you are not doing any content updates, you can completely ignore this file.
If you are planning to do content updates, you will need the version of this file produced for the previous release. We recommend checking it into version control and creating a branch each time you release a player build. More information is available on our content update workflow page.
As your project grows larger, keep an eye on the following aspects of your assets and bundles:
For most platforms and collection of content, it is recommended to use Requested Asset and Dependencies
. This mode will only load what is required for the Assets requested with LoadAssetAsync
or LoadAssetsAsync
.
This prevents situations where Assets are loaded into memory that are not used.
Performance in situations where you will load all Assets that are packed together, such as a loading screen. Most types of content will have either have similar or improved performance when loading each individually using Requested Asset and Dependencies
mode.
Loading performance can vary between content type. As an example, large counts of serialised data such as Prefabs or ScriptableObjects with direct references to other serialised data will load faster using All Packed Assets and Dependencies
. With some other Assets like Textures, it is often more performant to load each Asset individually.
When loading Addressables synchronously, there is little performance between between Asset load modes. Because of greater flexibility it is recommended to use Requested Asset and Dependencies
where you know the content will be loaded synchronously.
Note: The above examples are taken for Desktop and Mobile. Performance may differ between platforms. All Packed Assets and Dependencies
mode typically performs better than loading assets individually on the Nintendo Switch.
It is recommended to profile loading performance for your specific content and platform to see what works for your Application.
On loading the first Asset with All Packed Assets and Dependencies
, all Assets are loaded into memory. Later LoadAssetAsync calls for Assets from that pack will return the preloaded Asset without needing to load it.
Even though all the Assets in a group and any dependencies are loaded in memory when you use the All Packed Assets and Dependencies option, the reference count of an individual asset is not incremented unless you explicitly load it (or it is a dependency of an asset that you load explicitly). If you later call Resources.UnloadUnusedAssets
, or you load a new Scene using LoadSceneMode.Single
, then any unused assets (those with a reference count of zero) are unloaded.
When editing Assets loaded from Bundles, in a Player or when using "Use Existing Build (requires built groups)" playmode setting. The Assets are loaded from the Bundle and only exist in memory. Changes cannot be written back to the Bundle on disk, and any modifications to the Object in memory do not persist between sessions.
This is different when using "Use Asset Database (fastest)" or "Simulate Groups (advanced)" playmode settings, in these modes the Assets are loaded from the Project files. Any modifications that are made to loaded Asset modifies the Project Asset, and are saved to file.
In order to prevent this, when making runtime changes, create a new instance of the Object to modify and use as the Object to create an instance of with the Instantiate method. As shown in the example code below.
var op = Addressables.LoadAssetAsync<GameObject>("myKey");
yield return op;
if (op.Result != null)
{
GameObject inst = UnityEngine.Object.Instantiate(op.Result);
// can now use and safely make edits to inst, without the source Project Asset being changed.
}
Please Note, When instancing an Object:
In the most general case, loaded assets no longer have a tie to their address or IResourceLocation
. There are ways, however, to get the properly associated IResourceLocation
and use that to read the field PrimaryKey. The PrimaryKey field will be set to the assets's address unless "Include Address In Catalog" is disabled for the group this object came from. In that case, the PrimaryKey will be the next item in the list of keys (probably a GUID, but possibly a Label or empty string).
Retrieving an address of an AssetReference. This can be done by looking up the Location associated with that reference, and getting the PrimaryKey:
var op = Addressables.LoadResourceLocationsAsync(MyRef1);
yield return op;
if (op.Status == AsyncOperationStatus.Succeeded &&
op.Result != null &&
op.Result.Count > 0)
{
Debug.Log("address is: " + op.Result[0].PrimaryKey);
}
Loading multiple assets by label, but associating each with their address. Here, again LoadResourceLocationsAsync is needed:
Dictionary<string, GameObject> _preloadedObjects = new Dictionary<string, GameObject>();
private IEnumerator PreloadHazards()
{
//find all the locations with label "SpaceHazards"
var loadResourceLocationsHandle = Addressables.LoadResourceLocationsAsync("SpaceHazards", typeof(GameObject));
if( !loadResourceLocationsHandle.IsDone )
yield return loadResourceLocationsHandle;
//start each location loading
List<AsyncOperationHandle> opList = new List<AsyncOperationHandle>();
foreach (IResourceLocation location in loadResourceLocationsHandle.Result)
{
AsyncOperationHandle<GameObject> loadAssetHandle = Addressables.LoadAssetAsync<GameObject>(location);
loadAssetHandle.Completed += obj => { _preloadedObjects.Add(location.PrimaryKey, obj.Result); };
opList.Add(loadAssetHandle);
}
//create a GroupOperation to wait on all the above loads at once.
var groupOp = Addressables.ResourceManager.CreateGenericGroupOperation(opList);
if( !groupOp.IsDone )
yield return groupOp;
Addressables.Release(loadResourceLocationsHandle);
//take a gander at our results.
foreach (var item in _preloadedObjects)
{
Debug.Log(item.Key + " - " + item.Value.name);
}
}
If you have a pre-build step that triggers a domain reload, then you must take special care that the Addressables build itself does not start until after the domain reload is finished.
Using methods such as setting scripting define symbols (PlayerSettings.SetScriptingDefineSymbolsForGroup) or switching active build target (EditorUserBuildSettings.SwitchActiveBuildTarget), triggers scripts to recompile and reload. The execution of the Editor code will continue with the currently loaded domain until the domain reloads and execution stops. Any platform dependant compilation or custom defines will not be set until after the domain reloads. This can lead to unexpected issues where code relies on these defines to build correctly, and can be easily missed.
When building via commandline arguments or CI, Unity recommends restarting the Editor for each desired platform using command line arguments. This ensures that scripts are compiled for a platform before -executeMethod is invoked.
To switch Platform, or modify Editor scripts in code and then continue with the defines set, a domain reload must be performed. Note in this case, -quit argument should not be used or the Editor will exit immediately after execution of the invoked method.
When the domain reloads, InitialiseOnLoad is invoked. The code below demonstrates how to set scripting define symbols and react to those in the Editor code, building Addressables after the domain reload completes. The same process can be done for switching platforms and platform dependant compilation.
[InitializeOnLoad]
public class BuildWithScriptingDefinesExample
{
static BuildWithScriptingDefinesExample()
{
bool toBuild = SessionState.GetBool("BuildAddressables", false);
SessionState.EraseBool("BuildAddressables");
if (toBuild)
{
Debug.Log("Domain reload complete, building Addressables as requested");
BuildAddressablesAndRevertDefines();
}
}
[MenuItem("Build/Addressables with script define")]
public static void BuildTest()
{
#if !MYDEFINEHERE
Debug.Log("Setting up SessionState to inform an Addressables build is requested on next Domain Reload");
SessionState.SetBool("BuildAddressables", true);
string originalDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
string newDefines = string.IsNullOrEmpty(originalDefines) ? "MYDEFINEHERE" : originalDefines + ";MYDEFINEHERE";
Debug.Log("Setting Scripting Defines, this will then start compiling and begin a domain reload of the Editor Scripts.");
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, newDefines);
#endif
}
static void BuildAddressablesAndRevertDefines()
{
#if MYDEFINEHERE
Debug.Log("Correct scripting defines set for desired build");
AddressableAssetSettings.BuildPlayerContent();
string originalDefines = PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
if (originalDefines.Contains(";MYDEFINEHERE"))
originalDefines = originalDefines.Replace(";MYDEFINEHERE", "");
else
originalDefines = originalDefines.Replace("MYDEFINEHERE", "");
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup, originalDefines);
AssetDatabase.SaveAssets();
#endif
EditorApplication.Exit(0);
}
}