Реализация системы динамически загружаемого контента (DLC) для мобильной игры в Unity 3D

от автора

Недавно, для одной игры на Unity 3D, которую мы разрабатывали, возникла необходимость добавить DLC систему. Хотя это оказалось далеко не так просто, как казалось в начале, мы успешно справились с возникшими проблемами и игра ушла в gold. В этой статье я хочу изложить наш вариант реализации DLC, рассказать о возникших проблемах и как мы их решили.

Постановка задачи

В игре есть магазин, где игрок покупает вещи за игровую или реальную валюту. В магазине – более 200 вещей. Когда игрок заходит в игру, ему доступно 20 вещей в магазине. Если есть интернет, игра без ведома юзера опрашивает сервер на предмет наличия DLC и, если таковое имеется, скачивает в бэкграунде. Когда игрок повторно зайдет в магазин, он увидит все новые вещи из DLC.
Еще есть набор локаций. Каждая локация имеет набор текстур и .asset файлов. Новые локации также должны добавляться через DLC.
Загрузка ресурсов из DLC должна быть синхронной.
Платформа: iOS (iPhone 3GS и выше.) и Android (Samsung Galaxy S и выше).

Содержимое DLC и работа с ним в игре

В игре вещи полностью определяются файлом itemdata.txt, в котором содержится информация о вещах и их текстурах. Значит, в каждом DLC будет находиться файл itemdata.txt с набором тех вещей, которые есть в DLC + тестуры для этих вещей. А когда магазин запросит базу данных вещей, мы склеим все текстовые файлы со всех DLC и дадим ему этот файл.
Аналогично для локаций есть файл locationdata.txt со списком и характеристиками локаций + текстуры и asset файлы для них.
Соответствующий код на C# для загрузки ресурсов в игровой логике будет выглядеть так:

public String GetItemDataBase() {   if(DLCManager.isDLCLoaded() == true) {     //склеить все файлы itemdata.txt во всех загруженных DLC и вернуть как один string     String itemListStr = DLCManager.GetTextFileFromAllDLCs(“itemdata”);      return itemListStr;   }   else {     //загружаем файл по умолчанию     TextAsset  itemTextFile = Resources.Load(“itemdata”) as TextAsset;     return itemTextFile.text;   }      return String.Empty; } 

Аналогично при запросе текстуры, мы проверяем её наличие в DLC. Если она там есть, загружаем, иначе загружаем из игровых ресурсов. Если и там нет, то загружаем что то дефолтное.

public Texture GetTexture(string txname) {   Texture tx = null;   if(DLCManager.isDLCLoaded() == true) {     tx = DLCManager.GetTextureFromDLC(txname);   }   if(tx == null) {     tx = Resources.Load(txname) as Texture;   }   if(tx == null) {     Assert(tx, “Texture not find: ” + txname);     tx = Resources.Load(kDefaultItemTexturePath) as Texture;   }   return tx; } 

Аналогично для файлов .asset будет функция GetAsset(string assetName). Её реализация будет аналогичной, поэтому пропустим её.

Файл DLC

Мы определились, что у нас должно быть в DLC. Осталось определиться, в виде чего это все хранить.

Первый вариант – хранить DLC в виде зип архива. В каждом архиве – текстовой файл + N текстур. Текстуры должны быть в формате PVRTC для экономии видео памяти. Но тут мы имеем первую проблему – Unity поддерживает загрузку текстур из файловой системы только в формате PNG или JPG [link]. Затем текстуру можно записать в PVRTC текстуру [link]. Это медленный процесс, т.к. требует переконвертации в PVR в риалтайме. К тому же т.к. в DLC планируется хранить файлы типа .asset, а возможно и игровые уровни (.scene), такой метод и вовсе непригоден.

Второй вариант – использовать AssetBundle. Это решение идеально подходит для DLC в играх.
Судя по документации, он обладает массой плюсов:

  • Может хранить любые ресурсы Unity, включая сжатые в нужный формат текстуры (то что нам нужно).
  • Это архив с хорошим сжатием.
  • Просто и удобно использовать.
  • Поддерживает параметр version и хеш сумму (при загрузке функцией LoadFromCacheOrDownload), что удобно для контроля версий DLC

Из минусов только то, что AssetBundle требует Pro версию Unity и не поддерживает шифрование. Решили остановиться на этом решении, т.к. оно очевидно более привлекательно и позволяет решить все наши задачи.

Имплементация (Вариант 1)

Для начала была сделана тестовая версия DLC системы с самым элементарным функционалом.
Сначала все 200 с лишним текстур магазинных итемов и файлы локаций были упакованы в один AssetBundle и залиты на сервер. Файл получился порядка 200 мб. Упаковка в AssetBundle выполнялась скриптом в эдиторе. Как делать упаковку ресурсов в AssetBundle хорошо описано в документации, поэтому не будем на этом останавливаться.

Далее, после запуска игры делаем следующие шаги:

  1. Сначала нужно скачать DLC с сервера. Делаем это согласно коду из мануала Unity. Далее пишем загруженные данные в файл на диск для дальнейшего использования.
    // Start a download of the given URL using assetBandle version and CRC-32 Checksum WWW www = WWW.LoadFromCacheOrDownload (urlToAssetBundle, version, crc32Checksum);  // Wait for download to complete yield return www;  // Get the byte data byte[] byteData = www.bytes;  // Тут можно вставить свой метод дешифровки бандла, если необходимо byteData = MyDescriptionMethod(byteData);  //сохраняем byteData в файл с расширением .unity3d ...  // Frees the memory from the web stream www.Dispose();  //DLC успешно загружено и его можно использовать в игре DLCManager.SetDLCLoaded(true); 

    На этом коде мы c большой вероятностью получим креши по памяти на low девайсах вроде iPhone 3GS, т.к. класс WWW не поддерживает буферизированною загрузку и хранит всю загруженную информацию в памяти. Мы поговорим об этой проблеме чуть позже. Пока запомним этот момент и пойдем дальше.

  2. Загрузка ресурсов из DLC.
    Теперь нам нужно определить функции GetTextureFromDLC(), GetAssetFromDLC() и GetTextFileFromAllDLCs(). Определение последних пока опустим, т.к. оно почти ничем не будет отличаться от первой кроме типа загружаемого ресурса.

    Основная задача функции GetTextureFromDLC – синхронная загрузка текстуры по имени из DLC.
    Попробуем определить её следующим образом.

    public Texture GetTextureFromDLC(String textureName) {    //загружаем DLC с диска. Можем использовать только синхронный метод.   AssetBundle asset = AssetBundle.CreateFromFile(pathToAssetBandle);    //синхронная загрузка текстуры из DLC   Texture  texture = asset.Load(textureName) as Texture;    //выгрузка бандла из памяти без удаления объекта texture   asset.Unload(false);    return texture; } 

Приведенный выше код пока единственный возможный способ загрузить ресурс синхронно из AssetBundle. И как оказалось, тут есть масса нюансов. Разберем их по порядку.

Функция AssetBundle.CreateFromFile согласно документации синхронно загружает ассет с диска. Но есть один нюанс – «Only uncompressed asset bundles are supported by this function.» Таким образом, синхронно загрузить возможно только несжатый AssetBundle. Что существенно увеличит трафик и время загрузки DLC с сервера. К тому же Unity не поддерживает конвертацию AssetBundle из сжатого в несжатый, поэтому не получится скачать сжатый бандл, а потом распаковать его на клиенте.

Читатель может задаться вопросом, почему бы не загрузить AssetBundle асинхронно, например, функцией LoadFromCacheOrDownload, а затем просто брать из него нужные ресурсы синхронно. Ведь логично, что AssetBundle при загрузке из файловой системы должен подгрузить только заголовок файла, а потому в памяти должен заниматься немного.

Однако это оказалось не так. Загруженный AssetBundle хранится в памяти полностью со всем своим содержимым в распакованном виде. Таким образом, чтобы загрузить одну текстуру из 200, Unity загрузит все 200 текстур в память, возьмет одну, а потом освободит память для остальных 199 текстур. Мы это выяснили экспериментально по замерам памяти на девайсе.
Очевидно, что для мобильных устройств это неприемлемо.

Резюме

Приведенный вариант — единственный найденный нами способ реализации синхронной загрузки DLC и ресурсов из него.
Требуется несжатый AsssetBundle, что приводит к большие потерям времени и трафика при загрузке DLC.
Вариант подходит для относительно небольших AssetBaundle-ов, т.к. потребляет очень много оперативной памяти.

Работа над ошибками (Вариант 2)

Попробуем учесть все предыдущие проблемы и найти решения для них.

Проблема с загрузкой больших assetBundle-ов можно решить двумя способами.
Первый – использовать класс WebClient. Однако с ним у нас возникли проблемы на iOS. WebClient ничего не мог скачать, однако на десктопе работал отлично.
Второй вариант – использовать нативные функции ОС. Например, NSURLConnection для iOS и URLConnection для Android соответственно, которые поддерживаю буферизированную загрузку прямо в файл на диске.
Но это не такая уж и большая проблема, т.к. нам в любом случае надо уменьшать размер AssetBaundle для синхронной загрузки. Поэтому пока мы оставили текущий способ загрузки бандлов с сервера.

Намного более серьезная проблема – синхронная загрузка AssetBaundle. Т.к. он должен быть не только несжатым, но и занимать мало места в памяти, мы так или иначе должны разбивать наш один большой файл DLC на много маленьких файлов. Однако, если мы разобьем на слишком маленькие файлы, их будет много и это сильно увеличит время загрузки, т.к. придется для каждого файла устанавливать соединение заново. Значит, нам таки придется хранить их сжатыми для лучшей экономии времени загрузки и трафика.

Для решения этой проблемы было решено использовать свой собственный архиватор. Была выбрана открытая библиотека архиватора для C#, которую без особых усилий получилось завести под Mono в Unity.

Далее алгоритм действий был следующим:

  1. При создании бандла указывалась опция BuildOptions.UncompressedAssetBundle, чтобы получить несжатый бандл.
  2. Затем бандл архивировался и шифровался архиватором и заливался на сервер.
  3. Во время работы приложения создавался отдельный поток, который в бэкграунде выкачивал бандлы, распаковывал их и складывал в специальную папку.

Тут у нас возникла еще одна проблема. Т.к. мы теперь используем сжатый архиватором бандл, мы уже не можем выкачивать его функцией LoadFromCacheOrDownload. А значит, теперь мы должны определить нашу собственную систему контроля версий для DLC.

Для системы контроля версий DLC было выбрано следующее решение. На сервере в папке, где лежали фалы DLC завели текстовой файл dlcversion. Он содержал список DLC в папке и md5 хеши для них. Эти хеши считались на этапе аплода DLC на сервер. На клиенте имелся такой же точно файл, и при старте приложения клиент сравнивал свой файл с файлом на сервере. Если какой-то DLC файл имел отличные хеши или хеша вовсе не было, считалось, что файл на клиенте устарел и клиент подтягивал с сервера новый файл DLC.

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

Описанная система была успешно имплементирована и отлично работает. Единственный минус, который мы имели, это небольшие просадки по fps (лаги) при закачке и распаковке DLC в бэкграунде. А также немного возросли пиковые значения потребления памяти приложения.

Спасибо за внимание. Буду рад ответить на ваши вопросы.

ссылка на оригинал статьи http://habrahabr.ru/post/188824/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *