Using Play Asset Delivery in Unity games

1. Introduction

Last Updated: 2023-06-12

What is Play Asset Delivery

Play Asset Delivery is Google Play's solution for delivering large amounts of game assets by extending the Android App Bundle format. Play Asset Delivery offers developers flexible delivery methods and high performance. It has a small API footprint and is free to use. All asset packs are hosted and served on Google Play so you don't need to use a content delivery network (CDN) to get your game resources to players. Play Asset Delivery includes a feature called Texture Compression Format Targeting (TCFT). With TCFT, you can include multiple versions of texture assets using different texture compression formats inside your asset packs. At install time, Google Play will select the appropriate compression format for a specific device and only download and install texture assets matching the selected compression format. This optimizes install size, since unused compression formats are not downloaded.

What you'll build

You'll be taking a Unity project and modifying it to use Play Asset Delivery to manage distribution of its assets.

What you'll learn

In this codelab, you'll be learning how to:

  • Install the Unity packages for Play Asset Delivery.
  • Create asset packs for use with Play Asset Delivery.
  • Use the Play Asset Delivery plugin for Unity to download asset packs.
  • Access your assets and AssetBundles from asset packs.
  • Test locally on a generated build and also on a build distributed from Google Play.
  • Generate textures in multiple compression formats for TCFT.
  • Handle potential conditions like requesting permission to download large asset packs over a mobile data network if wi-fi is not available.

What you'll need

  • Unity 2018 LTS or higher with Android build support installed.
  • An Android-powered device, connected to your computer, that has Developer options and USB debugging enabled.
  • A Google Developer account, and access to the Play Console to upload your app.

2. Getting set up

Getting the example project

The example project consists of a parent directory containing two subdirectories, start and final. Each subdirectory is a Unity project. The projects were created with Unity 2018.4.27f1. If you are using a later version of Unity, choose to upgrade the project version when opening the project. The start subdirectory has the version of the project we will be modifying in this codelab. The final subdirectory is a reference of what the project should look like at the end of the modifications.

Cloning the repo

The code for this codelab is located in the Android games-samples repository on GitHub. From the command line, change to the directory you wish to contain the root games-samples directory and clone it from GitHub: git clone https://github.com/android/games-samples.git. The root directory of the codelab in your local clone of the repository is games-samples/codelabs/unity-gamepad.

After cloning the repo, launch the Unity editor and open the project in the codelabs/unity-gamepad/start directory.

Test the project

After opening the project follow these steps:

  1. Select File -> Build Settings from the Unity menu bar.
  2. In the Build Settings window, select the Android platform from the list and click the Switch Platform button.
  3. Once Unity completes switching platforms and importing the assets for Android, select Assets -> Build PAD Demo Runtime Assets Streaming from the Unity menu bar.
  4. After the AssetBundle build operation finishes, click on the Build And Run button in the Build Settings window.
  5. When the save pop up appears enter testStreaming in the Save As field and click the Save button.

The project will build, install and run on your connected device. After a brief loading display, the example app should resemble this (the ordering of images may vary):

c3b49d8a3b43c648.png

Select an image under Available images to display it under Current Image.

About the project

The example project consists of a minimal UI, displaying a list of images sourced from AssetBundles as well as a discrete image file. Both the AssetBundles and the discrete image are directly embedded in the application package using the Unity StreamingAssets feature. The Build PAD Demo Runtime Assets Streaming editor script command located in Editor/BuildRuntimeAssets.cs is used to build the AssetBundles in StreamingAssets, and to copy the discrete image file into StreamingAssets.

The project consists of two Unity scenes: a loading scene and the main sample scene. The loading scene takes a list of AssetBundles and loads them. After the AssetBundles are loaded, each AssetBundles is iterated and all the Sprite assets it contains are loaded. Finally, the scene processes a list of discrete image files, loading and converting them into Sprite assets. The loading scene displays progress information on screen while performing these operations. After all the assets have been loaded, the loading scene transitions to the main sample scene.

After adding the Play Asset Delivery plugin to the example project, we will use the plugin to create asset packs containing the existing AssetBundles. We will then modify the project to retrieve the AssetBundles via the Play Asset Delivery plugin instead of through StreamingAssets. The status of asset packs will be displayed on screen in the example UI.

As an optional step, we will explore how to generate asset packs programmatically, for situations where the default plugin generation may not meet the needs of a project. In our example, we will use programmatic generation to create an asset pack for the discrete image file, which is not contained in an AssetBundle.

Installing the Google Play plugins for Unity

To install the plugins for Play Asset Delivery, follow these steps:

  1. Download the latest release from Google Play Plugins for Unity releases.
  2. Import the .unitypackage file by selecting Assets -> Import Package -> Custom Package from the Unity menu bar.
  3. Import all the items.

After installation, you will see a Google menu in the Unity menu bar.

3. Creating asset packs using the plugin UI

The Play Asset Delivery plugin includes a tool for quickly generating asset packs from AssetBundles. The tool associates each specified AssetBundle with an identically-named asset pack.

Using Texture Compression Format Targeting

We will use TCFT to include multiple texture compression formats in our asset packs and have Google Play deliver the optimal format for a particular device at install time. TCFT works by looking for specific suffixes in the names of directories contained in an asset pack. For example, the suffix for ASTC texture compression is #tcf_astc. If a valid suffix is found, the directory is presumed to contain textures using the compression format specified by the suffix. A directory without a suffix is considered to contain textures in the intended default format. You should choose a default format supported by a very high percentage of devices. It is strongly recommended that you choose ETC2 as a default format. This example will use ETC2 as the default format, and ASTC as an additional format.

Since our textures are contained in asset bundles, we will build each asset bundle twice: once with ETC2 specified as an override texture compression type, and once with ASTC. The ETC2 asset bundles will be stored in a directory named AssetBundles. The ASTC bundles will be stored in a directory named AssetBundles#tcf_astc.

Redirecting AssetBundle generation

The editor script invoked by the Build PAD Demo Runtime Assets Streaming command originally placed generated AssetBundles and discrete images into the Assets/StreamingAssets directory. StreamingAssets is a special directory that Unity embeds directly in the application package. This is not what we want for our asset packs, so we need to locate our AssetBundles somewhere else.

At the top of the BuildRuntimeAssets class body, add the following function:

    [MenuItem("Assets/Build PAD Demo Runtime Assets AssetPacks - Plugin")]
    static void BuildRTAssets_AssetPacks_Plugin()
    {
        // Save the current setting
        MobileTextureSubtarget originalSetting = EditorUserBuildSettings.androidBuildSubtarget;

        // Clean out any old data
        DeleteTargetDirectory(streamingName);
        DeleteTargetDirectory(assetPacksName);

        // Build the AssetBundles, both in ETC2 and ASTC texture formats
        BuildAssetBundles(assetPacksName, astcSuffix, MobileTextureSubtarget.ASTC);
        BuildAssetBundles(assetPacksName, "", MobileTextureSubtarget.ETC2);
        AssetDatabase.Refresh();

        EditorUserBuildSettings.androidBuildSubtarget = originalSetting;
    }

After Unity recompiles the script file, select Assets -> Build PAD Demo Runtime Assets AssetPacks - Plugin from the Unity menu bar. New folders called AssetPacks and AssetPacks#tcf_astc will be created in the root Assets folder.

Mapping AssetBundles to asset packs

Select Google -> Android App Bundle -> Asset Delivery Settings from the Unity menu bar to bring up the Asset Delivery window. Click the Add Folder button, navigate to the Assets/AssetPacks directory and click Choose.

The plugin will scan the specified directory and search for a .manifest file inside sharing the same name as the parent directory. It will use that manifest file to generate Asset Pack definitions for the AssetBundles listed in the manifest. Each asset pack will be named after the AssetBundle it contains.

Next we need to set the Delivery Mode for our new asset packs. There are three Delivery Modes defined by Play Asset Delivery:

  • Install-time asset packs, which are embedded in the application package
  • Fast-follow asset packs, which are not included in the application package, but are automatically downloaded from the Google Play store as soon as the app is installed
  • On-demand asset packs, which are not downloaded from Google Play until specifically requested by the application

We will assign one from each category:

  1. Set the Delivery Mode of fastfollow to "Fast Follow".
  2. Set the Delivery Mode of installtime to "Install Time".
  3. Set the Delivery Mode of ondemand to "On Demand".

After configuring the asset packs, we need to add the asset bundles containing the alternate ASTC textures. Click the Add Folder button, navigate to the Assets/AssetPacks#tcf_astc directory, and click Choose.

The AssetBundle Configuration section in the Asset Delivery window should resemble this: 66e1986b2ce38fe3.png

Close the Asset Delivery window.

4. Implementing asset pack support

We will now add code to interact with the Play Asset Delivery plugin to query, retrieve, and work with our new asset packs.

Updating the loading scene

Select File -> Open Scene from the Unity menu bar. Enter the Scenes directory and open LoadingScene.unity. In the scene hierarchy, select the GameAssetManager object and note the Inspector panel. For simplicity, the names of the AssetBundles/asset packs we will be using have been pre-populated in the GameAssetManager script component.

Expand the Canvas object in the scene hierarchy and select the LoadingPanel object it contains. In the Inspector for the LoadingPanel, locate the Loading Scene Transition script component. Find the Using Asset Packs checkbox, which is currently unchecked. Check this checkbox and select File -> Save from the Unity menu bar to save the edited scene.

b23ca48fb30b0e88.png

The Using Asset Packs checkbox sets a property in the LoadingSceneTransition class that acts as a toggle between StreamingAssets and asset pack behavior. While the conditional logic already exists, we now need to add implementation code in LoadingSceneTransition to actually use asset packs.

Updating the LoadingSceneTransition class

Open the LoadingSceneTransition.cs file located in Assets/Scripts. Begin by adding the following line at the bottom of the list of using statements:

using Google.Play.AssetDelivery;

When we checked the Using Asset Packs checkbox, it changed the behavior of the SetLoadingSceneState function. The relevant code is below:

                case LoadingSceneState.LOADING_ASSETBUNDLES:
                    headerText.text = "Loading AssetBundles";
                    if (usingAssetPacks)
                    {
                        StartLoadingAssetBundlesFromAssetPacks();
                    }
                    else
                    {
                        StartLoadingAssetBundlesFromStreaming();
                    }
                    break;
                case LoadingSceneState.LOADING_DISCRETE:
                    if (usingAssetPacks)
                    {
                        headerText.text = "Loading discrete pack";
                        StartLoadingDiscreteAssetPack();
                    }
                    else
                    {
                        SetupDiscreteAssetsStreaming();
                    }
                    break;

When the Using Asset Packs checkbox is unchecked, the LoadingSceneState.LOADING_ASSETBUNDLES case statement calls the StartLoadingAssetBundlesFromStreaming function. If the checkbox is checked, it calls the StartLoadingAssetBundlesFromAssetPacks function instead.

Similarly, the LoadingSceneState.LOADING_DISCRETE case statement calls the SetupDiscreteAssetsStreaming function if Using Asset Packs is unchecked, and StartLoadingDiscreteAssetPack if checked.

At the moment, both the StartLoadingAssetBundlesFromAssetPacks and the StartLoadingDiscreteAssetPack functions are empty. Populate the StartLoadingAssetBundlesFromAssetPacks function with the following code:

        currentItemsLoaded = 0;
        totalItemsToLoad = 0;
        var assetBundleQueue = new Queue<string>();

        // Always add fast-follow packs, if they are still in the process
        // of being downloaded our request will wait until they finish
        // to load the newly available AssetBundles from them
        foreach (string fastfollowPackName in 
            gameAssetManager.fastfollowAssetPackNameList)
        {
                assetBundleQueue.Enqueue(fastfollowPackName);
                ++totalItemsToLoad;
        }

        // For install-time and on-demand, only add if they are marked as
        // downloaded (for install-time this should always be the case)
        foreach(string installPackName in 
            gameAssetManager.installtimeAssetPackNameList)
        {
            if (PlayAssetDelivery.IsDownloaded(installPackName))
            {
                ++totalItemsToLoad;
                assetBundleQueue.Enqueue(installPackName);
            }
        }

        foreach(string ondemandPackName in 
            gameAssetManager.ondemandAssetPackNameList)
        {
            if (PlayAssetDelivery.IsDownloaded(ondemandPackName))
            {
                ++totalItemsToLoad;
                assetBundleQueue.Enqueue(ondemandPackName);
            }
        }

        StartCoroutine(LoadAssetBundlesFromAssetPacks(assetBundleQueue));


The code retrieves the names of asset packs from the example's GameAssetManager class. The GameAssetManager class splits these lists up by delivery mode, so each list is processed in turn. Note that install-time and on-demand packs are only queued for load if the PlayAssetDelivery.IsDownloaded function returns true. For install-time packs this condition should always be true. In our example, we want to require user interaction to initiate downloading an on-demand pack. Because of this, we do not want to load an on-demand pack if it has not been downloaded since the mechanism for the load would trigger a download.

In the case of the fast-follow asset packs, we always try to load them as it is presumed that they will be automatically downloaded under normal operation. In local testing, as opposed to testing on a Google Play build, fast-follow packs behave like on-demand packs, so this has the side effect of emulating the behavior of a Google Play build.

Once the queue of asset packs is populated, the function ends by starting a coroutine to load the asset packs. The LoadAssetBundlesFromAssetPacks function used for this coroutine does not currently exist in the file. Add the following code after the end of the StartLoadingAssetBundlesFromAssetPacks function:

    IEnumerator LoadAssetBundlesFromAssetPacks(Queue<string> assetBundleQueue)
    {
        while (assetBundleQueue.Count > 0)
        {
            string assetBundleName = assetBundleQueue.Dequeue();
            PlayAssetBundleRequest bundleRequest = 
                PlayAssetDelivery.RetrieveAssetBundleAsync(assetBundleName);
            
            while (!bundleRequest.IsDone)
            {
                yield return null;
            }

            if (bundleRequest.Error == AssetDeliveryErrorCode.NoError)
            {
                gameAssetManager.AddAssetBundle(assetBundleName, 
                    bundleRequest.AssetBundle);
                ++currentItemsLoaded;
                if (currentItemsLoaded == totalItemsToLoad)
                {
                    Debug.Log("All AssetBundles loaded");
                    SetLoadingSceneState(
                        LoadingSceneState.LOADING_DISCRETE);
                }
            }
            else
            {
                Debug.LogErrorFormat("Couldn't load bundle {0} : {1}", 
                    assetBundleName, bundleRequest.Error);
            }
        }
    }

LoadAssetBundlesFromAssetPacks calls the PlayAssetDelivery.RetrieveAssetBundleAsync function for each asset pack name. Since we used the Play Asset Delivery plugin to construct the asset packs, the AssetBundle and asset pack names are identical. PlayAssetDelivery.RetrieveAssetBundleAsync returns a PlayAssetBundleRequest which is used to monitor the status of a request, and retrieve the results of the request when complete. Upon a successful retrieval, the LoadAssetBundlesFromAssetPacks uses the AssetBundle property of the PlayAssetBundleRequest to get the newly-loaded AssetBundle. It then passes the AssetBundle to the GameAssetManager.AddAssetBundle function to be added to GameAssetManager's internal list of loaded AssetBundles.

Finally, we need to add code to the empty StartLoadingDiscreteAssetPack function. However, since we created our asset packs based on AssetBundles using the Play Asset Delivery plugin, we do not currently have the ability to create or use asset packs that incorporate discrete asset files. We will cover how to do so as an optional step later in the codelab. For the moment, add the following line to the StartLoadingDiscreteAssetPack function to skip to the next state in the loading process:

            SetLoadingSceneState(LoadingSceneState.LOADING_ASSETS);

Save your changes to the file.

Updating the GameAssetPack class

Open the GameAssetPack.cs file located in Assets/Scripts. The GameAssetPack class is a simple container class that holds information about an asset pack. We will also use it to manage request objects returned by PlayAssetDelivery.RetrieveAssetBundleAsync and PlayAssetDelivery.RetrieveAssetPackAsync. Right now, the class is a skeleton, with unused properties and functions empty of code. We will implement the missing functionality below.

Begin by adding the following line at the bottom of the list of using statements:

using Google.Play.AssetDelivery;

Adding request variables

We will now add variables for PlayAssetDelivery request objects. There are two request types we will be using in this example: PlayAssetBundleRequest and PlayAssetPackRequest.

The PlayAssetDelivery.RetrieveAssetBundleAsync function returns a PlayAssetBundleRequest object. PlayAssetDelivery.RetrieveAssetBundleAsync is intended for use with asset packs created by the plugin, which only contain a single identically-named AssetBundle. As we saw in the loading scene, PlayAssetBundleRequest makes it easy to retrieve the associated AssetBundle using its AssetBundle property.

The PlayAssetPackRequest object is returned by PlayAssetDelivery.RetrieveAssetPackAsync. If you are using an asset pack containing multiple AssetBundles, discrete asset files, or some combination of the two, then you would choose to use PlayAssetPackRequest.

We will support both types of request in GameAssetPack. The IsSingleAssetBundlePack property is used to determine whether GameAssetPack uses PlayAssetDelivery.RetrieveAssetBundleAsync or PlayAssetDelivery.RetrieveAssetPackAsync when the StartDownload function is called.

Add the following lines after the other private variable declarations in GameAssetPack:

    // if isSingleAssetBundlePack is true we use a PlayAssetBundleRequest
    // otherwise we use a PlayAssetPackRequest
    private PlayAssetBundleRequest assetBundleRequest;
    private PlayAssetPackRequest assetPackRequest;

Add the following lines to the end of the Awake function:

        assetBundleRequest = null;
        assetPackRequest = null;

Updating empty functions

We will now fill in empty functions to work with the Play Asset Delivery plugin and the request objects. First, replace the return true statement with following code in the IsDownloaded function:

        bool isDownloaded;

        if (isSingleAssetBundlePack && assetBundleRequest != null)
        {
            isDownloaded = assetBundleRequest.IsDone;
        }
        else
        {
            isDownloaded = PlayAssetDelivery.IsDownloaded(assetPackName);
        }
        return isDownloaded;

Next, add the following code to StartDownload:

        if (isSingleAssetBundlePack && assetBundleRequest == null)
        {
            assetBundleRequest = PlayAssetDelivery.RetrieveAssetBundleAsync(
                assetPackName);
        }
        else if (!isSingleAssetBundlePack && assetPackRequest == null)
        {
            assetPackRequest = PlayAssetDelivery.RetrieveAssetPackAsync(
                assetPackName);
        }

Finally, locate the IsDownloadActive function and replace the return false statement with the following line:

        return assetBundleRequest != null || assetPackRequest != null;

The GameAssetPack class can now determine whether an asset pack has been downloaded, can start a retrieval request, and check if a retrieval request is active.

Adding finish functions

After a retrieval request completes successfully, the user of a GameAssetPack needs to be able to access the contents of the asset pack. We will add functions that perform this service for both the PlayAssetBundleRequest and the PlayAssetPackRequest types. These functions will also release the internal references to the request objects. Add the following code after the StartDownload function:

    public AssetBundle FinishBundleDownload()
    {
        AssetBundle assetBundle = assetBundleRequest.AssetBundle;
        assetBundleRequest = null;
        return assetBundle;
    }

    public PlayAssetPackRequest FinishPackDownload()
    {
        PlayAssetPackRequest request = assetPackRequest;
        assetPackRequest = null;
        return request;
    }

In the case of a PlayAssetBundleRequest, we can just return the AssetBundle directly since it represents the sole contents of the asset pack, and is loaded and available for use. If the retrieval used PlayAssetPackRequest, then we return the PlayAssetPackRequest object itself, as it will be used to retrieve location information for the files contained in the asset pack.

Adding new status functions

We will add some new functions to GameAssetPack to return information held by a retrieval request. We will be adding:

  • A GetStatus function to return an AssetDeliveryStatus enum describing the current state of an asset pack retrieval request.
  • A GetError function to return an AssetDeliveryErrorCode enum describing an error returned during an asset pack retrieval request.
  • A GetDownloadProgress function to return the completion percentage of an active retrieval download operation.

Locate the existing GetError function. Select it and delete it. In its previous location add the following code after the IsDownloadActive function:

    public AssetDeliveryStatus GetStatus()
    {
        if (assetBundleRequest != null)
        {
            return assetBundleRequest.Status;
        }
        if (assetPackRequest != null)
        {
            return assetPackRequest.Status;
        }
        return AssetDeliveryStatus.Pending;
    }

    public AssetDeliveryErrorCode GetError()
    {
        if (assetBundleRequest != null)
        {
            return assetBundleRequest.Error;
        }
        if (assetPackRequest != null)
        {
            return assetPackRequest.Error;
        }
        return AssetDeliveryErrorCode.NoError;
    }

    public float GetDownloadProgress()
    {
        if (assetBundleRequest != null)
        {
            return assetBundleRequest.DownloadProgress;
        }
        if (assetPackRequest != null)
        {
            return assetPackRequest.DownloadProgress;
        }
        return 0.0f;
    }

Adding an asset pack size query

We will inform the user of the size of an asset pack before the user chooses to download it. To do that, we need to retrieve the size of an asset pack, which is handled by an asynchronous operation returned by PlayAssetDelivery.GetDownloadSize. Since it is asynchronous, GameAssetPack has an IsAssetPackSizeValid property that returns true if the AssetPackSize property contains a valid size. A caveat is that install-time asset packs will always return a download size of 0. Add the following code at the bottom of the GameAssetPack class:

    private void StartAssetPackSizeQuery()
    {
        var getSizeOperation = PlayAssetDelivery.GetDownloadSize(assetPackName);
        getSizeOperation.Completed += (operation) =>
        {
            if (operation.Error != AssetDeliveryErrorCode.NoError)
            {
                Debug.LogErrorFormat("Error getting download size for {0}: {1}",
                    assetPackName, operation.Error);
                return;
            }

            assetPackSize = operation.GetResult();
            isAssetPackSizeValid = true;
        };
    }

To run the size query on a newly created GameAssetPack, we will add a call to it in the GameAssetPack.InitializeAssetPack function. Find the GameAssetPack.InitializeAssetPack function and add StartAssetPackSizeQuery(); to the bottom. The end result should look like this:

    public void InitializeAssetPack(string newAssetPackName,
        bool isSingleBundlePack)
    {
        AssetPackName = newAssetPackName;
        IsSingleAssetBundlePack = isSingleBundlePack;
        StartAssetPackSizeQuery();
    }

Adding mobile data permission requests

If a device is connected to the Internet via wi-fi, asset packs of any size can be downloaded without user confirmation. If wi-fi is not available, downloading an asset pack larger than 150MB in size requires explicit user consent to download using a mobile data connection. The PlayAssetDelivery.ShowCellularDataConfirmation function is used to ask for this consent and retrieve the results. We will add a RequestCellularDataDownload function to implement this request and set the GameAssetPack.DidApproveCellularData property with the results.

First, add the following code to the empty RequestCellularDataDownload function:

        if (!isCellularConfirmationActive)
        {
            didApproveCellularData = false;
            isCellularConfirmationActive = true;
            StartCoroutine(AskForCellularDataPermission());
        }

Then, add the following code at the bottom of the GameAssetPack class:

    IEnumerator AskForCellularDataPermission()
    {
        var asyncOperation = PlayAssetDelivery.ShowCellularDataConfirmation();
        // Wait until user has confirmed or cancelled the dialog.        
        yield return asyncOperation;
        bool permissionAllow = asyncOperation.Error == 
            AssetDeliveryErrorCode.NoError && asyncOperation.GetResult() == 
            ConfirmationDialogResult.Accepted;
        if (permissionAllow)
        {
            yield return new WaitUntil(() =>
                GetStatus() != AssetDeliveryStatus.WaitingForWifi);
        }
        didApproveCellularData = permissionAllow;
        isCellularConfirmationActive = false;
    }

Save your changes to the file.

Updating the AssetPackListController class

Open the AssetPackListController.cs file located in Assets/Scripts. The AssetPackListController class is responsible for populating the UI list of asset packs. It does this by instantiating instances of the AssetPackInfo prefab for each asset pack. The prefab is then added to the content object of the UI list. The AssetPackInfo prefab contains UI elements for displaying the name and status of an asset pack, a download button for downloading asset packs that are not present on device, and a progress bar for displaying progress of active downloads. The prefab uses the AssetPackInfoController script component to control these elements.

To populate the list, add the following code to the Start function:

        var assetPackNames = 
            gameAssetManager.GetAssetPackNameList();

        foreach(string assetPackName in assetPackNames)
        {
            AddAssetPack(assetPackName);
        }

Save your changes to the file. Note that the AddAssetPack function already exists at the bottom of the file, it uses the AssetPackInfoController.SetAssetPack function to configure the asset pack information for the prefab.

Updating the AssetPackInfoController class

Open the AssetPackInfoController.cs file located in Assets/Scripts. The AssetPackInfoController class keeps track of the state of its assigned asset pack. It uses this internal state to manage the UI elements from its parent AssetPackInfo prefab.

Begin by adding the following line at the bottom of the list of using statements:

using Google.Play.AssetDelivery;

The UpdateDownloading function is currently empty. We will add code to manage an active download based on the AssetDeliveryStatus of an active Play Asset Delivery request. Add the following code to UpdateDownloading:

        if (infoStatus != AssetPackInfoStatus.PACKSTATUS_DOWNLOADING)
        {
            infoStatus = AssetPackInfoStatus.PACKSTATUS_DOWNLOADING;
            SetupDownloadUI();
        }

        AssetDeliveryStatus deliveryStatus = gameAssetPack.GetStatus();

        if (deliveryStatus == AssetDeliveryStatus.Failed)
        {
            SetupErrorUI();
        }
        else if (deliveryStatus == AssetDeliveryStatus.Loaded ||
                    deliveryStatus == AssetDeliveryStatus.Available )
        {
            if (gameAssetPack.IsSingleAssetBundlePack)
            {
                AssetBundle newBundle = gameAssetPack.FinishBundleDownload();
                StartCoroutine(LoadAssetsFromBundle(newBundle));
            }
        }
        else if (deliveryStatus == AssetDeliveryStatus.Loading)
        {
            assetPackStatusLabel.text = "Loading";
            assetPackProgressBar.PercentComplete = 1.0f;
        }
        else if (deliveryStatus == AssetDeliveryStatus.Retrieving)
        {
            assetPackProgressBar.PercentComplete =
                gameAssetPack.GetDownloadProgress();
        }
        else if (deliveryStatus == AssetDeliveryStatus.WaitingForWifi)
        {
            Debug.Log("Download status WaitingForWifi");
            infoStatus = AssetPackInfoStatus.PACKSTATUS_NEEDS_PERMISSION;
            waitingOnPermissionResult = true;
            gameAssetPack.RequestCellularDataDownload();            
        }

UpdateDownloading handles several different conditions reported by the AssetDeliveryStatus of an active delivery request:

  • If user permission is required to initiate a download over a mobile data connection, UpdateDownloading will call RequestCellularDataDownload and change internal state to wait for the results of the user choice.
  • If an error occurs, UpdateDownloading will switch the UI to display an error condition.
  • While the download is progressing, UpdateDownloading will update the UI using the value returned by GetDownloadProgress.
  • When the download completes successfully, UpdateDownloading will retrieve the newly-loaded AssetBundle and use LoadAssetsFromBundle to load the assets for use.

Save your changes to the file. We have completed adding the necessary code to implement asset pack support. Our next step is to test our modified example on a local build.

5. Testing asset packs locally

Play Asset Delivery supports local testing on a device without having to upload a build to Google Play. There are a few important differences and limitations to be aware of when testing locally:

  • Fast-follow asset pack types behave as on-demand asset pack types. They won't be automatically fetched when the game is locally installed on device.
  • The asset packs are fetched from external storage instead of from the Google Play servers, so you cannot test how your code behaves in the case of network errors.
  • Local testing does not cover the wait-for-Wi-Fi/cellular authorization scenario.
  • Updates are not supported. Before installing a new version of your build, manually uninstall the previous version.

Building and running the app bundle

Play Asset Delivery requires building the project as an Android App Bundle. Unity includes app bundles as an option in the Android platform build settings. However, Unity is not aware of asset packs and will not include them in an app bundle when using the standard build commands.

The Play Asset Delivery plugin installs additional menu items to build app bundles with asset packs. These commands are available in the Google menu of the Unity menu bar. Select Google -> Build and Run to build our updated project, deploy it to the connected device with local testing mode enabled, and run it.

After the loading scene transition, our example should now resemble this:

31ffa9d1538d2068.png

Note that the asset pack list in the UI is now populated. The On Demand pack has not been downloaded yet, and the images it contains are missing from the image list. Tapping the Download (size in MB) button will retrieve the images and add them to the image list (you may need to scroll down in the UI to see them). Since we are running a build with local testing, these images are being ‘downloaded' from a staging location in the device's external storage instead of being transferred over the Internet from the Google Play servers.

6. Creating a build for Google Play

In order to upload a build of the example project to Google Play, we will need to change its package name to a unique identifier, and create a keystore to generate a signed release build.

Set a unique package name for the game

Every Android app has a unique application ID that looks like a Java package name, such as com.example.myapp. This ID uniquely identifies your app on the device and in the Google Play Store. If you already have a game released or uploaded to the Play Console, you already chose a package name for it.

The example project currently has an application id of com.google.sample.unitypad. To be able to upload it on your account, you need to choose a new, unique application id.

Open the Player Settings (from the Build Settings window). In the Android tab, open Other Settings section and find the Package Name field:

6e96dff2ac012723.png

Enter a unique, valid application id.

Create and setup a keystore for the game

Android requires that all apps are digitally signed with a certificate before they are installed on a device or updated.

We'll create a "Keystore" for the game in this codelab. If you're publishing an update to an existing game, reuse the same Keystore as you did for releasing previous versions of the app.

Create a keystore

You can create a Keystore with Android Studio or generate one on the command line. Using the command line, navigate to a folder where you want to store the key, then run keytool:

keytool -genkey -v -keystore mykeystore.keystore -alias alias -keyalg RSA -keysize 2048 -validity 10000

Answer the questions about your identity and confirm by entering "yes" when asked if the details are correct. You'll need to enter a password and an alias password.

Set up the keystore in Unity

Now that you have a keystore, navigate to Player Settings (from the Build Settings window). In the Android tab, open the Publishing Settings section. Check Custom Keystore, and then select your keystore file. Enter the password that was used for the keystore, the alias name and the alias password. These are the ones you chose when creating the keystore using Android Studio or the keytool command line tool. Note that the keytool command uses the same password for the keystore and alias, which is acceptable for this codelab but discouraged for production apps.

7. Testing a Google Play build

Testing with internal app sharing

Internal app sharing can be used to easily install builds uploaded to Google Play. To test the example app using internal app sharing, do the following:

  1. Build the app as a release, signed Android App Bundle using the Google -> Build Android App Bundle from the Unity menu bar.
  2. Follow the Play Console instructions on how to share your app internally.
  3. On the test device, click the internal app-sharing link for the version of the app you just uploaded.
  4. Install the app from the Google Play Store page you see after clicking the link.

8. Creating asset packs programmatically (optional)

The plugin's user interface to configure asset packs is easy to use, but has limitations. It only allows for asset packs to be configured from AssetBundles found in specified folders. It creates an asset pack for each AssetBundle it finds, naming each asset pack for the AssetBundle it contains. For games that have data architecture requirements that do not fit within this model, it is possible to generate asset packs programmatically through editor scripts. These asset packs can contain any number of AssetBundles, discrete files, or both.

In its initial state, the example loaded a discrete asset file from StreamingAssets in addition to the AssetBundles. When we switched to the plugin asset pack generation, that discrete asset was no longer being included in the runtime data. We will change our asset pack generation process to create a new asset pack that contains the missing discrete asset, in addition to generating the AssetBundle asset packs.

Adding the asset pack creation menu item

First, we will remove the existing asset pack configuration. Select Google -> Android App Bundle -> Asset Delivery Settings to bring up the Asset Delivery window. Click the two Remove buttons to remove the folders from the current configuration and then close the Asset Delivery window.

Next, open the BuildRuntimeAssets.cs file from the Assets/Editor folder.

Begin by adding the following lines at the bottom of the list of using statements:

using Google.Android.AppBundle.Editor;
using Google.Android.AppBundle.Editor.AssetPacks;

Next, add the following code inside the BuildRuntimeAssets class:

    [MenuItem("Assets/Build PAD Demo Runtime Assets AssetPacks - Scripted")]
    static void BuildRTAssets_AssetPacks_Scripted()
    {
        // Save the current setting
        MobileTextureSubtarget originalSetting = EditorUserBuildSettings.androidBuildSubtarget;

        // Clean out any old data
        DeleteTargetDirectory(streamingName);
        DeleteTargetDirectory(assetPacksName);

        // Build the AssetBundles, both in ETC2 and ASTC texture formats
        BuildAssetBundles(assetPacksName, astcSuffix, MobileTextureSubtarget.ASTC);
        BuildAssetBundles(assetPacksName, "", MobileTextureSubtarget.ETC2);
        AssetDatabase.Refresh();

        // Copy our discrete test image asset into a new directory
        // which will be used for the 'discrete' asset pack source
        string discreteFileName = "Discrete1.jpg";
        string discretePackName = "discretepack";

        string discretePath = Path.Combine(Path.GetTempPath(), 
            discretePackName);
        Directory.CreateDirectory(discretePath);
        string destPath = Path.Combine(discretePath, discreteFileName);
        if (File.Exists(destPath))
        {
            File.Delete(destPath);
        }

        string sourcePath = Path.Combine(Application.dataPath,
            "Images");
        sourcePath = Path.Combine(sourcePath, discreteFileName);
        File.Copy(sourcePath, destPath);
        Debug.Log("Copied discrete file to : " + destPath);

        // Create an AssetPackConfig and start creating asset packs
        AssetPackConfig assetPackConfig = new AssetPackConfig();

        // Create asset packs using AssetBundles
        string assetBundlePath = Path.Combine(Application.dataPath,
            assetPacksName);

        // Add the default ETC2 bundles
        assetPackConfig.AddAssetBundle(Path.Combine(assetBundlePath,
            "installtime"), AssetPackDeliveryMode.InstallTime);

        assetPackConfig.AddAssetBundle(Path.Combine(assetBundlePath,
            "fastfollow"), AssetPackDeliveryMode.FastFollow);

        assetPackConfig.AddAssetBundle(Path.Combine(assetBundlePath,
            "ondemand"), AssetPackDeliveryMode.OnDemand);
        
        // Add the ASTC bundles
        assetBundlePath += astcSuffix;
        assetPackConfig.AddAssetBundle(Path.Combine(assetBundlePath,
            "installtime"), AssetPackDeliveryMode.InstallTime);

        assetPackConfig.AddAssetBundle(Path.Combine(assetBundlePath,
            "fastfollow"), AssetPackDeliveryMode.FastFollow);

        assetPackConfig.AddAssetBundle(Path.Combine(assetBundlePath,
            "ondemand"), AssetPackDeliveryMode.OnDemand);

        // Create an asset pack from our discrete directory
        assetPackConfig.AddAssetsFolder(discretePackName, discretePath,
            AssetPackDeliveryMode.OnDemand);

        // Configures the build system to use the newly created 
        // assetPackConfig when calling Google > Build and Run or 
        // Google > Build Android App Bundle.
        AssetPackConfigSerializer.SaveConfig(assetPackConfig);

        EditorUserBuildSettings.androidBuildSubtarget = originalSetting;
    }

Save your changes to the file. After Unity recompiles the script, select Assets -> Build PAD Demo Runtime Assets AssetPacks - Scripted from the Unity menu bar to generate the asset packs using the new command. The new command uses the AssetPackConfig class from the Play Asset Delivery plugin. AssetPackConfig is used to define the contents of asset packs and to save the new asset pack configuration. The plugin build commands from the Google menu in the Unity menu bar use this saved configuration to generate the asset packs when building an Android App Bundle. An AssetPackConfig may also be passed to the BundleTool.BuildBundle function to programmatically build an Android App Bundle.

Adding support for the new asset pack

Our example project already has support for using PlayAssetPackRequests as well as PlayAssetBundle requests. We will now add some code to use this functionality to access our new asset pack containing the discrete image file.

Updating the LoadingSceneTransition class

Open the LoadingSceneTransition.cs file located in Assets/Scripts. First, we will replace the existing code in StartLoadingDiscreteAssetPack with the following code:

        // Only load if it has been downloaded
        if (PlayAssetDelivery.IsDownloaded(
            gameAssetManager.discreteAssetPackName))
        {
            currentItemsLoaded = 0;
            totalItemsToLoad = 1;
            StartCoroutine(LoadDiscreteAssetPack(
                gameAssetManager.discreteAssetPackName));
        }
        else
        {
            // If not downloaded, just start loading the asset list
            SetLoadingSceneState(LoadingSceneState.LOADING_ASSETS);
        }

Next, we need to implement the LoadDiscreteAssetPack function referenced by our new code. Since this asset pack request uses PlayAssetPackRequest and not PlayAssetBundleRequest, we need to use the PlayAssetPackRequest.GetAssetLocation function to retrieve location and size information about the asset we want to load. Add the following code after the StartLoadingDiscreteAssetPack function:

    IEnumerator LoadDiscreteAssetPack(string assetPackName)
    {
        PlayAssetPackRequest packRequest = 
            PlayAssetDelivery.RetrieveAssetPackAsync(assetPackName);
        
        while (!packRequest.IsDone)
        {
            yield return null;
        }

        if (packRequest.Error == AssetDeliveryErrorCode.NoError)
        {
            ++currentItemsLoaded;
            if (currentItemsLoaded == totalItemsToLoad)
            {
                // Get the list of discrete assets, 
                // retrieve their location from the asset pack
                // and queue for load
                List<string> discreteAssets = 
                    gameAssetManager.GetDiscreteAssetNameList();
                foreach(string discreteAsset in discreteAssets)
                {
                    var assetLocation = packRequest.GetAssetLocation(
                        discreteAsset);
                    if (assetLocation != null)
                    {
                        assetQueue.Enqueue(new GameAssetReference(
                            discreteAsset,
                            assetLocation.Path, null, 
                            (long)assetLocation.Size, 
                            (long)assetLocation.Offset));
                    }
                }

                Debug.Log("All discrete asset packs loaded");
                SetLoadingSceneState(
                    LoadingSceneState.LOADING_ASSETS);
            }
        }
        else
        {
            Debug.LogErrorFormat("Couldn't load pack {0} : {1}", 
                assetPackName, packRequest.Error);
        }
    }

Save your changes to the file.

Updating the AssetPackListController class

Open the AssetPackListController.cs file located in Assets/Scripts. Now that we are generating the discretepack asset pack file, we need to add it to our list of asset packs in the example UI. Add the following line to the bottom of the Start function:

        AddAssetPack(gameAssetManager.discreteAssetPackName);

Save your changes to the file.

Updating the AssetPackInfoController class

Open the AssetPackInfoController.cs file located in Assets/Scripts. We need to make a modification to UpdateDownloading to handle completing a download of our new asset pack. Find the if (gameAssetPack.IsSingleAssetBundlePack) statement and add the following else block to it:

            else
            {
                PlayAssetPackRequest packRequest =
                    gameAssetPack.FinishPackDownload();
                LoadAssetsFromPack(packRequest);
            }

Next, at the bottom of the AssetPackInfoController class, add the following code to create the LoadAssetsFromPack function:

    void LoadAssetsFromPack(PlayAssetPackRequest packRequest)
    {
        List<string> discreteAssets = 
            gameAssetManager.GetDiscreteAssetNameList();
        foreach(string discreteAsset in discreteAssets)
        {
            var assetLocation = packRequest.GetAssetLocation(
                discreteAsset);
            if (assetLocation != null)
            {
                long assetSize = (long)assetLocation.Size;
                long assetOffset = (long)assetLocation.Offset;
                var assetFileStream = File.OpenRead(assetLocation.Path);
                var assetBuffer = new byte[assetSize];
                assetFileStream.Seek(assetOffset, SeekOrigin.Begin);
                assetFileStream.Read(assetBuffer, 0, assetBuffer.Length);
                Debug.Log("Finished discrete load of " + discreteAsset);
                // The asset is a raw .jpg or .png, convert to a sprite
                gameAssetManager.AddSpriteAsset(discreteAsset,
                    GameAssetManager.GenerateSpriteFromRawImage(
                        assetBuffer));
            }
        }
    }

Save your changes to the file.

Testing the discrete pack

Select Google -> Build and Run from the Unity menu bar. When the updated example launches, the discrete asset pack will be displayed in the UI list:

85e8ba5cb77ce217.png

9. Confirming large downloads over mobile data (optional)

If a device is connected to the Internet via wi-fi, asset packs of any size can be downloaded without user confirmation. If wi-fi is not available, downloading an asset pack larger than 0MB in size requires explicit user consent to download using a mobile data connection. The Play Asset Delivery plugin includes functions to ask for this consent and report the results. While the code we added handles this scenario, the current on-demand asset pack is well under the size limit. To validate this behavior, we need to increase the size of the on-demand asset pack and submit a new build to Google Play.

Padding the on-demand asset pack size

In the Unity Project explorer, select the Padding folder in the main Assets folder. Make a group selection of all the numbered Padding images in the folder. At the bottom of the Inspector, change the AssetBundle assignment of the selected assets from None to ondemand.

35614f1b11f24f3e.png

Choose File -> Save Project from the Unity menu bar. Then choose Assets -> Build PAD Demo Runtime Assets AssetPacks - Scripted (or -Plugin if you skipped the previous optional step) to rebuild the asset packs.

Incrementing the version, rebuilding and resubmitting

Before making a new build, be sure to increment the Bundle Version Code field in the Player Settings -> Other Settings tab. After updating the app version, make a new Android App Bundle by selecting Google -> Build Android App Bundle from the Unity menu bar and upload the output app bundle file to Google Play.

Confirming mobile data access

When you run the new version of the app on a device with a mobile data connection, but no active wi-fi connection, tapping the on-demand asset pack Download button will bring up the Asset Pack Manager consent UI. If the user consents, the on-demand asset pack will download over the mobile data connection.

10. Congratulations

Congratulations, you've successfully integrated Play Asset Delivery into a Unity game and dynamically downloaded assets from Google Play. You are now ready to take advantage of the convenience and flexibility of delivering on-demand content directly from Google Play.