I would like to ask about how to use Unity's Addressables mechanism to correctly load content downloaded when online and offline.

I would like to ask about how to use Unity’s Addressables mechanism to correctly load content downloaded when online and offline.

Problem Summary

  • The current system has a problem with scenes in asset bundles not loading properly when offline. Scenes can be loaded, but materials and fonts are lost. This is in contrast to the previous system, which worked fine when online and offline.

system changes.

  1. Previous system:.
  • Each project (UPM) was downloaded to the Ikedayama project, and Addressable Asset Groups were built on the Ikedayama side.
  • These were built and .bundle files were uploaded to the server (no catalog files).
  • Ikedayama’s Unity project was built individually and scene transitions were performed by downloading .bundle files from the server in specific scenes.
  • When offline, once the .bundle file was downloaded to the apk, the scene could be loaded using Addressables.LoadSceneAsync. In that case, the Provide method’s Fetch method was called well.

2.Current system:.

  • Each project builds and builds its own Addressable Asset Group (including catalog files).
  • The system is to upload these to the server and download them with the apk of the Ikedayama project.
  • However, in a situation where the .bundle file has been downloaded once in the apk when offline, the Provide method does not work and the scene cannot be loaded correctly, even if you use Addressables.LoadSceneAsync.

About the Provide method

  • The Provide method exists in the XserverAssetBundleProvider and IkedayamaAssetBundleProvider classes, which extend the ResourceProviderBase class.
  • The method is defined in the form of public override void Provide(ProvideHandle providerInterface).

Changes to the `class that downloads the .bundle

  • The previously nonexistent await CatalogFilesUtil.GetCatalogFiles(token); is added at line 45.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using MessagePipe;
using TMPro;
using UniRx;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.UI;
using Zenject;

namespace Ikedayama
{
    public class DownloadAssetBundles : MonoBehaviour
    {
        [Inject] IPublisher<DownloadAssetMessage.EachAsset> EachAssetMessage;
        [Inject] IPublisher<DownloadAssetMessage.TotalAsset> TotalAssetMessage;
        [Inject] IPublisher<DownloadAssetMessage.CurrentDownload> CurrentDownloadMessage;
        [Inject] IPublisher<DownloadAssetMessage.AssetCount> NowDownloadingAssetCountMessage;

        [SerializeField] TextMeshProUGUI _nowDownloadingContentText;
        [SerializeField] TextMeshProUGUI _textOffline;
        [SerializeField] Slider _sliderTotal;
        [SerializeField] Slider _sliderEach;
        [SerializeField] float _timeLimitUntilShowGuestMode = 8.0f;
        [SerializeField] GameObject _touchCanvasRoot;
        [SerializeField] GameObject _offlineEventDirector;
        float _lastOnlineTime;
        public readonly BoolReactiveProperty _onOnline = new BoolReactiveProperty();

        async void Start()
        {
            _lastOnlineTime = Time.realtimeSinceStartup;
            _touchCanvasRoot.SetActive(false);
            _offlineEventDirector.SetActive(false);

            var cts = new CancellationTokenSource();
            var token = this.GetCancellationTokenOnDestroy();
            var labels = new Dictionary<string, string>();
            var counterOnline = 0;
            
            // Get catalog files
            await CatalogFilesUtil.GetCatalogFiles(token);

            // Online
            _onOnline.Where(value => value)
                .Subscribe(async value =>
                {
                    // On the first online connection, get labels
                    // Referenced from IkedayamaContentsList read from Resources
                    if(counterOnline == 0)
                    {
                        labels = await AvailableContentsUtil.GetAvailableContentsDict(token);
                        // Save to EasySave
                        AvailableContentsUtil.SaveAvailableContents(labels);
                    }
                    counterOnline++;

                    cts?.Cancel();

                    _touchCanvasRoot.SetActive(false);
                    _offlineEventDirector.SetActive(false);
                    _textOffline.gameObject.SetActive(false);
                    _sliderEach.gameObject.SetActive(true);
                    _sliderTotal.gameObject.SetActive(true);
                    _nowDownloadingContentText.gameObject.SetActive(true);

                    cts = new CancellationTokenSource();
                    await DownloadAssetBundleByLabel(labels, cts.Token);
                    await SceneTransition.Instance.LoadTargetSceneAsync(2f, cts.Token);
                    
                }).AddTo(this);

            // Offline
            _onOnline.Where(value => !value)    
                .Subscribe(value =>
                {
                    cts?.Cancel(); // Cancel download processes etc. during online

                    _textOffline.gameObject.SetActive(true);
                    _sliderEach.gameObject.SetActive(false);
                    _sliderTotal.gameObject.SetActive(false);
                    _nowDownloadingContentText.gameObject.SetActive(false);

                }).AddTo(this);
        }

        void Update()
        {
            // Detect offline
            if(Application.internetReachability == NetworkReachability.NotReachable)
            {
                _onOnline.Value = false;

                if (Time.realtimeSinceStartup - _lastOnlineTime > _timeLimitUntilShowGuestMode)
                {
                    _touchCanvasRoot.SetActive(true);
                    _offlineEventDirector.SetActive(true);
                }

            }
            else // Detect online
            {
                _onOnline.Value = true;
                _lastOnlineTime = Time.realtimeSinceStartup;
            }

        }

        async UniTask DownloadAssetBundleByLabel(Dictionary<string, string> labels, CancellationToken token)
        {
            var totalSliderMax = 0;
            // Initialize totalSliderMax
            foreach (var label in labels)
            {
                // Get all IResourceLocation of Addressable assets with the specified label
                var handle = Addressables.LoadResourceLocationsAsync(label.Value);
                IList<IResourceLocation> locations = await handle;

                // Group by DependencyHashCode
                var groups = locations.GroupBy(x => x.DependencyHashCode);

                // Get the number of iterations
                foreach (IGrouping<int, IResourceLocation> groupLocations in groups)
                {
                    totalSliderMax++;
                }
                
                // Explicitly release
                Addressables.Release(handle);
            }
            
            var count = 0;
            foreach (var label in labels)
            {
                // Send the total number of valid contents for the user and the count of currently downloading content
                NowDownloadingAssetCountMessage.Publish(new DownloadAssetMessage.AssetCount()
                {
                    TotalCount = totalSliderMax, NowCount = (count+1)
                });

                // Send label name
                CurrentDownloadMessage.Publish(new DownloadAssetMessage.CurrentDownload()
                {
                    Label = label.Value
                });

                // Overwrite ContentId
                AddressableProfilesLoadPath.ContentId = label.Key;
                var handle = Addressables.LoadResourceLocationsAsync(label.Value);
                IList<IResourceLocation> locations = await handle;
                var groups = locations.GroupBy(x => x.DependencyHashCode);

                foreach (IGrouping<int, IResourceLocation> groupLocations in groups)
                {
                    var dl = Addressables.DownloadDependenciesAsync(groupLocations.ToList());
                    dl.Completed += (AsyncOperationHandle) =>
                    {
                        Debug.Log($"<color=yellow>DownloadDependenciesAsync!: {groupLocations.Key}</color>");
                    };

                    var initialPercent = 0f;
                    bool downloadFailed = false;
                    while (dl.GetDownloadStatus().Percent < 1 && !dl.IsDone)
                    {
                        if (initialPercent == 0f && dl.GetDownloadStatus().Percent > 0f)
                        {
                            initialPercent = dl.GetDownloadStatus().Percent;
                        }

                        var percent = (dl.GetDownloadStatus().Percent - initialPercent) / (1 - initialPercent);

                        TotalAssetMessage.Publish(new DownloadAssetMessage.TotalAsset()
                        {
                            Percent = (count + percent) / totalSliderMax
                        });

                        EachAssetMessage.Publish(new DownloadAssetMessage.EachAsset()
                        {
                            Percent = percent
                        });

                        await UniTask.Yield(token);

                        // Perform error check here, and if there is an error, consider the download failed
                        if (dl.OperationException != null)
                        {
                            // Output message about download failure
                            Debug.Log($"Download failed: {dl.OperationException}");
                            downloadFailed = true;
                            break;
                        }
                    }

                    // Increase count only if download was successful
                    if (!downloadFailed)
                    {
                        count++;
                    }

                    if (count == totalSliderMax)
                    {
                        CurrentDownloadMessage.Publish(new DownloadAssetMessage.CurrentDownload()
                        {
                            Label = ""
                        });

                        TotalAssetMessage.Publish(new DownloadAssetMessage.TotalAsset()
                        {
                            Percent = 1f
                        });
                    }
                }

                // Explicitly release
                Addressables.Release(handle);

                // Interrupt the process if the app crashes midway, etc.
                if (token.IsCancellationRequested)
                {
                    Debug.Log("Token was cancelled!!!");
                    return;
                }
                
                // No need to save whether the download is complete or not, as catalog file updates need to be checked
                // Save completed downloads to EasySave
                // var downloadedContents = IkedayamaES3.Load<List<string>>(ESKeys.DownloadedVrContents, defaultValue: new List<string>());
                // if (!downloadedContents.Contains(label.Key))
                // {
                //     downloadedContents.Add(label.Key);
                // }
                // IkedayamaES3.Save(ESKeys.DownloadedVrContents, downloadedContents);
                
                // Check the cache location where the AssetBundle is stored
                // List<string> cachePaths = new List<string>();
                // Caching.GetAllCachePaths(cachePaths);
                // foreach (var path in cachePaths)
                // {
                //     Debug.Log(path);
                // }
            }   
            
            
        }
    }
}

Overview of the DownloadAssetBundles class

  • The DownloadAssetBundles class manages asset bundle downloads and scene transitions.
  • It behaves differently when online and offline, downloading assets and performing scene transitions when online, and displaying different UI elements when offline.

AddressableAssetSettings and Addressable Asset Group