Привет, Хабр!
В разработке игр зачастую необходимо создавать систему по доставке и установке нового контента (DLC). Для мобильных игр такое требование обусловлено не только желанием обеспечить долгую пост-релизную поддержку игры, но также необходимостью учитывать технические ограничения на размер игры — ключевые площадки по дистрибуции мобильных игр, Google Play и Apple Store, имеют достаточно строгие ограничения на размер установщика приложения.
В рамках этой статьи рассказываю, как мы реализовали такую систему в новой мобильной игре на UE4. Речь пойдет об использовании чанков для создания DLС, работе с .pak файлами, создании патчей для DLC контента, а также об использовании плагина Mobile Patching для загрузки DLC файлов.
Задача
Разделение контента игры на несколько частей — минимально достаточной для установки и запуска приложения (по сути, для создания .apk или .ipa файла) и второй части, с остальными составляющими приложения, де-факто стало базовым для многих мобильных игр. Например, в таких сборках, в качестве минимального необходимого контента для игры, может выступать стартовая карта с виджетами интерфейса и логикой для загрузки остальной части контента.
Данное решение стало настолько распространенным, что в UE4 для него уже предоставляется конфигурация по сборке проекта. Но для нашего проекта такой конфигурации было недостаточно. В первую очередь, нам необходимо было добиться возможности создание новых групп контента для будущих DLC. Во вторую, нас интересовала возможность удалять из клиента игры более неактуальный контент — сюжетные эпизоды, к которым игрок больше не имеет возможности вернуться, более не актуальные игровые эвенты, сезонный контент.
Всё это позволило бы поддерживать относительно небольшой размер игры, что немаловажно, учитывая технические ограничения мобильных устройств.
Функционал чанков (chunk)
Отправной точкой для создания подходящей нам системы по организации DLC стал функционал чанков (chunk’s) и пример его использования в мобильной игре от Epic Games — Battle Breakers.
Использование чанков позволяет объединить ассеты проекта в группы — чанки. В свою очередь, это приводит к созданию отдельных .pak файлов для каждого чанка при сборке проекта. Сам механизм объединения ассетов в чанки основан на использовании Primary Assets и их зависимостей — Secondary Assets. Данное разделение носит концептуальный характер и не влияет на какие-либо свойства файлов, но позволяет применить дополнительные настройки для ассетов, которые в свою очередь и позволяют разнести контент между различными .pak файлами. Грубо говоря, каждый Primary Asset (и его зависимости) и становится в итоге отдельным чанком.
Таким образом, проведя чанкование проекта, вместо монолитного .pak файла со всем контентом игры, мы можем получить набор отдельных .pak файлов — каждый со своей группой ассетов, объединенных по любым критериям. В самом простом случае, каждый чанк может представлять собой отдельную карту со всеми ассетами, которые использует данная карта.В итоге задача по организации DLC контента сводится к:
Выбору Primary Asset-ов;
Настройке параметров Primary Asset-ов;
Настройке параметров Asset Manager.
Для сборки проекта с чанками достаточно включить опцию «Generate Chunks» в настройках сборки проекта: Project Settings → Packaging → Generate Chunk. C чанками можно собрать любой проект, т.к по умолчанию, все ассеты принадлежат нулевому чанку. В результате сборки такого проекта можно будет обнаружить единственный .pak файл с нулевым индексом.
Сам процесс сборки проекта с чанками можно схематично представить следующим образом:
Параметры Primary Asset-ов
Перед тем, как затронуть вопрос о выборе Primary Asset-ов, стоит рассмотреть дополнительные параметры, которые используются Asset Manager-ом для работы с Primary Asset-ами (в контексте использования чанков). К таким параметрам относятся:
Chunk ID — численный идентификатор, указывающий к какому чанку ассет будет принадлежать. По умолчанию, все ассеты относятся к Chunk ID = 0.
Priority — приоритет владения Secondary Asset-ом. Если несколько Primary Asset-ов ссылаются на общий Secondary Asset, то владельцем такого ассета будет считаться Primary Asset с наивысшим приоритетом. Это крайне важный параметр для корректного распределения ассетов по чанкам.
Apply Recursively — ко всем зависимостям (reference) Primary Asset-а будут применены параметры указанные в Chunk ID и Cook Rule, с учетом механизма Priority. Изучить зависимости ассетов можно через инструмент Reference Viewer.
Cook Rule — отдельный параметр, который не связан напрямую с чанками и не связан с конфигурацией сборки (Shipping, Development и т.д). Контекст Cook Rule связан с отдельной настройкой Asset Manager-a «Cook Only Production Assets». Кстати, данное поведение можно изменить с помощью виртуального метода AssetManager::VerifyCanCookPackage
Найти данные настройки можно в настройках Asset Manager-a: Project Settings → Asset Manager → Primary Asset Types To Scan.
Стоит сразу отметить, что все перечисленные параметры настраиваются для каждого типа Primary Asset-а отдельно. Ключевыми параметрами для групп ассетов являются Priority и Apply Recursively.
Apply Recursively
Apply Recursively применяет все правила категории Rules Primary Asset-a ко всем его зависимостям. Предположим, что один из блюпринтов игры, например, гейм мод, отнесен к chunk id = 1. В таком случае, со включенной галкой Apply Recursively все зависимости GameMode также попадут в первый чанк.
С выключенной опцией зависимости окажутся в нулевом чанке (там оказываются все ассеты, которые не отнесены к другим чанкам).
Priority
Теперь добавим в наше уравнение второй параметр — Priority. В случае, если на Secondary Asset ссылаются сразу несколько Primary Asset-ов выбор итогового id чанка для такого ассета будет осуществлен путем сравнения приоритетов Primary Asset-ов. Например, предположим, что на наш ассет GameMode также ссылается другой Primary Asset, оба Primary Asset-a также применяют свои правила для всех Secondary Assets (Apply Recursive = true)
В таком случае, все Secondary Asset-ы окажутся в chunk id = 1, т.к этот чанк имеет более высокий приоритет по сравнению с конкурентом — chunk id = 99.
Иерархия чанков
И снова добавим новую переменную в наше уравнение :) Для разрешения проблем дублирования ассетов между разными чанками в UE4 существует возможность создавать иерархию чанков с помощью настроек в DefaultEngine.ini.
Иерархия чанков позволяет задавать для каждого чанка «старший» (parent) чанк. Если parent чанк уже содержит ассет из child чанка, то он так и останется только в parent чанк и дублирование ассета в child чанк не произойдет. За счет правильной выстроенной иерархии можно избежать дублирования ассетов между разными чанками. Формат настройки иерархии выглядит следующим образом:
DependencyArray = (ChunkID=<ChildChunkId>, ParentChunkID=<ParentChunkID>)
Пример настройки иерархии для нескольких чанков:
[/Script/UnrealEd.ChunkDependencyInfo]
DependencyArray=(ChunkID=1,ParentChunkID=0)
DependencyArray=(ChunkID=2,ParentChunkID=1)
При такой иерархии чанков мы избежим дублирования ассетов из нулевого чанка, а во второй чанк не попадут дубли ассетов из первого и нулевого.
Обход правил Apply Recursively и Priority
В некоторых случаях можно столкнуться с проблемой, когда требуется исключить какой-либо ассет, либо его зависимости из процесса чанкования, либо проигнорировать применение правил “Apply Recursively” для такого чанка. Например, такая проблема может возникнуть при отделении тестового контента, либо контента для реализации системы читов от контента основной игры, при этом читы могут ссылаться на разные игровые ассеты. Здесь на выручку может прийти реализация метода UAssetManager::ShouldSetManager. Данный метод вызывается Asset Manager-ом при применении правил Primary Asset-ов и позволяет проигнорировать применение правил для зависимостей ассета или наоборот, заставить их применить.Легче всего объяснить суть метода на примере из нашего проекта, где зависимости ряда ассетов исключаются из применения правил за счет следующей реализации метода:
EAssetSetManagerResult::Type UMHAssetManager::ShouldSetManager(const FAssetIdentifier& Manager, const FAssetIdentifier& Source, const FAssetIdentifier& Target, UE::AssetRegistry::EDependencyCategory Category, UE::AssetRegistry::EDependencyProperty Properties, EAssetSetManagerFlags::Type Flags) const
{
EAssetSetManagerResult::Type result = Super::ShouldSetManager(Manager, Source, Target, Category, Properties, Flags);
if (result == EAssetSetManagerResult::DoNotSet)
{
return result;
}
UMHAssetManagerSettings* settings = UMHAssetManagerSettings::StaticClass()->GetDefaultObject<UMHAssetManagerSettings>();
bool bHasExclude = settings->ExcludeRecurse.ContainsByPredicate([Target](const auto& lhs) {
return lhs.GetLongPackageName() == (Target.PackageName.ToString());
});
if (bHasExclude)
{
return EAssetSetManagerResult::SetButDoNotRecurse;
}
return result;
}
Сам метод дополняет изначальную логику проверкой содержится ли Target ассет в списке исключений и, если да, то возвращает для такого ассета результат EAssetSetManagerResult::SetButDoNotRecurse, который исключает зависимости данного ассета от применения текущих правил. В зависимости от ситуации можно возвращать иной тип результата EAssetSetManagerResult, например, наоборот, SetAndRecurse — что заставит Asset Manager применить правила для всех зависимостей Target ассета.
Выбор Primary Asset-ов
Primary Asset-ом считается любой ассет, который обладает валидным Primary Asset Id. В UE4 существует несколько способов создать такой Primary Asset Id для ассета:
реализовать метод GetPrimaryAssetId в родительском классе ассета;
использовать готовые классы с реализацией GetPrimaryAssetId — UPrimaryAssetLabel, UPrimaryDataAsset;
использовать настройки Asset Manager для выбора Primary Assets (вместе с опцией «Should Manager Determine Type and Name», которая «автоматизирует» создание Primary Asset Id для выбранных ассетов)
Хочу сразу отметить, что какой бы вы путь не выбрали, в настройках Asset Manager потребуется указать путь к Primary Asset:
Универсального подхода по выбору и методу создания Primary Asset-ов нет, т.к каждой игре потребуется свой подход, тем не менее, одним из крайне удобных вариантов оказалось использование ассета UPrimaryAssetLabel.UPrimaryAssetLabel представляет собой Data Asset, который позволяет выбрать ассеты и за счет создания такой зависимости, выбранные ассеты становятся «частью» данного Primary Asset-а.
Использование UPrimaryAssetLabel позволяет хранить все настройки чанков в одном ассете. Например, в одном из примеров от Epic Games, Shooter Game Example, реализовано чанкование проекта по картам. В таком случае, каждый UPrimaryAssetLabel (чанк) содержит свою карту.
Кроме того, использование UPrimaryAssetLabel позволяет:
Удобно менять содержимое чанка — зачастую есть необходимость скорректировать содержимое чанка добавив определенный ассет в него «вручную».
Опция Label Assets In My Directory — создает зависимость (reference) на все ассеты в папке с UPrimaryAssetLable. Удобная опция, когда часть ваших ассетов (например, контент для тестирования игры) сгруппирован по определенному пути, но при этом все ассеты имеют разный тип. Тогда, включив Label Assets In My Directory и задав определенный chunk id для таких ассетов, можно избежать необходимости маркировать каждый ассет в ручную в Asset Manager.
Во многом использование UPrimaryAssetLabel дублирует функционал настроек Asset Manager-а, но использование отдельного ассета для организации чанков позволяет легче контролировать их содержимое.
Проверка содержимого чанков
Настроив все Primary Assets возникает вопрос о том, как проверить итоговое содержимое чанков. Для такой проверки UE4 предоставляет удобный инструмент — Asset Audit : Window → Developer Tools → Asset Audit.
Asset Audit
Предварительно для корректной работы инструмента стоит собрать проект с чанками. Для этого требуется:
Включить Generate Chunks в настройках Packaging: Project Settings → Packaging → Generate Chunks;
Собственно, собрать проект.
Меню Asset Audit предлагает доступ к обзору созданных чанков и их содержимому и позволяет узнать более конкретную информацию о разделение ассетов.
Добавление чанков в меню осуществляется с помощью кнопки Add Chunks.
Для просмотра содержимого чанка можно воспользоваться несколькими меню: Size View или Reference View через ПКМ:
Кроме этого, в меню доступны несколько полезных опций:
Selected Platform — позволяет выбрать для какой платформы отображать данные.
Для non-Editor платформ корректные данные отображаются только после сборки проекта с чанками.
Add Asset Class — позволяет выбрать любой тип ассета и получить информацию о том, в каком чанке он находится (столбец Chunks):
Add Primary Asset Type — позволяет выбрать один из типов Primary Asset, полезная опция для проверки корректности настроек Primary Asset-ов в Asset Manager
Настроив распределение контента по чанкам и получив несколько .pak файлов возникает вопрос о том, какие операции доступны с .pak файлами и каким образом их можно доставить до клиента?
Pak файл
Стандартными этапами сборки любой игры на UE4 являются:
сборка кода игры под целевую платформу;
сборка (cooking) используемых игрой ассетов под целевую платформу и их «упаковывание» в .pak файл — архив с ассетами;
создание контейнера (установщика) приложения (например, .apk файла для Android платформы), который будет содержать результаты сборки проекта с предыдущих шагов.
Как оказалось, само взаимодействие с .pak файлами сводится к нескольким вещам: их монтированию (установке) и демонтированию.
Операция по монтированию .pak файла позволяла использовать весь контент (ассеты) из .pak файла без каких-либо дополнительных вмешательств.
Для работы с .pak файлами используется движковый класс FPakPlatformFile, который в свою очередь является реализацией интерфейса IPlatformFile. Получить объект класса можно через файловый менеджер - “FPlatformFileManager”:
FPakPlatformFile* PakPlatformFile = FPlatformFileManager::Get().FindPlatformFile(FPakPlatformFile::GetTypeName());
Существующая реализация предоставляла для нас все необходимые методы:
bool FPakPlatformFile::Mount(const TChar* Filename, uint32 PakOrder, const TChar* InPath, bool bLoadIndex);
— для монтирования пак файла;
В качестве входных параметров достаточно передавать только два — Filename, указывающий полный путь до пак файла и PakOrder, который применяется для сортировки пак файлов. В наших условиях параметр всегда равен единице, т.к порядок монтирования пак файлов для нас не имеет значения.
bool FPakPlatformFile::Unmount(const TChar* InPakFilename)
— для демонтирования;
void GetMountedPaks(TArray<FString>& OutPakfiles)
— для получения списка установленных .pak файлов.
Кроме того, для монтирования пак файла можно просто воспользоваться глобальным делегатом FFCoreDelegates::MountPak, а для демонтирования его аналогом — FCoreDelegates::OnUnmountPak. Хотя само по себе использование глобальных делегатов для таких операций выглядит сомнительно.
Быстрая проверка API FPakPlatformFile показала нам, что добавление (Mount) новых .pak файлов не вызывает никаких проблем, а также, что мы сможем удалять (Unmount) новые .pak файлы из клиента не вызывая проблем с игрой.
Таким образом мы получили ответ на один из наших вопросов: как именно можно добавлять и удалять дополнительные .pak файлы.
Mobile Patching
Для загрузки новых .pak файлов можно написать свой функционал, который будет просто скачивать файлы с сетевого хранилища.Но UE4 предоставляет несколько решений, ориентированных на работу с загрузкой чанков.Одно из них реализовано плагином Mobile Patching. Не смотря на такое название, плагин отлично справляется с работой и на desktop платформах.
Плагин использует встроенный в UE4 функционал по созданию .pak файлов чанков в оптимизированном формате для загрузки через интернет.Такой функционал включается в настройках Packaging -> Build Http Chunk Install Data.
Это приводит к созданию нескольких дополнительных файлов:
манифестов с описанием мета-данных о .pak файле каждого чанков;
множества небольших по размеру «кусков» .pak файлов; Их объединение в .pak файл осуществляется функционалом MobilePatching.
Кроме того, такой формат загрузки и сборки .pak файла гораздо лучше подходит для загрузки через интернет - загрузка отдельных небольших файлов позволяет реализовать “дозагрузку” недостающих файлов при обрыве соединения;
Mobile Patching реализует следующий функционал:
Загрузку .pak файлов;
Патчинг .pak файлов;
Проверку на целостность .pak файлов.
Загрузка .pak файлов осуществляется с помощью метода:
UMobilePendingContent* UMobilePatching::RequestContent(const FString& RemoteManifestURL, const FString& CloudURL, const FString& InstallDirectory, const FString& PatchStagingFolder, FOnRequestContentSucceeded OnSucceeded, FOnRequestContentFailed OnFailed);
В качестве входных параметров передается URL манифеста нужного .pak файла и URL CloudDir — это папка, сформированная в ходе сборки проекта с включенными опциями Build Http Chunk Install Data.
Параметр InstallDirectory — путь по которому будет скачан требуемый .pak файл.
Возвращаемый объект UMobilePendingContent можно использовать для получения прогресса загрузки .pak файла.
В целом, метода UMobilePatching::RequestContent достаточно для загрузки новых .pak файлов, а их установку можно произвести с помощью функционала, который я описывал ранее в статье. Также, метод UMobilePatching::RequestContent можно безболезненно вызывать, даже если вы уже знаете, что скачали и установили требуемый .pak файл. Повторный вызов UMobilePatching::RequestContent не начнет загрузку заново, но проведет проверку .pak файла на целостность и необходимость его обновления (если RemoteManifestURL содержит новый манифест).
Кстати, сам плагин так же позволяет производить загрузку .pak файлов прямо из игровой сессии в редакторе, что значительно облегчает тестирование функционала. Единственной проблемой может выступать отсутствие функционала по работе с .pak файлами в редакторе — весь контент нам доступен изначально.
Создание патчей
Для создания новой версии чанков (например, если вы произвели какие-то изменения в них, добавили новый контент), достаточно изменить опцию Http Chunk Install Data Version — вся остальная логика по загрузке патча и обновлению .pak файла на клиенте уже реализована в методе UMobilePatching::RequestContent.
Дополнительные настройки
Плагин содержит ряд дополнительных настроек, который в основном связан с решением проблем загрузки новых чанков через интернет в связи с низкой скоростью загрузки. Это наиболее актуально для мобильных платформ, т.к качество соединение может быть достаточно слабым. Все настройки относятся к секции [Portal.BuildPatch] Engine.ini конфига и начать их изучение можно с метода —FBuildPatchInstaller::BuildConnectionCountConfig.
Для наших условий самыми важными оказались следующее настройки:
ChunkDownloadsLowerLimit — позволяет снизить количество параллельных загрузок;
DisconnectedDelay — влияет на время выставления флага о «провале» загрузки чанка. Для мобильных платформ имеет смысл выставлять относительно большое значение.
Заключение
Подводя итоги, хочу отметить, что опасения о «сырости» функционала по чанкованию проекта, к счастью, не оправдались.
UE4 предоставляет весь набор инструментов, которые позволяют получить любой удобный для работы набор .pak файлов, а также готовые подходы по доставке таких .pak файлов до клиента. Отмечу, что сам по себе функционал чанкования становится одним из необходимых этапов разработки для мобильных платформ в связи с появлением нового формата приложений для Android платформ — Android Application Bundle. Вдаваться в детали организации этого формата я уже не буду, но его «модульное» устройство, в терминах UE4 основывается как раз на чанках.
ufna
Спасибо за статью! Очень здорово что вы детально расписываете эти вещи, еще очень крутой была бы вторая часть про связь чанков и запихивания всего в AAB "по запросу", а не в install-time :)
Правда кажется не верным использовать термин DLC в рамках этих мобильных "патчей". В терминах движка это все-таки не DLC, со всем вытекающим - "настоящих" DLC на мобилах как таковых нет.
Mernion Автор
Привет!
Под DLC в статье подразумевал просто загружаемый (скачиваемый) контент игрой - пак файл. В самом пак файле может быть что угодно - и полноценный аддон, небольшой контент пак или просто патч (обновление пак файла).Для мобильных игр, конечно, в самом простом варианте придется использовать чанки для публикации в AAB формате, хотя бы как замену старого разделения на apk + .obb.
Вообще, про чанки и работу с ними можно долго чего рассказывать - и про поиск наиболее оптимального способа весь контент по ним разнести, и о том как доставить их до игрока (использовать сторонний сервис или новое API для работы с AAB, как раз про то, как их не только install time делать) Тут пока более привлекательно выглядит вариант со сторонним сервисом, использование условного сетевого хранилища для чанков, позволяет одинаковое API использовать на разных платформах - IOS, Android и даже прямо из редактора проверять скачивание пак файлов, что сильно ускоряет проверку и разработку всего связанного с чанками.
ufna
Вот как раз это и не так для мобилок. Внутри пак файлов не может быть исполняемого кода, что сильно лимитирует возможности такого "патча" - это именно загружаемый контент, но не аддон/модуль.