- PVSM.RU - https://www.pvsm.ru -
Недавно, для одной игры на Unity 3D, которую мы разрабатывали, возникла необходимость добавить DLC систему [1]. Хотя это оказалось далеко не так просто, как казалось в начале, мы успешно справились с возникшими проблемами и игра ушла в gold. В этой статье я хочу изложить наш вариант реализации DLC, рассказать о возникших проблемах и как мы их решили.
В игре есть магазин, где игрок покупает вещи за игровую или реальную валюту. В магазине – более 200 вещей. Когда игрок заходит в игру, ему доступно 20 вещей в магазине. Если есть интернет, игра без ведома юзера опрашивает сервер на предмет наличия DLC и, если таковое имеется, скачивает в бэкграунде. Когда игрок повторно зайдет в магазин, он увидит все новые вещи из DLC.
Еще есть набор локаций. Каждая локация имеет набор текстур и .asset файлов. Новые локации также должны добавляться через DLC.
Загрузка ресурсов из DLC должна быть синхронной.
Платформа: iOS (iPhone 3GS и выше.) и Android (Samsung Galaxy S и выше).
В игре вещи полностью определяются файлом 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 в виде зип архива. В каждом архиве – текстовой файл + N текстур. Текстуры должны быть в формате PVRTC для экономии видео памяти. Но тут мы имеем первую проблему – Unity поддерживает загрузку текстур из файловой системы только в формате PNG или JPG [link [2]]. Затем текстуру можно записать в PVRTC текстуру [link [3]]. Это медленный процесс, т.к. требует переконвертации в PVR в риалтайме. К тому же т.к. в DLC планируется хранить файлы типа .asset, а возможно и игровые уровни (.scene), такой метод и вовсе непригоден.
Второй вариант – использовать AssetBundle [4]. Это решение идеально подходит для DLC в играх.
Судя по документации, он обладает массой плюсов:
Из минусов только то, что AssetBundle требует Pro версию Unity и не поддерживает шифрование [5]. Решили остановиться на этом решении, т.к. оно очевидно более привлекательно и позволяет решить все наши задачи.
Для начала была сделана тестовая версия DLC системы с самым элементарным функционалом.
Сначала все 200 с лишним текстур магазинных итемов и файлы локаций были упакованы в один AssetBundle и залиты на сервер. Файл получился порядка 200 мб. Упаковка в AssetBundle выполнялась скриптом в эдиторе. Как делать упаковку ресурсов в AssetBundle хорошо описано в документации [6], поэтому не будем на этом останавливаться.
Далее, после запуска игры делаем следующие шаги:
// 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 не поддерживает буферизированною загрузку и хранит всю загруженную информацию в памяти. Мы поговорим об этой проблеме чуть позже. Пока запомним этот момент и пойдем дальше.
Основная задача функции 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
согласно документации [7] синхронно загружает ассет с диска. Но есть один нюанс – «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-ов, т.к. потребляет очень много оперативной памяти.
Попробуем учесть все предыдущие проблемы и найти решения для них.
Проблема с загрузкой больших assetBundle-ов можно решить двумя способами.
Первый – использовать класс WebClient. Однако с ним у нас возникли проблемы на iOS. WebClient ничего не мог скачать, однако на десктопе работал отлично.
Второй вариант – использовать нативные функции ОС. Например, NSURLConnection для iOS и URLConnection для Android соответственно, которые поддерживаю буферизированную загрузку прямо в файл на диске.
Но это не такая уж и большая проблема, т.к. нам в любом случае надо уменьшать размер AssetBaundle для синхронной загрузки. Поэтому пока мы оставили текущий способ загрузки бандлов с сервера.
Намного более серьезная проблема – синхронная загрузка AssetBaundle. Т.к. он должен быть не только несжатым, но и занимать мало места в памяти, мы так или иначе должны разбивать наш один большой файл DLC на много маленьких файлов. Однако, если мы разобьем на слишком маленькие файлы, их будет много и это сильно увеличит время загрузки, т.к. придется для каждого файла устанавливать соединение заново. Значит, нам таки придется хранить их сжатыми для лучшей экономии времени загрузки и трафика.
Для решения этой проблемы было решено использовать свой собственный архиватор. Была выбрана открытая библиотека архиватора для C#, которую без особых усилий получилось завести под Mono в Unity.
Далее алгоритм действий был следующим:
Тут у нас возникла еще одна проблема. Т.к. мы теперь используем сжатый архиватором бандл, мы уже не можем выкачивать его функцией LoadFromCacheOrDownload. А значит, теперь мы должны определить нашу собственную систему контроля версий для DLC.
Для системы контроля версий DLC было выбрано следующее решение. На сервере в папке, где лежали фалы DLC завели текстовой файл dlcversion. Он содержал список DLC в папке и md5 хеши для них. Эти хеши считались на этапе аплода DLC на сервер. На клиенте имелся такой же точно файл, и при старте приложения клиент сравнивал свой файл с файлом на сервере. Если какой-то DLC файл имел отличные хеши или хеша вовсе не было, считалось, что файл на клиенте устарел и клиент подтягивал с сервера новый файл DLC.
После того, как новый файл DLC был скачан и распакован, его хеш еще раз сверялся с серверным, и только после этого устаревший файл заменялся на новый и в файле dlcversion клиента делалась соответствующая запись.
Описанная система была успешно имплементирована и отлично работает. Единственный минус, который мы имели, это небольшие просадки по fps (лаги) при закачке и распаковке DLC в бэкграунде. А также немного возросли пиковые значения потребления памяти приложения.
Спасибо за внимание. Буду рад ответить на ваши вопросы.
Автор: mmortall
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/40125
Ссылки в тексте:
[1] DLC систему: http://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%B3%D1%80%D1%83%D0%B6%D0%B0%D0%B5%D0%BC%D1%8B%D0%B9_%D0%BA%D0%BE%D0%BD%D1%82%D0%B5%D0%BD%D1%82
[2] link: http://docs.unity3d.com/Documentation/ScriptReference/WWW-texture.html
[3] link: http://docs.unity3d.com/Documentation/ScriptReference/WWW.LoadImageIntoTexture.html
[4] AssetBundle: https://docs.unity3d.com/Documentation/ScriptReference/AssetBundle.html
[5] не поддерживает шифрование: http://answers.unity3d.com/questions/8855/how-to-encrypt-asset-bundles.html
[6] документации: http://docs.unity3d.com/Documentation/ScriptReference/BuildPipeline.BuildAssetBundle.html
[7] документации: http://docs.unity3d.com/Documentation/ScriptReference/AssetBundle.CreateFromFile.html
[8] Источник: http://habrahabr.ru/post/188824/
Нажмите здесь для печати.