Польза Addressables в Unity3D и варианты использования

В Unity3D есть новый инструмент Addressables, для чего он нужен и в чём сложности его использования читайте в этой статье.

Addressables следует использовать в тех случаях когда нужно:

-разделить сборку проекта и сборку ассетов для этого проекта, например, когда проект вырос до таких размеров, что сборка всего разом занимает часы, а баг в коде хочется починить быстро, и отловить можно только в билде, тогда ресурсы можно собрать единожды, исполняемая же часть будет собираться гораздо быстрее

-загрузить игровые ресурсы через сеть, при таком подходе можно распространять легковесный исполнительный файл, а все тяжёлые графические и аудио ресурсы загрузятся с сервера

-уменьшить потребление памяти, при обычном подходе, все префабы и связанные с ними ресурсы загружены в память уже в тот момент, когда сцена загружена, Addressables позволяет загружать и выгружать из памяти всё нужное в тот момент, когда в префабе пропала необходимость

-ускорить загрузку уровня, при старте уровня можно показать пользователю низкополигональные модели с текстурами низкого разрешения, а более качественные модели и текстуры загружать уже в тот момент, когда пользователь способен взаимодействовать со сценой, и не заставлять ждать когда всё загрузится полностью

Бонус: Addressables можно использовать, чтобы оценить сколько места занимают ресурсы относящиеся к определённой сцене.

Установка Addressables

Addressables устанавливается в проект при помощи Package Manager. Тремя простыми шагами:

Польза Addressables в Unity3D и варианты использования

Затем надо открыть окно с группами:

Польза Addressables в Unity3D и варианты использования

И создать файл настроек:

Польза Addressables в Unity3D и варианты использования

По умолчанию будет одна группа:

Польза Addressables в Unity3D и варианты использования

Каждая группа будет упаковывать принадлежащие ей ресурсы в отдельный файл. Создать новую можно вот так:

Польза Addressables в Unity3D и варианты использования

Как контент сделать загружаемым через Addressables.

Можно двумя способами сделать контент:
1) перетянув нужный файл в группу
2) поставив галочку в инспекторе рядом с надписью Addressable

Польза Addressables в Unity3D и варианты использования

Во втором варианте контент добавится в группу, которая стоит по умолчанию, сменить её можно нажав правой кнопкой мыши по группе и выбрав Set as Default.

Польза Addressables в Unity3D и варианты использования

Как использовать контент добавленный в Addressables

Пример кода для загрузки Sprite в Image:

using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.UI; public class AddressablesSpriteLoader : MonoBehaviour { [SerializeField] AssetReference loadableSprite; [SerializeField] Image uiImage; private IEnumerator Start() { AsyncOperationHandle<Sprite> handle = loadableSprite.LoadAssetAsync<Sprite>(); yield return handle; if (handle.Status == AsyncOperationStatus.Succeeded) { Sprite sprite = handle.Result; // Спрайт загружен и готов к использованию uiImage.sprite = sprite; // А здесь ресурс можно уже удалять Addressables.Release(handle); } } }

Или можно иначе, используя async/await:

using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.UI; public class AddressablesSpriteLoader : MonoBehaviour { [SerializeField] AssetReference loadableSprite; [SerializeField] Image uiImage; private async void Start() { AsyncOperationHandle<Sprite> handle = loadableSprite.LoadAssetAsync<Sprite>(); await handle.Task; if (handle.Status == AsyncOperationStatus.Succeeded) { Sprite sprite = handle.Result; // Спрайт загружен и готов к использованию uiImage.sprite = sprite; // А здесь ресурс можно уже удалять Addressables.Release(handle); } } }

Имейте в виду, что два этих подхода могут отличаться результатом, если исполняемый компонент будет отключён до того как ресурс будет загружен.
Подход через IEnumerable в таком случае не даст нужного результата и картинка не будет показана, так как любая Coroutine на выключенном объекте прервётся.
Подход через async загрузит картинку в любом случае.

Пример кода для Prefab'а:

using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; public class AddressablesPrefabInstantiator : MonoBehaviour { [SerializeField] AssetReference loadablePrefab; private async void Start() { AsyncOperationHandle<GameObject> handle = loadablePrefab.LoadAssetAsync<GameObject>(); await handle.Task; if (handle.Status == AsyncOperationStatus.Succeeded) { GameObject gameObjectPrefab = handle.Result; Instantiate(gameObjectPrefab); Addressables.Release(handle); } } }

Или сокращённый вариант на случай, если нужно только отобразить объект в сцене:

using UnityEngine; using UnityEngine.AddressableAssets; public class AddressablesPrefabInstantiator : MonoBehaviour { [SerializeField] AssetReference loadablePrefab; private void Start() { loadablePrefab.InstantiateAsync(); } }

Пример выше показывает насколько просто можно заменить обычный подход с инстанциированием префаба на инстанциирование Addressable-префаба. И можно пойти дальше. Если создать префаб, а потом перетянуть его из окна Project в поле скрипта, то этот префаб автоматически станет Addressable.

Польза Addressables в Unity3D и варианты использования

Пример кода для сцены:

using UnityEngine; using UnityEngine.AddressableAssets; public class SceneLoader : MonoBehaviour { [SerializeField] private AssetReference scene; public void LoadScene() { scene.LoadSceneAsync(UnityEngine.SceneManagement.LoadSceneMode.Single); } }

При таком подходе надо файл сцены сделать Addressable, и установить ссылку в инспекторе. Учтите, что все необходимые для сцены ресурсы будут упакованы в файл той группы, в которую он добавлен, а если разные сцены, использующие общие объекты, будут в разных группах, то ресурсы продублируются в файлах этих групп. Поэтому делите контент с умом, либо общий контент выделите в отдельную группу и сделайте его Addressables.

И НЕ добавляйте сцены, загружаемые через Addressables, в список "Scenes In Build", иначе эти сцены будут занимать место и в самом билде и в файлах Addressables.

Как собрать исполняемый билд

Если мы соберём билд привычным способом, то получим ошибку:

Exception encountered in operation UnityEngine.ResourceManagement.ResourceManager+CompletedOperation`1[UnityEngine.Sprite], result='', status='Failed': Exception of type 'UnityEngine.AddressableAssets.InvalidKeyException' was thrown., Key=509e2ec2f5450c7418713755b837e796, Type=UnityEngine.Sprite

Потому, что мы не поместили файлы Addressables рядом с исполняемым файлом. Возникает два вопроса:
1) Где взять файлы?
2) Куда и как их положить?

У каждой группы есть настройки откуда её загружать:

Польза Addressables в Unity3D и варианты использования

Build Path - куда скрипт сборки положит файлы

Load Path - откуда эти файлы будет пытаться загрузить процесс

Самый простой вариант - BuildTarget как на скриншоте.

Польза Addressables в Unity3D и варианты использования

После этого можно запускать сборку контента. Файлы будут лежать вот в этой папке:

Польза Addressables в Unity3D и варианты использования

Её же и нужно положить в корень папки билда рядом с exe.

Как загрузить контент через сеть

Для того чтобы файлы с контентом подгружались через сеть, в Addressables есть специальная опция, её можно задействовать открыв Profiles:

Польза Addressables в Unity3D и варианты использования

Задав ссылку на сервер:

Польза Addressables в Unity3D и варианты использования

И установив LoadPath равным RemoteLoadPath:

Польза Addressables в Unity3D и варианты использования

Как сэкономить память

Эта часть будет самой сложной в понимании, потому, что придётся понять и простить разработчиков этой новой системы. В презентациях обещают, что Addressables будет позволять выгружать из памяти всё ненужное и неиспользуемое в данный момент, но на самом деле это происходит не всегда, для версии 1.16.15 проблема актуальна.

Предположим, что всё работает как надо, тогда вот код, который позволит загрузить префаб, инстанциировать его и выгрузить через 5 секунд:

using System.Collections; using UnityEngine; using UnityEngine.AddressableAssets; public class AddressablesPrefabInstantiator : MonoBehaviour { [SerializeField] AssetReference loadablePrefab; public void MakeInstance() { StartCoroutine(MakeInstanceCoroutine()); } private IEnumerator MakeInstanceCoroutine() { var result = Addressables.InstantiateAsync(loadablePrefab); yield return new WaitForSeconds(5); Addressables.ReleaseInstance(result); } }

Важный момент - не удаляйте объекты через Destroy, или после обычного Destroy вызывайте Addressables.ReleaseInstance на объекте AsyncOperationHandle, тогда счётчик ссылок, который помогает Addressables выгружать ненужное, будет правильно понимать сколько ссылок осталось и вовремя выгрузит лишнее из памяти.
Так же инстанциируйте через Addressables.InstantiateAsync, это тоже связано со счётчиком. Загрузка через LoadAsync с последующим инстанциированием "как обычно" приведёт к тому, что Addressables не будет знать что выгружать надо, а что нельзя.

Для того, чтобы выгрузить из памяти сцену загруженную через Addressables надо всего лишь загрузить другую сцену в режиме Single.

5353
24 комментария

Комментарий недоступен

3

будет правильно понимать сколько ссылок осталось и вовремя выгрузит лишнее из памятиНе всегда.

Не всегда.Можно поподробнее? В чём там нюанс?

а как делать подзагрузку легкий ассетов? Загружать Addressables потом выгружать когда сцена загружена?

В теории вам лично руками ни загружать ни выгружать не нужно, в последнем примере кода в статье всё нужное есть. Инстанциируете через Addressables.InstantiateAsync, а уничтожаете через Addressables.ReleaseInstance. На этом всё управление памятью и должно закончиться, но там есть баги в реализации и текстуры на сегодня выгружаются не всегда. 

1

Я бы ещё добавил, что разные пакеты работают избыточно, в т.ч. пакуя скипты. Т.е. каждый пакет(бандл по сути) собирает достаточные данные для запуска. Это не было бы проблемой, если бы при загрузке ресурс переиспользовали с других бандлов, а не загружали новый.

И если какой-то объект есть в 2 пакетах, то будет отдельно грузиться для каждого. Что решается очередным пакетом, который содержит объединённые данные этих 2 пакетов. На деле это не работает, т.к. для 3 пакетов так уже не получится сделать. А сами объекты можно загружать максимум на 2 уровня, бандл, и, скажем, префаб или сцену. А вот уже их содержимое загрузить отдельно нельзя.

Для хорошего контроля памяти, на коммерческом проекте, нужно будет подгонять всё под систему. Либо забить на перфекционизм и мириться с тем, что загружаем, по сути, один и тот же объект, из разных мест, т.к. этот объект находится на 3 уровне вложенности в разных бандлах. Ну и ещё вариант, гипотетический - делаем тысячи пакетов, грузим каждый ассет вручную, а уже десятки тысяч объектов как-то там выстроят взаимосвязь, может и пол года не пройдёт.

PS: Может в новой версии это работает лучше, давно не проверял. Мне интересно, как это работает со скриптами и рефлексией, если кто знает, опишите.
PPS: Что им мешало сделать общее хранилище по идентификатору(со счётчиком)? Почему нельзя было сделать автоматический подхват ссылок и автопрогрузку только требуемых для сцены объектов, при загрузке оной? Почему Опять контроль памяти ручками? Если систему закачки и формирование бандлов затронуть, то там ещё больше вопросов будет.

Если я правильно понимаю принцип работы бандлов, то они вообще не грузят скрипты, только данные, никакого кода. Что будет со стриппингом тоже интересный для меня вопрос. 
"Почему нельзя было сделать автоматический подхват ссылок и автопрогрузку только требуемых для сцены объектов, при загрузке оной?" 
Сейчас так и есть без всяких Addressables, вместе со сценой грузится только то, что нужно для этой сцены, если же мы используем Addressables, то можно делать InstantiateAsync для каждого объекта. Весь смысл Addressables в том чтобы отделить загрузку лёгких ассетов от тяжёлых, например, сцена может грузиться лоупольная стандартной механикой без Addressables, а после загрузки появляются хайполи модельки там где это нужно в первую очередь уже через асинхронную подгрузку.
"Почему Опять контроль памяти ручками?"
В самом конце статьи говорится о том что не нужен контроль ручками, не забываем только загружать и выгружать, это единственное условие.
На каждый InstantiateAsync нужен свой ReleaseInstance, дальше память будет сама за собой следить.

1