Альтернативы CDN: наша система дозакачки контента напрямую из стора в игру

Что такое dynamic delivery и как с ее помощью мы экономим место на девайсах игроков.

Альтернативы CDN: наша система дозакачки контента напрямую из стора в игру

War Robots всегда была игрой, насыщенной контентом, а потому места в памяти девайсов занимала немало. Раньше эти цифры колебались в районе 800 МБ, но масштабная переработка графики в ремастере игры, как бы ни хотелось, не смогла бы оставаться в тех же пределах и наверняка бы привела к увеличению размера приложения в несколько раз. Что и случилось в нашем случае: вес клиента достиг 2,3 ГБ — в три раза больше «ванильной» версии. В то же время мы понимали, что HD-пресет потянет далеко не каждый мобильный телефон, а значит — нет смысла заставлять игроков качать ненужные гигабайты и тем самым ломать себе воронку конверсий.

Выход? Базовое качество скачивать по умолчанию и выбирать при первом запуске, а HD-пресет предлагать пользователю позже — и только если устройство его поддерживает.

Все это подстегнуло нас на активное внедрение такой технологии в нашу игру.

Толчок для старта разработки

Два года назад Google презентовала Dynamic Feature — технологию, позволяющую устанавливать части приложения по требованию прямо во время работы. Благодаря этому пользователи могут изначально скачивать «легкие» билды приложений, а необходимые модули подгружать по мере необходимости — например, языковые пакеты и нативные архитектурно-зависимые библиотеки процессоров, которые выбираются автоматически при установке базовой части приложения.

Загрузка происходит системным сервисом Google Play, а потому не прекращается при остановке приложения. Это выгодно отличает ее от привычного и уже традиционного подхода CDN, когда скачивание происходит через http-клиент приложения. И все это звучало бы хорошо, если бы не «но»: если нужно скачать модуль размером более чем 150 МБ, всплывет системное окно с запросом на разрешение скачивания.

Альтернативы CDN: наша система дозакачки контента напрямую из стора в игру

Как многих из новых пользователей это не испугает? Вот и нам тоже не хотелось проверять, как это может сказаться на метриках первой игровой сессии.

Годом позже Google анонсировала Play Asset Delivery. Это вариант Dynamic Feature, но без кода — только с файлами данных и без пугающего системного диалога перед стартом загрузки. Что-то подобное нам как раз и было нужно. К этому моменту мы уже начали разработку ремастера War Robots, так что необходимость загружать игровые ассеты не при установке игры, а позже, встала особенно остро.

Google предоставляла свой Unity-пакет для поддержки Play Asset Delivery, но у него хватало ограничений, так что пайплайн сборки подружить с тем, что уже было у нас, оказалось тяжело. На тот момент пакет предоставлял хранение только одного бандла на ассет-пак и сам же загружал его в память. Впоследствии появилась возможность запаковывать несколько бандлов из папки и контролировать их загрузку — но появилась она позже, чем нам того требовалось. К тому же, War Robots выходит не только на Android, но и на iOS, так что нужна была какая-то обертка, которая бы абстрагировала игровой код от платформы и стора, выдавая на выходе единое API. Это позволило бы в дальнейшем безболезненно использовать такую систему доставки не только на War Robots, но и на других наших проектах.

На iOS уже была система On-Demand Resources — в других же сторах без поддержки доставки контента можно было скачивать данные только через CDN. Еще мы хотели избежать дополнительных аллокаций и иметь возможность загружать бандлы напрямую без буфера данных, чего Unity обеспечить не могла.

Что у нас в итоге получилось и с какими проблемами мы столкнулись, читайте далее.

Разновидности доставки контента

Рассмотрим типы ассет-пакетов по доставке контента. На Android основные типы — это:

  • install time — установка вместе с основной частью приложения;
  • fast follow — установка сразу после основной части, но отдельным прогрессом с возможностью запуска игры до окончания загрузки;
  • on demand — установка по запросу из кода.

На iOS их по сути тоже три: on demand, initial install и prefetched. Фактически, это та же логика распространения, но просто с другими названиями.

Подытожим это в таблице:

Альтернативы CDN: наша система дозакачки контента напрямую из стора в игру

Для себя мы выбрали более привычные Android-названия и сделали удобное окно в редакторе Unity, в котором можно указать имена пакетов, путь к папке с файлами и тип доставки.

Что касается fast follow — несмотря на то, что такие пакеты ставятся сразу после установки приложения автоматически, перед обращением к их ресурсам надо проверить, что установка прошла успешно. И, возможно, придется показать процесс закачки, если окажется, что данные еще не успели докачаться до конца.

Так выглядит окно настройки пакетов с ассетами (в нашем случае — с бандлами) из редактора Unity:

Альтернативы CDN: наша система дозакачки контента напрямую из стора в игру

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

Краткое описание системы

Прежде, чем перейти к описанию реализации в коде, вкратце пройдемся по тому, как система работает в целом:

Альтернативы CDN: наша система дозакачки контента напрямую из стора в игру

Что мы видим на этой схеме:

  • Small build (APK/IPA) – маленький билд в сторе, размер которого не превышает 150 МБ;
  • Store Pack — набор контента (в нашем случае — пресеты качества графики), которые также выкладываются в стор, но отдельно от основного приложения;
  • Pack — набор бандлов, используемых Unity; обычно они объединены в архив;
  • CDN — удаленный сервер, на котором хранятся дубликаты наших пресетов графики.

Работает это следующим образом: «тонкий» билд игры при помощи Quality Manager распознает, что девайс игрока поддерживает, например, ULD-пресет, после чего обращается к стору и запрашивает нужный пак для скачивания. Если во время загрузки пресета происходит ошибка, запрос перенаправляется к CDN. Или же билд может вообще не запрашивать паки у стора и сразу обращаться к CDN за нужным контентом.

Проблема CDN в том что, он предоставляется различными провайдерами за плату, и если все игроки будут скачивать контент только оттуда, это окажется крайне дорогим решением для студии. Возможность скачивания пака со стора, в свою очередь, предоставляется платформами бесплатно.

Реализация

На Android установка происходит через AssetPackManager из пакета play-core. Пакет включается добавлением зависимости в build.gradle:

dependencies { implementation 'com.google.android.play:core:1.9.1' }

Для Unity мы пишем java-обертку с необходимыми вызовами API. Максимально упрощенно код выглядит так:

AssetPackManager assetPackManager = AssetPackManagerFactory.getInstance(UnityPlayer.currentActivity..getApplicationContext()); assetPackManager.registerListener(myStateUpdateListener); List<String> assetPacks = Collections.singletonList("asset_pack_name"); assetPackManager.fetch(assetPacks).addOnCompleteListener(onCompleteListener);

Подробно с этим API можно ознакомиться на странице документации. Тут важно уточнить, что мы не используем никакую C++ или Unity-обертку, которые там предлагают. Java API позволяет нам отслеживать и прогресс закачки, и результат: все события мы отсылаем простым колл-бэком через Unity JNI.

Результат закачки — местоположение файлов — тоже можно передать строками и обращаться к ним стандартными средствами Unity.

Важный нюанс: путь распаковки файлов зависит от deliveryType пакета:

  • fast-follow и on-demand пакеты распаковываются во внутреннее хранилище Android — нам же возвращается абсолютный путь к этой папке, и файлы оттуда можно читать через стандартный шарповый System.IO либо передавать на вход AssetBundle.LoadFromFile(path), если это бандл Unity;
  • содержимое install time пакета складывается в assets напрямую в APK — а значит, его можно читать через Application.streamingAssetsPath. Этот путь по сути является магической строкой, заставляющей Unity обрабатывать такие пути внутри себя через андроидовский AssetManager. В этом случае бандл можно загрузить из файла путем вызова AssetBundle.LoadFromFile и тем самым избежать необходимости создавать поток данных с рандомным доступом, не поддерживаемый нативными API, а потому требующий выделения дополнительной памяти.

Получившийся код java-плагина:

public string getPackLocation(String packName, String streamingAssetsPath) { AssetPackLocation location = assetPackManager.getPackLocation(packName); if (location == null) // пакет не стоит return null; return location.packStorageMethod() == AssetPackStorageMethod.STORAGE_FILES ? location.assetsPath() : streamingAssetsPath

И код загрузки бандла на Unity:

var bundle = AssetBundle.LoadFromFile(Path.Combine(packLocation, relativePath));

Сейчас уже появился механизм API-configured asset packs в Unity-плагине, который также позволяет получать просто путь к файлу бандла, offset и размер и загружать его через AssetBundle.LoadFromFile. Возможно, с ним бы было проще, но в наше время его еще не было.

На iOS ситуация проще: там поддержка on-demand resources (ODR) уже встроена в редактор Unity — об этом подробнее можно почитать здесь. Но если процесс сборки оттуда получилось адаптировать под наш пайплайн, то с runtime-частью все оказалось не так-то просто. В ней не было отчета о прогрессе загрузки, и все делалось через механизм coroutines — стандартный механизм Unity для асинхронного программирования, действующий до тех пор, пока жив связанный с ним Game Object. Поэтому мы сами написали обертку на ObjectiveC и получили API, аналогичное Android. При скачивании ресурс-пакета сам ресурс можно загрузить стандартными средствами Unity, используя специальный префикс "res://" в пути:

var bundle = AssetBundle.LoadFromFile( "res://" + relativePath);

По сути, это тот же вызов, что и на Android, но при этом все ресурс-паки лежат по пути "res:/". В таком виде мы отдаем пути к ресурс-пакам другой нашей системе — ресурсной, а она независимо от платформы уже хранит у себя информацию, какой префикс к пути добавить в AssetBundle.LoadFromFile, когда потребуется поднять тот или иной бандл в памяти. Так мы разделили экраны загрузки ресурсов из интернета, которые происходят при запуске игры или при смене качества графики, и экраны загрузки сцен, происходящие между боями и при выходе в ангар.

Так как Delivery System у нас не используется непосредственно игрой, а общается с другой системой — ресурсной, мы решили не использовать понятие ассет-пака внутри игры при формировании интерфейсов взаимодействия с системой. Ресурсная система запрашивает только список ассетов, которые ей нужны. Delivery System, в свою очередь, при сборке формирует манифест, в котором указывает, какие ассеты в какие пакеты попали, и по этому манифесту смотрит, каких ассетов не хватает, в каких пакетах они лежат, какие пакеты скачаны, а какие надо скачать, и на основе этого формирует список пакетов для запроса из стора. После скачивания дополнительно проверяются хеши файлов, и результат скачивания с указанием места хранения файлов (префиксы res://, streaminAssetsPath или абсолютные пути) отдается обратно в ресурсную систему. Также существует запрос без непосредственного скачивания для подсчета объема данных, необходимых для скачивания и для хранения на диске, чтобы потом показать нужные диалоги пользователю перед скачиванием.

Проблемы и их решения

После того, как Delivery System пришла к своему конечному виду, мы запустили массовое внешнее тестирование.

И тогда стали всплывать пикантные подробности, которые заставили нас полностью пересмотреть принцип работы Delivery System.

Первые проблемы появились на iOS. При использовании install time пакетов и установки с TestFlight в некоторых случаях ассеты из пакета перестали находиться. Самый точный метод воспроизведения этого бага — при начале установки с TestFlight поставить установку на паузу, нажав на ярлык приложения, потом продолжить, нажав еще раз. Игра довольно быстро финиширует установку и на старте говорит, что ассет-пак уже стоит, но файлы из него не находятся. Так как в App Store нет такого ограничения на размер скачиваемой базовой части приложения, как в Google Play, пользу install time пакеты нам особую не дают, и мы просто решили отказаться от их использования, включая такие ассеты напрямую в билд.

С on demand у нас подобных проблем не было — зато возникли другие. При монтировании нескольких пакетов ассетов и удержании на них ссылки, когда суммарно общее количество обращений на чтение ассетов из них достигало района 1 ГБ, оперативная память начинала резко утекать и довольно быстро приводила к крешу Out of Memory. То есть, в обычной ситуации ее расход незначителен (в районе десятков МБ), а при достижении порога — подскакивает, что видно на графике:

Альтернативы CDN: наша система дозакачки контента напрямую из стора в игру

Достойного решения мы не нашли — Apple вообще рекомендует не использовать пакеты больше 50 МБ. Поэтому мы использовали запасной план: при монтировании пакета стали извлекать все его содержимое в Application.persistentDataPath, размонтировать (отпускать ссылку) и помечать на удаление. Размер пакетов ассетов тоже сделали поменьше — 200 МБ вместо ограничения магазина в 500 МБ. Пакеты больше заданного размера у нас просто делятся на части.

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

Чтобы как-то улучшить ситуацию, мы добавили компрессию складываемых в пакет ассетов. Алгоритм использовали Z-Std: он дает хорошие показатели компрессии и скорости распаковки. Все это можно проставить в настройках редактора, как показано на первом скриншоте.

Время поджимало, и мы в таком виде отправили сборку на внешнее тестирование.

По его итогам оказалось, у пользователей действительно основные проблемы связаны с сетью и нехваткой места. На Android иногда досаждала недоступность магазина или отсутствие поддержки play asset delivery в принципе. Для таких случаев мы застраховали себя переключением на дозакачивание с CDN: для пользователя это внешне никак не заметно, переключение происходит автоматически при возникновении ошибки. Поскольку таких устройств довольно мало, мы все равно выигрываем значительным снижением трафика на CDN.

Также около 20% пользователей не дожидаются окончания загрузки и закрывают игру. Точное число людей, которые не дожидались установки непосредственно через стор билда со всеми качествами весом 2,3 ГБ, мы не знаем и можем только догадываться по косвенным аналитическим данным. Поэтому сейчас, после недавнего релиза Delivery System, мы держим руку на пульсе — и готовы переключиться только на CDN с включением install time пакетов для базового качества.

К слову, install time на Android мы начали использовать практически сразу: в таком случае все ассеты включаются в APK, и это мало отличается от варианта с большим толстым APK, так что поведение при таком сценарии стабильно и не требует модификации игрового кода. Но это актуально только в том случае, если размер APK меньше 1 ГБ. Его увеличение ведет к удорожанию инсталлов и невозможности установки на слабые девайсы. Поэтому релиз HD-качества ремастера War Robots невозможен без использования Delivery System с доставкой контента по запросу хоть в каком-то виде.

Подводя итоги

  • Если магазин бесплатно предлагает сервис по доставке контента — используйте его. Это выгодно как издателю, который таким образом может сэкономить трафик, так и пользователю, потому что данные скачиваются из одного места привычным образом. В целом что Google Play Asset Delivery, что On-Demand Resources ведут себя довольно стабильно и работают, как должны. Однако, если не хочется терять процент игроков, у которых возникают сложности, лучше все же предусмотреть запасной вариант в виде CDN: с ним количество фатальных ошибок, мешающих загрузить игру, оказывается минимальным.
  • Если не нравится Unity-обертка для какого-то API, предоставляемого платформой (или самим Unity) в виду каких-то ограничений или недостатков, не нужно бояться писать свою реализацию — ведь нативные API, написанные на Java, Kotlin, ObjectiveC или Swift, обычно более функциональны и продуманы. Правда, есть исключения: листинга содержимого пакета ODR нет ни в Unity, ни в ObjectiveC, и добавить его никак нельзя, поэтому обходные пути все равно приходится искать.
  • И самое главное: маленький вес приложения в сторе с дозакачкой контента — это хорошо. Пользователи охотнее запускают приложение, и дальше все уже в наших руках: можно показывать видео или интригующие скриншоты, или же давать вводную текстовую информацию на экране загрузки, пока происходит скачивание игровых ассетов. Сам объем данных тоже можно оптимизировать, чтобы как можно быстрее отправить нового игрока в первый бой. Вариантов для развития много, и мы обязательно будем пробовать и экспериментировать в поисках лучшего решения.
Андрей Грузинов
Старший разработчик в Pixonic
3939
22 комментария

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

19
Ответить

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

5
Ответить

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

6
Ответить

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

1
Ответить

Тебе все красиво расписали, поделились опытом. Очень логичный комментарий от тебя.... Свинтус неблагодарный!

Ответить

Спасибо за статью!
Как всегда информативно и полезно.

Наш опыт использования Play Asset Delivery со стандартным плагином - это много крешей и ANR, много жалоб в отзывах о проблемах с загрузкой.

Один раз заплатив за месяц cdn $1000 за 200 ТБ (а это был похоже самый дешевый cdn) интегрировали Play Asset Delivery с возможностью докачки с cdn в случае ошибки.
На iOS засунули всё в билд - благо там ограничение выше чему у Google.

3
Ответить

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

Мы пробовали выкладывать все в билд на iOS, оказалось, что когда размер билда больше 1ГБ, у пользователя резко пропадает желание такую игру скачивать. Особенно, если это новые игроки, которые это делают просто ради интереса, нажав ссылку на рекламе.

Ответить