lenec ru

← все посты

Unity Addressables на практике: как структурировать ассеты без ада

19K

Addressables я подключал на трёх проектах разного масштаба: маленький мобильный F2P, средний ПК-инди и большой live-service на iOS+Android. Каждый раз через два-три месяца после внедрения я смотрел на структуру ассетов и думал «надо переделать». Получилось это сделать только на третьем проекте, и то не идеально. Расскажу, какие решения окупаются, какие — мина замедленного действия, и как настроить Addressables в Unity 6, чтобы потом не плакать.

Ориентируюсь на Addressables 2.x в Unity 6 (6000.x). Если у тебя 1.21 — основные понятия те же, но часть UI и API могут отличаться.

Зачем вообще Addressables, если есть Resources и AssetBundles

Если коротко: Resources — мина для производительности (всё содержимое грузится в память при первом обращении и сериализуется в одну большую кучу), AssetBundles — мощно, но ручной труд по 70%, со своим менеджером загрузок, версионированием и инвалидацией кэша. Addressables это, по сути, надстройка над AssetBundles с человеческим лицом: ты помечаешь ассет «адресом», Unity сама собирает бандлы, ты грузишь по адресу.

Что даёт это на практике:

  • Динамическая загрузка по строке-адресу: Addressables.LoadAssetAsync<GameObject>("Enemies/Goblin").
  • Деплой ассетов отдельно от билда (CDN, OTA-обновления).
  • Зависимости считаются автоматически: если префаб ссылается на материал, материал упакуется в нужный бандл.
  • Профилирование загрузки и памяти из коробки.

Минус — кривая обучения. Первый раз настраиваешь Groups, Profiles, Build Pipelines, Catalogs — и ничего не работает. Это типично, нужно один раз пройти.

Группы: как разбивать ассеты

Группы (Groups) — это, по сути, рецепт сборки бандла. У каждой группы свой Schema: что собирать, куда складывать, как сжимать.

Распространённый антипаттерн: одна группа на всё. Получаешь один гигантский бандл, при загрузке любого ассета подтягивается весь, и over-the-air обновления идут на 200 мегабайт.

Другой антипаттерн: группа на каждый ассет. Получаешь сотни мелких бандлов, overhead на запросы CDN, медленный старт.

Что работает у меня:

  • Local_Bootstrap — всё, что нужно сразу при запуске: лого, начальная сцена, базовый UI, небольшой набор шрифтов. Build & Load Path: Local.
  • Local_Common — ассеты, используемые во всём проекте: общие шейдеры, материалы, иконки. Тоже Local.
  • Remote_Levels_* — отдельная группа на каждый уровень или мир. Build Path: Remote, грузится с CDN по требованию.
  • Remote_Characters — герои/враги. Если их много, разбиваю по фракциям: Remote_Characters_Player, Remote_Characters_Enemies_Forest, Remote_Characters_Enemies_City.
  • Remote_UI_Skins — UI-темы, скины кнопок и панелей. Часто меняются маркетингом, удобно отдельно.

Идея простая: группа объединяет ассеты, которые загружаются вместе и обновляются вместе. Если игрок зашёл в локацию «лес» — он скачивает Remote_Levels_Forest + Remote_Characters_Enemies_Forest. Ничего лишнего.

Profiles: для разных окружений

Profiles — отдельная штука, которую часто игнорируют. Это набор переменных для путей: BuildTarget, RemoteBuildPath, RemoteLoadPath.

У меня минимум три профиля:

  • Default — для разработчика на локальной машине. Remote = http://localhost:8080, серверим бандлы простым Python http.server.
  • Staging — Remote = https://stage-cdn.mygame.com/[BuildTarget].
  • Production — Remote = https://cdn.mygame.com/[BuildTarget].

Между ними переключаешься в Addressables Settings. Это спасает от классического бага «забыл переключить путь, отправил билд в ревью с staging-CDN».

Загрузка по адресу: коротко и больно

Базовый паттерн загрузки:

public class EnemyFactory : MonoBehaviour
{
    [SerializeField] private AssetReferenceGameObject _goblinReference;

    public async Awaitable<GameObject> SpawnGoblin(Vector3 at)
    {
        var handle = _goblinReference.LoadAssetAsync<GameObject>();
        var prefab = await handle.Task;
        return Instantiate(prefab, at, Quaternion.identity);
    }
}

Несколько подводных камней:

Не теряй handle

Когда ты вызываешь LoadAssetAsync, возвращается AsyncOperationHandle. Если ты его не сохранил, не сможешь сделать Release и получишь утечку памяти.

private AsyncOperationHandle<GameObject> _goblinHandle;

public async Awaitable Init()
{
    _goblinHandle = _goblinReference.LoadAssetAsync<GameObject>();
    await _goblinHandle.Task;
}

private void OnDestroy()
{
    if (_goblinHandle.IsValid())
        Addressables.Release(_goblinHandle);
}

InstantiateAsync vs LoadAsset + Instantiate

У Addressables есть InstantiateAsync, который возвращает уже заинстанцированный объект. Удобно, но если ты хочешь спавнить десять врагов, это десять загрузок (с кэшем — десять обращений к менеджеру). Лучше один раз LoadAssetAsync, потом обычный Instantiate в цикле.

ReleaseInstance, не Destroy

Если ты использовал InstantiateAsync, удалять надо через Addressables.ReleaseInstance(go), а не Destroy. Иначе счётчик ссылок Addressables не обнулится, бандл не выгрузится.

AssetReference vs строки-адреса

Вопрос, который встаёт сразу: использовать AssetReference поля в инспекторе или грузить по строке?

AssetReference:

  • Тип проверяется компилятором. Не загрузишь Texture2D как GameObject случайно.
  • В инспекторе видишь, на что ссылка. Удобно искать через Find References.
  • Связи между сценами и префабами — нативные.

Строки:

  • Гибкость: можно собрать адрес динамически (например, $"Enemies/{enemyType}/{level}").
  • Удобно для data-driven: ScriptableObject с конфигом, а в нём строки.
  • Минус: опечатки ловишь только в рантайме.

Я обычно делаю гибрид: системные ссылки (UI, главный персонаж) — через AssetReference, контентные (враги, лут, генерируемые объекты) — через строки. Со временем в проекте появляется свой helper:

public static class AddressableKeys
{
    public static string Enemy(string type, int level) => $"Enemies/{type}/Lvl{level}";
    public static string Item(string id) => $"Items/{id}";
}

Хотя бы опечатки в одном месте.

Сборка бандлов: что конфигурировать

В Unity 6 в группах есть Schema — Content Packing & Loading. Главные параметры:

  • Bundle Mode. Pack Together — все ассеты группы в один бандл. Pack Separately — каждый ассет в свой бандл. Pack Together by Label — по лейблу. Для контентных групп почти всегда Pack Together.
  • Compression. LZ4 — быстро декомпрессируется, средний размер. LZMA — лучшее сжатие, но медленнее. Для мобилок я ставлю LZ4 на горячее (бутстрап) и LZMA на редкое (DLC, новые сезоны).
  • Bundle Naming Mode. Лучше Filename (без хеша). Тогда обновлять можно без полной пересборки каталога — это критично для OTA.

Build Pipeline почти всегда — Default Build Script. Кастомные пайплайны нужны редко: для собственных шифрований бандлов или интеграции с CI.

OTA-обновления: где обычно ломается

Главная фишка Addressables — отдельный билд ассетов от билда приложения. Игроки качают новый контент через CDN без захода в стор. На бумаге.

На практике натыкаешься на:

Mismatch версии каталога и приложения

Если в новой версии ассета изменилось API скрипта, который этот ассет использует, старое приложение не сможет инстанцировать его. Решение: ставить Player Version Override в Addressables Settings и проверять совместимость в рантайме перед загрузкой.

Обновление без Update a Previous Build

Кнопка Update a Previous Build (а не New Build → Default Build Script) — обязательна для OTA. Она пересобирает только изменившиеся бандлы и патчит каталог. Если жмёшь New Build, у пользователей все хеши изменились, нужно скачать всё заново.

Кэш CDN

CloudFront, Cloudflare, любой CDN кэширует catalog.json и catalog.bin. Каждый раз после деплоя нужно делать invalidate. Я однажды три часа искал баг, пока понял, что CDN раздаёт старый каталог, хотя в S3 уже новый.

Профилирование: что смотреть

В Window → Asset Management → Addressables → Event Viewer показывает, что грузится, когда, и как меняется reference count. Полезно прогонять перед релизом — видно, что-то висит загруженным дольше нужного.

Memory Profiler — другой инструмент, показывает память по бандлам. Если видишь, что после смены сцены бандл всё ещё в памяти — кто-то держит ссылку. Часто это статические менеджеры с кэшем префабов.

Build Layout Report (можно включить в настройках) — html-отчёт после билда, показывает, что в каждый бандл попало. Спасает от «ой, у меня в Local_Bootstrap половина игры, потому что один префаб случайно ссылается на ScriptableObject, который ссылается на огромный массив».

Типичные ошибки и как их ловить

Несколько граблей, на которые наступал я и видел у других:

  • Цикл в зависимостях. Префаб A ссылается на префаб B, B на C, C на A. Addressables соберёт, но при загрузке любого из них поднимет всех троих. Если один из них тяжёлый — неприятный сюрприз.
  • Дубликаты материалов. Build Layout показывает, как один материал попал в три разных бандла. Это значит, что три ассета его используют, и материал не вынесен в отдельную группу. Кладёшь в Local_Common или Remote_Shared_Materials.
  • Сцены в Addressables vs Build Settings. Сцена, помеченная Addressable, не должна быть в Build Settings (галка в Build Settings). Иначе попадёт и туда, и в бандл.
  • Сериализованные ссылки на не-Addressable объекты. Если в Addressable префабе поле ссылается на ассет, не помеченный как Addressable, — он автоматически попадает в бандл этого префаба. Может неожиданно раздуть бандл.

Что почитать и что попробовать

Начни с официального гайда: документация Addressables в Unity Manual + туториал на Learn. Дальше — серия статей Ryan Hipple про дизайн ассет-структур (он сейчас в Unity, был на консультациях у нескольких студий).

Если уже работаешь — попробуй один эксперимент. Сделай минимальный проект: одна сцена, один префаб, один материал. Помечай Addressable, выноси в группу, билдишь, грузишь по строке. Прогоняешь Event Viewer и Build Layout Report. Через час понимаешь поток данных лучше, чем после трёх статей.

Главное правило, к которому я в итоге пришёл: Addressables — это инструмент для проектов, где у тебя есть структура контента и понимание, что и когда грузится. На прототипах из 50 префабов это оверкилл, и обычные ссылки работают быстрее в разработке. На проекте с тысячами ассетов и OTA — без Addressables (или эквивалента) ты не справишься.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.