Pull to refresh

Unity Addressables: памяти хватит всегда

Reading time 13 min
Views 25K
Original author: Ruben Torres Bonet
image

Вы руководите командой из нескольких программистов и художников, работающих над портированием красивой VR-игры на PS4 под Oculus Quest. У вас есть на это шесть месяцев. Каким будет ваш первый ход? Давайте попробуем воспользоваться Unity Addressables.

Вы понимаете, что придётся одновременно решать несколько довольно трудных задач. Некоторые будут для вас сложнее других, это зависит от вашего опыта в каждой из областей. Если выбирать, какая из них лишала вас сна чаще всего, то что это будет?

Предположу следующее: примерно 70% читателей скажут, что самой большой проблемой при портировании игры на Quest является производительность ЦП/GPU. Я могу ответить на это: скорее всего, вы правы. Повышение производительности — одна из сложнейших областей в VR-игре. Для оптимизаций такого типа необходимо глубокое изучение продукта, а на это требуется время. Иногда дальнейшая оптимизация невозможна, из-за чего обычно приходится избавляться от затратных элементов геймплея и графики. А разочаровывать игроков опасно.

Скорость, скорость, скорость… Чего же ожидать в этом отношении от платформы Quest? Насколько она производительна? Дело в том, что если вы уже имели опыт разработки на ней, то знаете, что несмотря на её мобильность, она на удивление мощна.

«Да ладно, автор, зачем ты врёшь? Мой телефон начинает тормозить, как только я открываю вторую вкладку браузера. Как ты можешь говорить, что мобильные платформы способны быть производительными?»

Огромная разница заключается в системе активного охлаждения Quest, дающей огромное преимущество для её ЦП/GPU, не обеспечиваемое ни одной другой мобильной платформой. Это мощный вентилятор, сдувающий пыль с ваших волос, и не дающий процессору расплавиться на вашем лице.

Кроме того, более специализированная ОС лучше оптимизирована для рендеринга виртуальной реальности (сюрприз), чем стандартный Android. За последние несколько лет мобильное железо начало быстро догонять стационарные платформы.

Но в то же время не стану отрицать, что задача постоянного рендеринга с частотой 72 fps будет сложной, особенно для портов VR-игр, приходящих с мощных платформ. Когда мы говорим об Oculus Quest, то можете представить себе Snapdragon 835 с экраном, аккумулятором, четырьмя камерами и вентилятором.

То, что выглядит недостатком, можно на самом деле воспринимать как преимущество. Эта мобильная платформа — хорошо исследованное устройство со слабым железом. Можно сказать, что существует тысяча известных трюков для быстрого снижения нагрузки на ЦП и GPU до приемлемого уровня. Если вам это интересно, то можете почитать об этом в моих последующих постах. А в этой статье мы вынесем производительность за скобки.

Привлечь наше внимание в этой проблеме может то, что по сравнению с PS4 у Quest есть одна аппаратная характеристика в два раза меньше: ёмкость ОЗУ. Именно, объём снизился с 8 до 4 ГБ ОЗУ. Это аппроксимация, потому что на обеих платформах операционная система не позволяет использовать их полностью, чтобы можно было отслеживать несколько подсистем, необходимых для работы экосистемы. В Quest можно использовать примерно до 2,2 ГБ ОЗУ; если больше, то уже начинается хаос.

«Но что же ты подразумеваешь под хаосом?» Дело в том, что для игры критически важно реализовать правильное управление памятью. Так получилось, потому что у нас есть два ограничения:

  • Жёсткое ограничение памяти: если превысить определённый порог, то ОС просто убьёт игру
  • Мягкое ограничение памяти

Очевидно, мы не хотим, чтобы в игре произошло первое или второе. Можете представить ярость игрока, потерявшего последние два часа прохождения? Да, он обязательно зайдёт в магазин приложений и не скажет там ничего приятного.

Конечно, гарантированное наличие 2,2 ГБ ОЗУ — это не так много. Обычно это не проблема для новых проектов, в которых статистика постоянно отслеживается с самого начала, но определённо становится затруднением для порта на сильно более слабом железе.

Если вы имели дело с похожими портами в прошлом, то быстро поймёте, как чрезвычайно сложно становится вдвое снижать количество доступной ОЗУ. Это сильно зависит от того, насколько хорошо архитектура игры была готова к такому изменению, но в большинстве случаев приводит только к слёзам.

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

Не все платформы поддерживают одинаковые параметры импорта: при разработке под разные устройства значительно увеличиваются затраты на конвейер сборки, не говоря уже о сложности QA, графики дизайна и программирования. Например, поддерживает ли это Android-устройство ASTC, или только ETC2 (и вообще что-нибудь из них)? О, и нам ведь ещё нужны 64-битные сборки, но в то же время мы хотим сохранить игроков с 32-битными версиями. Сколько отдельных APK нам нужно создать и протестировать для каждого обновления, выполняемого в игре? Если вы хотите упросить себе жизнь, то не стоит полагаться только на эти техники.

Поэтому нам нужно двинуться глубже. Разумеется, мы хотим, чтобы всё было как можно проще, особенно при создании порта. Переработка игры целиком ради производительности — ещё худший вариант, чем просто её не портировать. В рамках темы статьи я покажу одно из самых важных преимуществ: вы узнаете, как всего за несколько часов уменьшить требуемый объём памяти в два раза.

Разве это не здорово?

Ну давайте, давайте, спросите меня: неужели это действительно возможно в вашем случае? Я отвечу: это зависит от начальных условий, но по моему опыту, ответом будет ДА. Unity Addressables могут оказать здесь огромную услугу. В чём же хитрость? Вам придётся вложить силы и освоить процесс. Но такой рабочий процесс позволит вам завоевать звание сотрудника месяца.

Если вы заинтересовались, то продолжайте чтение.

В этом посте мы пройдём путь от традиционного управления ассетами до системы управления ассетами на основе addressables. Чтобы проиллюстрировать этот процесс, мы портируем упрощённый олдскульный проект в новую эпоху Unity Addressables.

Вы можете задать вопрос: почему бы тебе просто не показать результат на твоей реальной работе?

В мире без конкуренции я бы просто показал вам все созданные мной материалы. Однако в реальном мире, меня скорее всего за это накажут. А то и посадят.

Поэтому вместо этого я предлагаю свою помощь: мы с вами проработаем проект, в котором представлены все сложности, с которым вы столкнётесь завтра в своём следующем проекте. И начнём мы с того, то примем в свою семью рекомендуемых пакетов Unity Addressables.

В этом посте я познакомлю вас с Addressables, чтобы вы могли за считанные минуты реализовать собственную систему Unity Addressables.


Unity Addressables: зачем они нужны?


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

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

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

Насколько это плохо?

Ситуации бывают разные. Если ваша игра скорее всего будет ограничена объёмом памяти? то это очень рискованное решение, потому что игра плохо будет масштабироваться с увеличением количества добавляемых ассетов (например, подумайте о будущих DLC). Если вы выполняете разработку для разнородных устройств, например, для Android, то у вас нет единого объёма памяти; каждое устройство имеет собственную ёмкость, поэтому придётся рассчитывать на наихудший случай. ОС может в любой момент решить убить приложение, если пользователь вдруг переключится, чтобы ответить на сообщение в Facebook. Когда он вернётся, его будет ждать сюрприз — игра уже была закрыта.

Весело ли это?

Совершенно нет.

Усложняет ситуацию и то, что если позже вы решите (или кто-то решит за вас) портировать игру на другую, менее мощную платформу с сохранением кросс-плея, то вам остаётся только пожелать удачи. Вам точно не захочется столкнуться с такой технической проблемой.

С другой стороны, существуют ли ситуации, в которых вполне подходит традиционное управление ассетами? Да, конечно. Если вы выполняете разработку для однородной платформы, например, для PS4 и большинство требований известно с самого начала, преимущества глобальных объектов потенциально могут перевесить дополнительную сложность улучшенной системы управления памятью.

Потому что нужно признать: старый добрый глобальный объект, хранящий всё, что нам нужно — это простое решение, если оно вам подходит. Оно упростит код и заранее загрузит все ассеты, на которые выполняются ссылки.

Как бы то ни было, традиционное управление памятью неприемлемо для разработчиков, стремящихся максимально использовать ресурсы железа. Вы читаете статью, а значит, хотите повысить свои навыки. Поэтому настало время это сделать.

Встречайте Unity Addressables.

Требования к проекту с Unity Addressables


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

  • Руки
  • Умная голова
  • Unity 2019.2.0f1 или выше
  • Проект уровня 1 с GitHub (скачайте zip или через командную строку)
  • Желание покопаться во внутренностях Unity Addressables

Репозиторий git содержит три коммита, по одному на каждый уровень этого поста (если только я чего-то не перепутаю и не создам коммит с исправлением).

Скачайте проект в формате ZIP непосредственно с GitHub


Разработчик уровня 1: традиционное управление ассетами


Мы начнём с простейшего метода управления ассетами. В нашем случае для этого придётся составить список прямых ссылок на материалы скайбокса в компоненте.

Если вы будете делать это со мной, то подготовка займёт три простых шага:

  1. Скачайте проект с git
  2. Откройте проект в Unity
  3. Нажмите кнопку play!

Отлично. Теперь можно понажимать на кнопки, чтобы менять скайбокс. Так оригинально… и скучно. Как я понимаю, пока никаких Unity Addressables.

Вскоре вы увидите, зачем нам терпеть эти мгновения скуки.

Во-первых, как структурирован наш проект? Он опирается на две основные системы. С одной стороны, у нас есть игровой объект Manager. Этот компонент является основным скриптом, хранящим ссылки на материалы скайбокса и переключающий их в зависимости от событий UI. Всё довольно просто.

using UnityEngine;

public class Manager : MonoBehaviour
{
    [SerializeField] private Material[] _skyboxMaterials;

    public void SetSkybox(int skyboxIndex)
    {
        RenderSettings.skybox = _skyboxMaterials[skyboxIndex];
    }
}

Manager предоставляет системе UI функцию для применения к сцене определённого материала благодаря использованию API RenderSettings.

Во-вторых, у нас есть CanvasSkyboxSelector. Этот игровой объект содержит компонент холста, рендерящий набор распределённых по вертикали кнопок. Каждая кнопка при нажатии вызывает вышеупомянутую функцию Manager, заменяющую рендерящийся скайбокс в зависимости от id кнопки. Иными словами, событие OnClick каждой кнопки вызывает функцию SetSkybox объекта Manager. Всё просто, не так ли?


Unity Addressables — иерархия сцены

Теперь настало время приступать к работе. Давайте откроем профилировщик (ctrl/cmd + 7 или Window — Analysis — Profiler). Я буду считать, что вы знакомы с этим инструментом, а значит, знаете, что делать с верхней кнопкой record. После нескольких секунд записи остановите её и посмотрите метрики: CPU, memory и т.п. Есть что-нибудь интересное?

Производительность довольно хороша, и это неудивительно, учитывая масштаб проекта. Можно просто превратить этот проект в VR-игру и я гарантирую, что никого из игроков не стошнит, как это часто бывает в Eve: Valkyrie.

В нашем случае мы сосредоточимся на разделе памяти. В простом режиме просмотра вы увидите примерно такую картину:


Управление ассетами уровня 1 — простое профилирование памяти

Значения размера текстуры кажутся слишком большими для отображения одного скайбокса за раз, не находите? Вас ждёт сюрприз: подобный паттерн можно найти во многих неоптимизированных играх, разработкой которых вы будете руководить. Но в нашем случае это просто набор скайбоксов. В других проектах это будут персонажи, планеты, звуки, музыка…

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

Настало время магии. Переключим профилировщик памяти в подробный режим. Смотрите!


Управление ассетами уровня 1 — подробное профилирование памяти

Чёрт возьми, что здесь произошло? Все текстуры скайбокса загружены в память, но одновременно отображается только одна из них. Видите, что у нас получилось? Эта сырая архитектура занимает целых 400 МБ.

Это определённо нас не устраивает, учитывая, что это всего лишь небольшой кусочек будущей игры. Решение этой самой проблемы станет основой следующего раздела.

Подведём итог:

  • Традиционное управление ассетами подразумевает прямые ссылки
  • Поэтому все объекты загружены постоянно
  • Проект при этом плохо масштабируется


Разработчик уровня 2: процесс работы с Unity Addressables


В играх мы начинаем с уровня 1, и это нас устраивает, но как только мы разберёмся с правилами геймплея, настаёт время покинуть безопасные городские стены и повышать свой уровень. Именно этому посвящён данный раздел.

Теперь скачайте проект уровня 2.

Как мы видели ранее в профилировщике, все скайбоксы загружены в память, даже несмотря на то, что активно используется только один. Это решение не масштабируется, потому что на каком-то этапе мы окажемся ограниченными количеством различных вариаций ассетов, которые можем предложить игроку. Какой я могу дать совет? Не ограничивайте интересность игры для пользователей.

Позвольте мне вам помочь. Возьмём лопату, чтобы прорыть туннель для побега из тюрьмы традиционного управления ассетами. Давайте добавим в наш набор новый интересный инструмент: API Unity Addressables.

Первое, что нам надо сделать — установить пакет Addressables. Для этого перейдём в Window → Package Manager:


Unity Package Manager — Unity Addressables

После установки нам нужно пометить материалы как addressables. Выберем их и активируем флаг addressables в окне инспектора.


Управление ассетами уровня 2 (Unity Addressables)

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

Сейчас я покажу вам кое-что крутое. Откройте Window → Asset Management → Addressables. Догадываетесь, что это? Эта наша база данных, которой не терпится ожить!


Управление ассетами уровня 2 (Unity Addressables) — главное окно

Дорогой читатель, это была лёгкая часть. А теперь начинается интересная.

Я хочу, чтобы вы посетили нашего старого друга из предыдущего раздела: сэра Manager. Если его проверить, то мы обнаружим, что он по-прежнему хранит прямые ссылки на ассеты! Нам этого не нужно.

Вместо этого мы научим менеджер использовать косвенные ссылки, т.е. AssetReference (в Unreal Engine они называются мягкими ссылками — soft references).

Давайте сделаем наш компонент красивее:

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class Manager : MonoBehaviour
{
    [SerializeField] private List<AssetReference> _skyboxMaterials;

    private AsyncOperationHandle  _currentSkyboxMaterialOperationHandle;

    public void SetSkybox(int skyboxIndex)
    {
        StartCoroutine(SetSkyboxInternal(skyboxIndex));
    }

    private IEnumerator SetSkyboxInternal(int skyboxIndex)
    {
        if (_currentSkyboxMaterialOperationHandle.IsValid())
        {
            Addressables.Release(_currentSkyboxMaterialOperationHandle);
        }

        var skyboxMaterialReference = _skyboxMaterials[skyboxIndex];
        _currentSkyboxMaterialOperationHandle = skyboxMaterialReference.LoadAssetAsync();
        yield return _currentSkyboxMaterialOperationHandle;
        RenderSettings.skybox = _currentSkyboxMaterialOperationHandle.Result;
    }
}

Здесь происходит следующее:

  • Серьёзное изменение происходит в строке 7, где мы храним список косвенных ссылок (AssetReference) вместо прямых ссылок на материалы. Это изменение является ключевым, потому что эти материалы НЕ будут загружаться автоматически только когда на них ссылаются. Их загрузку необходимо будет выполнять явным образом. Затем изменим значение поля в редакторе.
  • Строка 13: поскольку теперь мы выполняем асинхронный рабочий процесс, лучше использовать корутину. Мы просто запустим новую корутину, которая будет заниматься изменением материала скайбокса
  • В строках 18-20 мы проверяем, есть ли у нас существующий дескриптор материала скайбокса, и если есть, то мы освобождаем скайбокс, который рендерили ранее. Каждый раз, когда мы выполняем такую операцию загрузки при помощи API Addressables, мы получаем дескриптор, который нужно хранить для будущих операций. Дескриптор — это просто структура данных, содержащая данные, относящиеся к управлению определённым addressable-ассетом.
  • Мы преобразуем ссылку на конкретный addressable в материал скайбокса в строке 23, а затем вызываем его функцию LoadAssetAsync, над которой можно выполнить операцию yield (строка 25), чтобы перед продолжением работы программы мы могли дождаться завершения этой операции. Благодаря использованию дженериков нет необходимости в использовании неприятных преобразований типов. Отлично!
  • Наконец, после того, как материал и его зависимости загрузятся, мы начинаем менять скайбокс сцены в строке 26. Материал будет передан в поле Result, которое принадлежит дескриптору, использованному для его загрузки.


Управление ассетами уровня 2 (Unity Addressables) — список AssetReference

Помните: этот код не готов к продакшену. Не используйте его при программировании самолёта. Я решил, что ради простоты стоит пожертвовать надёжностью.

Но хватит объяснений. Пора увидеть его в действии.

Пожалуйста, выполните следующие шаги:

  1. В окне addressables приготовьте контент (build player content)
  2. Затем выполните сборку для выбранной платформы
  3. Запустите её и подключите к ней профилировщик (памяти).
  4. Не уроните челюсть от удивления.


Уровень 2 (Unity Addressables) — Build Player Content


Управление памятью уровня 2 (Unity Addressables) — профилировщик памяти

Разве приготовленные ассеты не вкусны?

Мне нравится, когда профилировщик доволен. А сейчас мы видим самый счастливый профилировщик в мире. А довольный профилировщик означает следующее: во-первых, больше довольных игроков, которые смогут поиграть в вашу игру хоть на Nokia 3210. Во-вторых, это довольные продюсеры. А для вас это означает, что будет доволен ваш кошелёк.

В этом и состоит мощь системы Addressables.

Но Addressables накладывают небольшие дополнительные трудозатраты. С одной стороны, программистам нужно обеспечить поддержку асинхронных рабочих процессов (это легко реализуется при помощи корутин). Кроме того, дизайнерам придётся изучать возможности системы, например, addressable groups, и набраться опыта для принятия осознанных решений. И, наконец, IT-отдел очень обрадуется тому, что придётся настраивать инфраструктуру для передачи ассетов по сети, если вы предпочтёте хостить их онлайн.

Должен вас поздравить. Объясню, чего мы добились:

  • Правильное управление памятью.
  • Ускорение первоначальной загрузки.
  • Ускоренное время установки, уменьшение размера приложения в магазине.
  • Повышенная совместимость с устройствами.
  • Асинхронная архитектура.
  • Открыли дверь для хранения этого контента онлайн → то есть к отделению данных от кода.

Я бы гордился таким достижениями. Это хорошая отдача от наших вложений труда.

О, и не забудьте упомянуть про опыт работы с Addressables на собеседовании.

Вспомогательные материалы: создание инстансов и подсчёт ссылок. Информацию по этой теме можно прочитать в моём посте.

Дополнительно: альтернативные стратегии загрузки. Прочитать о них можно в моём посте.

Подведём итог:

  • Управление ассетами на основе Addressables замечательно масштабируется.
  • Addressables добавляют асинхронное поведение
  • Не забывайте подготавливать контент при изменениях, иначе игра будет иметь розоватый оттенок!


Управление ассетами уровня 3 (??) — сетевая доставка контента

Управление ассетами уровня 3 (??) — сетевая доставка контента


В предыдущем разделе мы совершили самый важный прорыв. Повысили свои навыки, перейдя от традиционной системы управления ассетами к рабочему процессу на основе addressables. Это огромная победа для проекта, потому что благодаря небольшим затратам времени мы обеспечили пространство для масштабирования объёма ассетов, сохраняя при этом низкий уровень потребления памяти. Это достижение на самом деле повысило вас до уровня 2, поздравляю! Однако нам предстоит ответить на ещё один вопрос:

Всё ли это?

Нет. Мы едва коснулись темы Addressables, существуют и другие способы улучшения проекта благодаря этому мощному пакету.

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

Из него вы узнаете о следующих аспектах:

  • Окно Addressables: важные подробности
  • Профилирование Addressables: не позвольте утечкам памяти испортить вам жизнь
  • Сетевая доставка: снижение времени с начала установки до игрового процесса
  • Интеграция в конвейер сборок
  • Практические стратегии: ускорение рабочего процесса, избавляющие от необходимости десятиминутных кофе-брейков

И, что ещё более важно, мы ответим на следующие вопросы:

  • В чём заключается скрытый смысл Send Profiler Events?
  • Насколько полезен API AddressableAssetSettings?
  • Как интегрировать всё это с API BuildPlayerWindow?
  • В чём разница между Virtual Mode и Packed Mode?

Руководство по уровню 3 можно прочитать в моём посте.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+5
Comments 1
Comments Comments 1

Articles