Как стать автором
Обновить

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

Время на прочтение7 мин
Количество просмотров39K
Недавно, для одной игры на 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 хорошо описано в документации. Вы также можете использовать мой скрипт для создания AssetBundle.

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

  1. Сначала нужно скачать DLC с сервера. Делаем это согласно коду из мануала Unity. Далее пишем загруженные данные в файл на диск для дальнейшего использования.

    // Start a download of the given URL using assetBundle 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(pathToAssetBundle);
    
      //синхронная загрузка текстуры из 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.
Вариант подходит для относительно небольших AssetBundle-ов, т.к. потребляет очень много оперативной памяти.

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


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

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

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

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

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

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


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

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

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

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

Спасибо за внимание. Буду рад ответить на ваши вопросы.
Теги:
Хабы:
Всего голосов 22: ↑18 и ↓4+14
Комментарии9

Публикации

Истории

Работа

Ближайшие события