Рассказы из окопов оптимизации: Экономия памяти с помощью Addressables

Эффективная передача данных в память и из памяти - ключевой элемент любой качественной игры. Будучи консультантом в нашей команде профессиональных услуг, я стремился повысить эффективность многих проектов клиентов. Именно поэтому я хочу поделиться некоторыми советами о том, как использовать систему Addressables Asset System от Unity для улучшения вашей стратегии загрузки контента.
Память - это ограниченный ресурс, которым необходимо тщательно управлять, особенно при переносе проекта на новую платформу. Использование Addressables может улучшить память во время выполнения за счет внедрения слабых ссылок для предотвращения загрузки ненужных активов. Слабые ссылки означают, что вы контролируете, когда ссылаемый актив загружается в память и выводится из нее; Addressables System сама найдет все необходимые зависимости и загрузит их тоже. В этом блоге мы рассмотрим ряд сценариев и проблем, с которыми вы можете столкнуться при настройке проекта на использование Unity Addressable Asset System, а также объясним, как их распознать и оперативно устранить.

В этой серии рекомендаций мы будем работать с простым примером, который устроен следующим образом:
- У нас есть скрипт InventoryManager в сцене со ссылками на наши три инвентарных актива: Префабы меча, меча босса, щита.
- Эти средства не нужны постоянно во время игры.
Вы можете загрузить файлы проекта для этого примера на моем GitHub. Мы используем пакет предварительного просмотра Memory Profiler для просмотра памяти во время выполнения. В Unity 2020 LTS перед установкой этого пакета из менеджера пакетов необходимо включить предварительный просмотр пакетов в Настройках проекта.
Если вы используете Unity 2021.1, выберите опцию Добавить пакет по имени из дополнительного меню (+) в окне Менеджера пакетов. Используйте имя "com.unity.memoryprofiler".
Давайте начнем с самой простой реализации, а затем перейдем к оптимальному подходу к настройке содержимого Addressables. Мы просто применим жесткие ссылки (прямое назначение в инспекторе, отслеживаемое по GUID) к нашим префабам в MonoBehaviour, который существует в нашей сцене.

Когда сцена загружается, все объекты в ней также загружаются в память вместе со своими зависимостями. Это означает, что каждый префаб, перечисленный в нашей InventorySystem, будет находиться в памяти, вместе со всеми зависимостями этих префабов (текстуры, сетки, звук и т.д.).
Создав сборку и сделав снимок с помощью Memory Profiler, мы видим, что текстуры для наших активов уже хранятся в памяти, хотя ни одна из них не была инстанцирована.

Проблема: В памяти хранятся активы, которые нам сейчас не нужны. В проекте с большим количеством инвентарных позиций это приведет к значительному увеличению объема памяти во время выполнения.
Чтобы избежать загрузки ненужных активов, мы изменим нашу систему инвентаризации на использование Addressables. Использование ссылок на объекты вместо прямых ссылок предотвращает загрузку этих объектов вместе с нашей сценой. Давайте перенесем наши префабы инвентаря в группу Addressables и изменим InventorySystem, чтобы она могла создавать и освобождать объекты с помощью Addressables API.

Создайте проигрыватель и сделайте снимок. Обратите внимание, что ни один из активов еще не находится в памяти, что очень хорошо, потому что они еще не были инстанцированы.

Инстанцируйте все элементы, чтобы увидеть, как они корректно отображаются в памяти вместе со своими активами.

Проблема: Если мы инстанцируем все наши предметы и депаундируем меч босса, мы все равно увидим текстуру меча босса "BossSword_E " в памяти, даже если она не используется. Причина в том, что, хотя вы можете частично загружать пакеты активов, невозможно автоматически частично выгружать их. Такое поведение может стать особенно проблематичным для бандлов с большим количеством активов, например, для одного AssetBundle, который включает в себя все наши префабы инвентаря. Ни один из активов в пакете не будет выгружен до тех пор, пока весь AssetBundle не перестанет быть нужным, или пока мы не вызовем дорогостоящую процессорную операцию Resources.UnloadUnusedAssets().


Чтобы решить эту проблему, мы должны изменить способ организации наших AssetBundles. Хотя сейчас у нас есть одна Addressables Group, которая упаковывает все свои активы в один AssetBundle, вместо этого мы можем создать AssetBundle для каждого префаба. Эти более детальные AssetBundles снимают проблему сохранения в памяти больших пакетов активов, которые нам больше не нужны.
Сделать это очень просто. Выберите Addressables Group, затем Content Packaging & Loading > Advanced Options > Bundle Mode и перейдите в Inspector, чтобы изменить режим упаковки с Pack Together на Pack Separately.
Используя Pack Separately для создания Addressable Group, вы можете создать AssetBundle для каждого актива в Addressables Group.

Активы и пакеты будут выглядеть следующим образом:

Теперь вернемся к нашему первоначальному тесту: Спавн трех предметов и последующий депаунинг меча босса больше не оставляют в памяти ненужных активов. Текстуры меча босса теперь выгружаются, поскольку весь пакет больше не нужен.
Проблема: Если мы спауним все три предмета и сделаем захват памяти, в памяти появятся дубликаты активов. Точнее, это приведет к появлению нескольких копий текстур "Sword_N" и "Sword_D". Как это может произойти, если мы меняем только количество пакетов?

Чтобы ответить на этот вопрос, давайте рассмотрим все, что входит в три созданных нами пакета. Хотя мы поместили в связки только три префаба, есть и дополнительные активы, которые неявно попадают в эти связки как зависимости от префабов. Например, в активе префаба меча также есть активы сетки, материалов и текстур, которые необходимо включить. Если эти зависимости не включены явно в Addressables, то они автоматически добавляются в каждый пакет, которому они нужны.

Addressables включает окно анализа, помогающее диагностировать расположение пучков. Откройте Window > Asset Management > Addressables > Analyze и запустите правило Bundle Layout Preview. Здесь мы видим, что комплект меча явно включает в себя sword.prefab, но в него также включено множество неявных зависимостей.

В том же окне выполните команду Check Duplicate Bundle Dependencies. Это правило выделяет активы, включенные в несколько пакетов активов на основе нашего текущего макета Addressables.

Мы можем предотвратить дублирование этих активов двумя способами:
1. Поместите префабы Sword, BossSword и Shield в один пакет, чтобы у них были общие зависимости, или
2. Явно включите дублированные активы в Addressables
Мы хотим избежать размещения нескольких префабов инвентаря в одном пакете, чтобы предотвратить сохранение ненужных активов в памяти. Поэтому мы добавим дублированные активы в их собственные комплекты (Bundle 4 и Bundle 5).

В дополнение к анализу наших пакетов правила анализа могут автоматически исправлять неработающие активы с помощью правил Fix Selected. Нажмите эту кнопку, чтобы создать новую адресную группу под названием "Duplicate Asset Isolation", в которую войдут четыре дублированных актива. Установите для этой группы режим Bundle Mode на Pack Separately, чтобы предотвратить сохранение в памяти других активов, которые больше не нужны.

Использование этой стратегии AssetBundle может привести к проблемам при масштабировании. Для каждого AssetBundle, загружаемого в определенный момент времени, существует избыточная память для метаданных AssetBundle. Эти метаданные, скорее всего, будут занимать неприемлемый объем памяти, если мы увеличим масштаб текущей стратегии до сотен или тысяч элементов инвентаризации. Подробнее о метаданных AssetBundle читайте в документации Addressables.
Просмотрите текущий расход памяти метаданных AssetBundle в профилировщике Unity. Перейдите к модулю памяти и сделайте снимок памяти. Загляните в категорию Другие > SerializedFile.

Для каждого загруженного AssetBundle в памяти имеется запись SerializedFile. В этой памяти хранятся метаданные AssetBundle, а не реальные активы в пакетах. Эти метаданные включают в себя:
- Два буфера для чтения файлов
- Дерево типов, в котором перечислены все уникальные типы, включенные в пакет
- Оглавление, указывающее на активы
Из этих трех элементов больше всего места занимают буферы чтения файлов. Эти буферы имеют размер 64 КБ каждый на PS4, Switch и Windows RT и 7 КБ на всех остальных платформах. В приведенном выше примере 1 819 пакетов * 64 КБ * 2 буфера = 227 МБ только для буферов.
Поскольку количество буферов линейно зависит от количества AssetBundles, простым решением для уменьшения памяти является загрузка меньшего количества пакетов во время выполнения. Однако ранее мы избегали загрузки больших пакетов, чтобы предотвратить сохранение в памяти ненужных активов. Как же уменьшить количество пакетов, сохранив при этом детализацию?
Первым правильным шагом будет группировка активов по их использованию в приложении. Если вы можете сделать разумные предположения, основанные на вашем приложении, то вы можете сгруппировать активы, которые, как вы знаете, всегда будут загружаться и выгружаться вместе, например, сгруппировать активы на основе уровня игры, на котором они находятся.
С другой стороны, вы можете оказаться в ситуации, когда не можете с уверенностью предположить, когда ваши активы понадобятся или не понадобятся. Например, если вы создаете игру с открытым миром, вы не можете просто сгруппировать все из лесного биома в один пакет активов, потому что ваши игроки могут взять предмет из леса и перенести его в другой биом. Весь пакет леса остается в памяти, потому что игроку все еще нужен один актив из леса.
К счастью, есть способ уменьшить количество пакетов, сохранив при этом необходимый уровень детализации. Давайте разумнее подходить к дедупликации наших пакетов.
Встроенное правило анализа дедупликации, которое мы запустили, обнаруживает все активы, находящиеся в нескольких пакетах, и эффективно перемещает их в одну Addressables Group. Установив для этой группы значение " Упаковать отдельно", мы получим один актив в каждом пакете. Однако есть некоторые дублирующиеся активы, которые можно смело упаковывать вместе, не создавая проблем с памятью. Рассмотрите приведенную ниже диаграмму:

Мы знаем, что текстуры "Sword_N" и "Sword_D" являются зависимостями одних и тех же пакетов (Bundle 1 и Bundle 2). Поскольку у этих текстур одни и те же родители, мы можем смело упаковывать их вместе, не создавая проблем с памятью. Обе текстуры меча должны быть всегда загружены или разгружены. Никогда не возникает опасений, что одна из текстур может остаться в памяти, поскольку не бывает случаев, когда мы специально используем одну текстуру, а не другую.
Мы можем реализовать эту улучшенную логику дедупликации в нашем собственном правиле Addressables Analyze Rule. Мы будем работать с существующим правилом CheckForDupeDependencies.cs. Полный код реализации можно увидеть в примере Inventory System. В этом простом проекте мы всего лишь сократили общее количество пучков с семи до пяти. Но представьте себе сценарий, в котором ваше приложение имеет сотни, тысячи или даже больше дубликатов активов в Addressables. В ходе работы с компанией Unknown Worlds Entertainment по оказанию профессиональных услуг для ее игры Subnautica в проекте изначально было 8 718 пакетов после использования встроенного правила анализа дедупликации. Мы сократили это число до 5 199 пучков после применения пользовательского правила для группировки дедуплицированных активов на основе их родительских пучков. Подробнее о нашей работе с командой вы можете узнать из этого кейса.
Таким образом, количество пакетов сократилось на 40 %, при этом в них осталось то же содержимое и сохранился тот же уровень детализации. Сокращение количества пакетов на 40 % аналогично уменьшило размер SerializedFile во время выполнения на 40 % (с 311 МБ до 184 МБ).
Использование Addressables позволяет значительно сократить потребление памяти. Вы можете еще больше сократить объем памяти, организовав AssetBundles в соответствии с вашим сценарием использования. В конце концов, встроенные правила анализа консервативны, чтобы подходить для всех приложений. Написание собственных правил анализа позволяет автоматизировать компоновку пакетов и оптимизировать ее для вашего приложения. Чтобы выявить проблемы с памятью, продолжайте часто создавать профили и проверяйте окно Analyze, чтобы узнать, какие активы явно и неявно включены в ваши пакеты. Ознакомьтесь с документацией по Addressables Asset System, чтобы узнать о лучших практиках, руководстве по началу работы и расширенной документации по API.
Если вы хотите получить больше практической помощи, чтобы узнать, как улучшить управление контентом с помощью Addressables Asset System, свяжитесь с отделом продаж и узнайте о профессиональном учебном курсе.