I literally did this yesterday 
Thnk @Fuduin for linking, I’ll repost it here
(note: updated post)
Steps:
Preprocessor to remove all bundles (except catalog.bundle) from streaming assets folder
This is to not include them in the base build, they will be added via the AssetPackConfig.
public class AddressablesPlayerBuildAndroidProcessor : IPreprocessBuildWithReport
{
public int callbackOrder => 2;
public void OnPreprocessBuild(BuildReport report)
{
IEnumerable<string> bundles = Directory.GetFiles(Addressables.PlayerBuildDataPath)
.Where(file => file.EndsWith(".bundle"))
.Where(file => !file.EndsWith("catalog.bundle"))
.ToDictionary(file => file, AssetPackBuilder.GetAssetPackGroupSchema)
.Where(pair => pair.Value != null)
.Select(pair => pair.Key);
foreach (string bundle in bundles)
{
File.Delete(bundle);
}
}
}
Callback order is import as you want it happening after the addressables Preprocessor, which copies the asset bundles to the StreamingAssets folder (PlayerBuildDataPath).
Generate AssetPackConfig based on all other bundles.
This is called during our build process after the addressables have been processed.
Update: Use AssetPackGroupSchema to determine which bundles should be included in an asset pack.
class AssetPackBuilder
{
public static AssetPackConfig CreateAssetPacks()
{
IEnumerable<Tuple<string, AssetPackGroupSchema, string>> bundles = Directory.GetFiles(Addressables.BuildPath)
.Where(file => file.EndsWith(".bundle") && !file.EndsWith("catalog.bundle"))
.Select(file => new Tuple<string, AssetPackGroupSchema, string>(file, GetAssetPackGroupSchema(file), Path.GetFileNameWithoutExtension(file)))
.Where(pair => pair.Item2 != null);
foreach (var bundle in bundles)
{
assetPackConfig.AssetPacks.Add(bundle.Item3, bundle.Item2.CreateAssetPack(bundle.Item1));
}
return assetPackConfig;
}
public static AssetPackGroupSchema GetAssetPackGroupSchema(string bundle)
{
return AddressableAssetSettingsDefaultObject.Settings.groups
.Where(group => group.HasSchema<AssetPackGroupSchema>())
.Where(group => Path.GetFileName(bundle).StartsWith(group.Name))
.Select(group => group.GetSchema<AssetPackGroupSchema>())
.FirstOrDefault();
}
}
the result is passed to Bundletool.BuildBundle
Add custom AssetBundleProvider to asset bundles.
Note: I’m using a very custom AssetBundle Provider, which handles delivering asset bundles synchronously after initial launch. As that is part of project I work on, I’m unable to share the entire code. But it is based on the default unity implementations.
Basic idea is that after checking if file path exists locally and before using a web request. The provider will check if the bundle is part of an asset pack. Here I do it very quickly by checking if the path starts with RuntimePath and ends with .bundle.
internal class CustomAssetBundleResource : IAssetBundleResource
{
...
private void BeginOperation()
{
...
if (File.Exists(path))
{
...
}
else if (TryHandleAssetPackFileAsynchronously(path))
{
return;
}
else if (ResourceManagerConfig.ShouldPathUseWebRequest(path))
...
}
private bool TryHandleAssetPackFileAsynchronously(string path)
{
if (!path.StartsWith(Addressables.RuntimePath) || !path.EndsWith(".bundle"))
{
return false;
}
string assetPackName = Path.GetFileNameWithoutExtension(path);
playAssetPackRequest = PlayAssetDelivery.RetrieveAssetPackAsync(assetPackName);
playAssetPackRequest.Completed += request => OnPlayAssetPackRequestCompleted(assetPackName, request);
return true;
}
private void OnPlayAssetPackRequestCompleted(string assetPackName, PlayAssetPackRequest request)
{
if (request.Error != AssetDeliveryErrorCode.NoError)
{
m_ProvideHandle.Complete(this, false, new Exception($"Error downloading error pack: {request.Error}"));
return;
}
if (request.Status != AssetDeliveryStatus.Available)
{
m_ProvideHandle.Complete(this, false, new Exception($"Error downloading status: {request.Status}"));
return;
}
var assetLocation = request.GetAssetLocation(assetPackName);
m_RequestOperation = AssetBundle.LoadFromFileAsync(assetLocation.Path, /* crc= */ 0, assetLocation.Offset);
m_RequestOperation.completed += LocalRequestOperationCompleted;
}
....
}
It’s also possible to load the bundle from the asset pack synchronously
(atleast for fast-install, haven’t tested rest.) (note: this is a requirement for the project I work on, so it’s great that it works ;))
private bool TryHandleAssetPackFileSynchronously(string path)
{
if (!path.StartsWith(Addressables.RuntimePath) || !path.EndsWith(".bundle"))
{
return false;
}
string assetPackName = Path.GetFileNameWithoutExtension(path);
playAssetPackRequest = PlayAssetDelivery.RetrieveAssetPackAsync(assetPackName);
Exception exception = null;
if (playAssetPackRequest.IsDone)
{
// asset pack was downloaded on initial launch of the game, so it should be done when loading it synchrounsly.
var assetLocation = playAssetPackRequest.GetAssetLocation(assetPackName);
m_AssetBundle = AssetBundle.LoadFromFile(assetLocation.Path, /* crc= */ 0, assetLocation.Offset);
}
else
{
exception = new Exception($"Asset Pack was not retrieved asynchronously: '{assetPackName}'.");
}
m_ProvideHandle.Complete(this, m_AssetBundle != null, exception);
return true;
}
Download progress
This is currently not working because addressables still thinks of the asset bundle as being a local bundle and not a remote.
So when it computes the download size it returns 0. The data for this is generated during the building of the asset bundles, changing this would require a custom build script (copy & changing the existing default one doesn’t work as it depends on a lot of internals…).
The solution to this is to call PlayAssetDelivery.GetDownloadSize
for each pack that needs to be downloaded (sadly no combined call for this).
improvements
for production ready code, you’ll probably will need to handle things like waiting for wifi (for large packs larger than 150MB), errors and per bundle configuration so you can specify the AssetPackDeliveryMode for each bundle (probably via adding a Schema to the Group).
[DisplayName("Play Asset Delivery")]
public class AssetPackGroupSchema : AddressableAssetGroupSchema
{
public enum AssetPackDeliveryMode
{
InstallTime = 1,
FastFollow = 2,
OnDemand = 3,
}
[SerializeField]
AssetPackDeliveryMode deliveryMode;
}
note
the current game I’m working is in no rush to add support for this as we are currently projected to be at the AAB cap in 8 to 9 months :), but it was a fun exercise and test to see if this would work.