Підручник із впровадження системи DLC Unity. Огляд проблем і особливостей.

У цій статті я розповім вам, як я застосував систему завантажуваного вмісту (система DLC) в Unity для однієї зі своїх ігор, і огляну деякі проблеми та функції.

Автор: Сергій Тарабан

(російська версія цієї статті)

Постановка проблеми

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

Який вміст має містити DLC і як ним керувати.

Ігрові предмети повністю визначаються у файлі itemdata.txt, який містить інформацію про речі та їх текстури. Отже, в кожному наборі DLC повинен бути файл itemdata.txt з набором тих речей, які є в DLC + блоки текстур для цих речей. І, коли магазин запитає базу даних, об’єднайте всі текстові файли з усіма DLC і дайте йому файл.

Подібним чином файл locationdata.txt містить список характеристик і розташування + текстури та файли ресурсів для них.

Відповідний C# код для завантаження ресурсів в логіці гри:

public String GetItemDataBase() {
    if(DLCManager.isDLCLoaded() == true) {
        //вставити всі файли в усі завантажені DLC і повернути як рядок
        Рядок itemListStr = DLCManager.GetTextFileFromAllDLCs(“itemdata”);
        повернути itemListStr;
    }
    ще {
        //завантажити файл за умовчанням
       TextAsset itemTextFile = Resources.Load(“itemdata”) як 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) як текстура;
    }
    if(tx == null) {
        Assert(tx, «Текстура не знайдена: » + txname);
        tx = Resources.Load(kDefaultItemTexturePath) як текстура;
    }
    повернення tx;
}

 

Так само для файлів .asset  у нас є функція GetAsset(string assetName). Він має таку саму реалізацію, тому ми його пропускаємо.

Файл комплекту DLC.

Тепер ми знали про вміст нашого набору DLC. Залишилося визначити форму зберігання контенту для комплекту DLC.Перший варіант – зберігати DLC як zip-архів. У кожному архіві – текстовий файл + N текстур. Текстури мають бути у форматі PVRTC для економії відеопам’яті (VRAM). Тут ми розібралися з першою проблемою – Unity підтримує завантаження текстур із файлової системи тільки у форматі PNG або JPG  [посилання]. Тоді ви можете записати піксельні дані до текстури PVRTC [посилання]. Це повільний процес, оскільки вам потрібно кодувати піксельні дані у формат PVR у режимі реального часу. У DLC ми плануємо зберігати файли .asset, а в майбутньому це можуть бути файли сцен (.scene). Тому цей спосіб не для нас.

Другий варіант – використання функції AssetBundle. Відповідно до документації AssetBundle має багато корисних функцій:

  • Може містити будь-які дані Unity, включаючи стиснуту текстуру PVRTC (на нашій вулиці).
  • Архів з хорошим стисненням
  • Простий і легкий у використанні
  • Підтримка контролю версій і хеш-суми (під час завантаження LoadFromCacheOrDownload)
Єдиним недоліком є те, що AssetBundle потребує версії Unity Pro та  не підтримує шифрування. Ми вирішили вибрати це рішення, оскільки воно, очевидно, більш привабливе та дозволяє вирішити наші проблеми.

Реалізація.

 

Для початку створю базову робочу версію DLC системи. Усі 200 файлів текстур і рівнів елементів було упаковано в один  AssetBundle і передано на віддалений сервер. Розмір файлу 200 мб. Я запакував AssetBundle за допомогою свого сценарію редактора Unity для генерації набору ресурсів.

Після запуску програми ми зробили наступні кроки:

  1. Спочатку нам потрібно завантажити DLC із віддаленого сервера. Відповідно до посібника Unity запишіть завантажені файли у файл на диску для подальшого використання:
    // Розпочніть завантаження вказаної URL-адреси за допомогою версії assetBundle і контрольної суми CRC-32
    WWW www = WWW.LoadFromCacheOrDownload (urlToAssetBundle, версія, crc32Checksum); 
    

    // Дочекайтеся завершення завантаження yield return www;

    // Отримати байтові дані byte[] byteData = www.bytes;

    // Тут ви можете додати свій спеціальний метод опису byteData = MyDescriptionMethod(byteData);

    //збереження byteData у файлі з розширенням .unity3d …

    // Звільняє пам’ять від веб-потоку www.Dispose();

    //DLC успішно завантажено, і тепер ми можемо використовувати його в грі DLCManager.SetDLCLoaded(true);

    У цьому коді ми, ймовірно, отримаємо збій низької пам’яті в 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;
    
    //вивантаження набору DLC з пам'яті без видалення об'єкта текстури
    asset.Unload(false);
    
    повернути текстуру;
    }

     

Наведений вище код є єдиним можливим способом синхронного завантаження ресурсу з AssetBundle. І тут дуже багато нюансів. Дозвольте мені розглянути їх по порядку.

Функція AssetBundle.CreateFromFile відповідно до  документації завантажує ресурс із файлової системи синхронно. Але «Ця функція підтримує лише нестиснуті пакети ресурсів». Таким чином, синхронно завантажувати нестиснений AssetBundle можна лише. Це значно збільшить трафік і час завантаження DLC з сервера. Крім того, Unity не підтримує перетворення стиснутого AssetBundle у нестиснений, тому неможливо отримати стислий пакет, а потім розпакувати його на стороні клієнта.

Читач може задатися питанням, чому б не завантажити AssetBundle асинхронно, наприклад, за допомогою функції LoadFromCacheOrDownload, а потім просто взяти з нього необхідні ресурси одночасно. Зрештою, логічно, що AssetBundle, завантажений із файлової системи, завантажує лише заголовок файлу, а тому не повинен займати багато пам’яті.

Однак це було не так. Завантажений AssetBundle зберігається в пам’яті з усім вмістом у нестисненому вигляді. Таким чином, щоб завантажити одну текстуру з 200, Unity завантажує всі текстури в пам’ять, надає вам одну, а потім звільняє пам’ять для інших 199 текстур. Це ми з’ясували експериментально, вимірявши пам’ять на пристроях. Для мобільних пристроїв це неприйнятно.

Підсумок

Реалізація нижче є єдиним способом, який ми знайшли для реалізації синхронного завантаження DLC і ресурсів із нього. Він вимагає нестисненого AsssetBundle і, як наслідок, великих втрат часу та пропускної здатності під час завантаження DLC. Варіант підходить для відносно невеликих AssetBundle-ів, оскільки споживає багато пам'яті.

Працюйте над помилками.

Спробуємо врахувати всі попередні проблеми та знайти для них вирішення.

Ми можемо вирішити проблему із завантаженням величезних AssetBundles двома способами: Проблему із завантаженням більших assetBundle-ов можна вирішити двома способами. Перший спосіб – за допомогою класу WebClient. Але у мене проблема з цим в iOS. WebClient не може нічого завантажити в iOS, але на робочому столі він працює належним чином. Другий спосіб – використовувати нативну функцію iOS. Наприклад, NSURLConnection для iOS або URLConnection для Android, які підтримують буферизоване завантаження у файл на диску.

Але це не велика проблема, тому що нам потрібно зменшити час завантаження.

Більш серйозною проблемою є синхронне завантаження AssetBundle. Він повинен бути нестиснутим і займати мало місця в пам’яті. Тому нам потрібно розділити наш величезний пакет активів на багато окремих частин. Однак, якщо розділити на занадто малі файли, їх буде багато, і це значно збільшить час завантаження. Тож ми все ще повинні тримати їх стиснутими для кращої економії часу та споживання трафіку.

Для вирішення цієї проблеми було вирішено використовувати зовнішню стиснуту бібліотеку. Був обраний компресор відкритої бібліотеки 7zip для C#.Наступні наші кроки:

  1. Створіть AssetBundle з параметром  BuildOptions.UncompressedAssetBundle.
  2. Потім пакет було зашифровано та стиснуто активатором і завантажено на віддалений сервер.
  3. Під час роботи з грою я створив окремий потік гри у фоновому режимі. Він завантажує пакети DLC із сервера, розпаковує їх і зберігає в папках файлової системи.
Тут ми маємо ще одну проблему. Оскільки ми використовуємо стислий AssetBundle, ми не можемо використовувати функцію LoadFromCacheOrDownload. Отже, тепер ми повинні визначити нашу власну систему контролю версій для DLC.

На сервері в папці з DLC створили файл dlcversion. Він містить усі імена файлів DLC у цій папці та їх хеш md5. Ми обчислюємо цей хеш під час завантаження файлу на сервер. На стороні клієнта ми маємо той самий файл, і під час запуску клієнт порівнює цей файл із файлом на сервері. Якщо якийсь вузол у версії dlc клієнта відрізнявся від сервера, ми завантажуємо новий DLC із сервера.

Після того, як новий файл DLC було завантажено та розархівовано, його хеш ще раз перевіряється з хешем сервера, і лише після цього ми переписуємо клієнтський файл DLC новим завантаженим DLC із сервера.

Ця система була успішно впроваджена і вона добре працює. Єдиний мінус – невелика просадка fps (лаги) при завантаженні та розпаковуванні DLC у фоновому режимі. А також трохи зросла пікове споживання пам’яті додатком.

Дякую, що прочитали. З радістю відповім на ваші запитання.

Автор: Сергій Тарабан

Інші цікаві статті