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.
- 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 useAddressables.LoadSceneAsync
.
About the Provide
method
- The
Provide
method exists in theXserverAssetBundleProvider
andIkedayamaAssetBundleProvider
classes, which extend theResourceProviderBase
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
- These settings have been changed in the current system.