Использование AssetBundles для загрузки обновлений

Общие вопросы о Unity3D

Использование AssetBundles для загрузки обновлений

Сообщение Inessa 29 окт 2016, 20:49

Мне необходимо найти корректный подход, каким образом можно использовать AssetBundles для загрузки обновлений для своей игры.

Игра у меня по современным меркам не очень большая (60 Мб).

Я предполагаю "засунуть" в AssetBundle всю свою игру.

Игра состоит из одной одной рабочей сцены, в которой каждый раз при старте очередного мини-гейма динамически генерируется уровень с новой игровой комнатой.

Сейчас я опробовала стандартный подход, когда после первоночальной загрузки игры на устройство на экране появляется сцена-заставка с логотипом игры.

Пока заставка "висит" на экране, я загружаю AssetBundle c основной сценой при помощи простого кода (см. ниже).

Синтаксис:
Используется csharp
using UnityEngine;
using System.Collections;

public class MainScene : MonoBehaviour
{
    public string BundleName = "gameloadscene";
    public string SceneName = "LoadScene";
    public string ManifestName = "Data";

    AssetBundleManifest manifest;
    string assetBundlePath = "https://mywebhostingsite.com/assetbundledata/";

    IEnumerator Start()
    {
        // Загрузка данных из PlayerPrefs
        Hash128 AssetBundleHashFromCache = Hash128.Parse(PlayerPrefs.GetString("AssetBundleHash", string.Empty));
        // Установка флага проверки доступности на возможность осуществления загрузки
        bool IsAssetBundleReachableToDownload = false;
        // Удаленное чтение значения манифеста для используемого AssetBundle
        using (WWW www = new WWW(assetBundlePath + ManifestName))
        {
            yield return www;
            if (!string.IsNullOrEmpty(www.error))
            {
                Debug.Log(www.error);
                // Загрузка невозможна (нет удаленного соединения)
                IsAssetBundleReachableToDownload = false;
                yield break;
            }
            // Успешная загрузка значения манифеста для используемого AssetBundle
            manifest = www.assetBundle.LoadAsset("AssetBundleManifest") as AssetBundleManifest;
            // Загрузка по удаленному соединению возможна
            IsAssetBundleReachableToDownload = true;
            yield return null;
            www.assetBundle.Unload(false);
        } // using()
        // Значение хеш-суммы из используемого AssetBundle
        Hash128 AssetBundleHashFromAssetBundle;
        // Установка флага необходимости загрузки бандла по удаленному соединению
        bool IsAssetBundleDownloadNeed = true;
        // Проверка доступности удаленного соединения
        if (IsAssetBundleReachableToDownload)
        {
            // Получение значения хеш-суммы для удаленного AssetBundle
            AssetBundleHashFromAssetBundle = manifest.GetAssetBundleHash(BundleName);
            // Проверка совпадения хеш-сумм для бандла из хеша и из удаленного хостинга
            if (AssetBundleHashFromCache.ToString() == AssetBundleHashFromAssetBundle.ToString())
            {
                IsAssetBundleDownloadNeed = false; // Загрузка удаленного бандла не нужна
            }
            else
            {
                IsAssetBundleDownloadNeed = true; // Загрузка удаленного бандла нужна
            }
        }
        else
        {
            // Сообщение в случае невозможности "достучаться" по сети до удаленного бандла
            Debug.Log("IsAssetBundleReachableToDownload: " + IsAssetBundleReachableToDownload.ToString());
        }
        // Проверка на необходимость загрузки с удаленного хостинга при наличии возможности "достучаться" до удаленного хостинга
        if (IsAssetBundleDownloadNeed && IsAssetBundleReachableToDownload)
        {
            // Получение значения хеш-суммы для удаленного AssetBundle
            AssetBundleHashFromAssetBundle = manifest.GetAssetBundleHash(BundleName);
            // Загрузка AssetBundle из удаленного хостинга
            using (WWW www = WWW.LoadFromCacheOrDownload(assetBundlePath + BundleName, AssetBundleHashFromAssetBundle))
            {
                yield return www;
                if (!string.IsNullOrEmpty(www.error))
                {
                    Debug.Log(www.error);
                    yield break;
                }
                // Загрузка сцены из полученного AssetBundle
                Application.LoadLevel(SceneName);
                yield return null;
                www.assetBundle.Unload(false);
            } // using()
            // Сохранение хеш-суммы из AssetBundle
            PlayerPrefs.SetString("AssetBundleHash", AssetBundleHashFromAssetBundle.ToString());
            PlayerPrefs.Save();
        }
        else
        {
            if (!IsAssetBundleDownloadNeed)
            {
                // Загрузка AssetBundle из локального кеша
                using (WWW www = WWW.LoadFromCacheOrDownload(assetBundlePath + BundleName, AssetBundleHashFromCache))
                {
                    yield return www;
                    if (!string.IsNullOrEmpty(www.error))
                    {
                        Debug.Log(www.error);
                        yield break;
                    }
                    // Загрузка сцены из полученного AssetBundle
                    Application.LoadLevel(SceneName);
                    yield return null;
                    www.assetBundle.Unload(false);
                } // using()
            }
        }
    }// Start()
}
 


Собираю бандл под Android, заливаю папку c бандлом на хостинг.

Все работает хорошо - бандл с основной сценой исправно подгружается, пока на экране висит сцена с логотипом игры.

В коде автоматически проверяется хеш-сумма бандла, и новый бандл подгружается в случае необходимости.

При тестировании работы с этим бандлом я заметила, что бандл подгружается каждый раз заново, несмотря на то, что он уже был загружен в предыдущий раз при старте игры.

Я использую метод LoadFromCacheOrDownload(), сохраняю в PlayerPrefs значение хеш-суммы загруженного бандла.

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

Если хеш-суммы не совпадают, провожу повторную загрузку бандла с хостинга на удаленном сервере.

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

Но этого почему-то не происходит - бандл из кеша c устройства не подгружается.

Я проверила это очень просто - отключив WiFi-сеть на мобильном устройстве.

В протоколе Logcat выводится сообщение "404: Not found".

Основная сцена при этом из кеша почему-то не подгружается, хотя обработка соответствующей ситуации реализована в программном коде.

Сцена с заставкой в запущенном приложении продолжает висеть на экране, ожидая подгрузки бандла из сети.

Скажите, почему не происходит загрузка бандла из кеша в случае использования моего кода?

Каким образом нужно модифицировать программный код, чтобы в случае совпадения хэша бандлов на сервере и на устройстве, сцена все-таки подгружалась из кеша c устройства?

Почему при отсутствии соединения с сервером при запуске игры, который я имитирую удалением папки с бандлом на сервере, не подгружается уже имеющийся бандл со сценой из кеша с устройства?

Опции isCacheEnabled = true,
Application.persistentDataPath = "/data/data/com.comp.assetbundle/files",
Application.temporaryCachePath = "/data/data/com.comp.assetbundle/cache".

Странно, но в этих папках нет ничего похожего на содержимое моей папки https://mywebhostingsite.com/assetbundledata/, которая содержит 4 файла со сгенерированном бандлом:
- Data
- Data.manifest
- gameloadscene
- gameloadscene.manifest

На мобильном устройстве нашла какие-то файлы в папке data/data/com.comp.assetbundle/files/UnityCache/Shared/03eb33c2e54214e09a3458ed89b17bea150e468c6
- __info
- __lock
- BuildPlayer-LoadScene
- BuildPlayer-LoadScene.sharedAssets

LoadScene - это как раз сцена, которую я подгружаю из бандла в проект, но в в Unity этот бандл со сценой у меня называется gameloadscene.

Где вообще должны храниться на Android-устройствах подгружаемые бандлы?

Сохраняют ли они свое изначальное имя или меняют его на BuildPlayer-LoadScene.sharedAssets?

В файл с настройками "/data/data/com.comp.assetbundle/shared_prefs/com.comp.assetbundle.xml" значения хеша из PlayerPrefs после удачного первоначального запуска приложения на устройстве так почему-то и не записались.

Помогите, пожалуйста, разобраться в чем тут загвоздка!

Спасибо за помощь в решении это задачи.
Inessa
UNITрон
 
Сообщения: 160
Зарегистрирован: 13 мар 2013, 11:56

Re: Использование AssetBundles для загрузки обновлений

Сообщение jetyb 31 окт 2016, 08:26

Мне необходимо найти корректный подход, каким образом можно использовать AssetBundles для загрузки обновлений для своей игры.

Зачем для этого использовать AssetBundles? Чтобы хранить и сверять версию, достаточно хранить и сверять одно значение (типа version = "1.02.560"), хранящееся в настройках и на сервере. Зачем для этого приплетать AssetBundles и хэш??
Для обновления хватит простой замены файлов.

Почему при отсутствии соединения с сервером при запуске игры, который я имитирую удалением папки с бандлом на сервере, не подгружается уже имеющийся бандл со сценой из кеша с устройства?

Опции isCacheEnabled = true,
Application.persistentDataPath = "/data/data/com.comp.assetbundle/files",
Application.temporaryCachePath = "/data/data/com.comp.assetbundle/cache".

Лучше вывести не относительный путь а полный путь к файлу. Сразу станет яснее какой там папки нет.
А еще лучше самому заботиться о местоположении этой папки.

В протоколе Logcat выводится сообщение "404: Not found".

Сетевая ошибка. Что-то не так с адресом или с настройкой сети.
jetyb
Адепт
 
Сообщения: 1486
Зарегистрирован: 31 окт 2011, 17:21

Re: Использование AssetBundles для загрузки обновлений

Сообщение Inessa 31 окт 2016, 20:41

AssetBundles используют все нормальные люди для загрузки обновлений и DLC для свих игр.

Актуальную версию бандла можно проверять на основании совпадения/несовпадения хеша или версии бандла.

Если проверять не хеш, а версию бандла, то как определить какая версия бандла у меня сейчас собирается?

После построения очередной версии AssetBundle у меня в соответствующей папке .\Data\ появляются 4 файла для загружаемой мной сцены, которую я полностью "запихнула" в бандл:
- Data
- Data.manifest
- gameloadscene
- gameloadscene.manifest

Data и gameloadscene - бинарные файлы, Data.manifest и gameloadscene.manifest - текстовые файлы.

Использую перегруженную версию метода:

public static WWW LoadFromCacheOrDownload(string url, int version);

Вот скрипт, который подгружает сцену из очередной версии бандла в проект.

Синтаксис:
Используется csharp
using UnityEngine;
using System.Collections;

public class MainScene : MonoBehaviour
{
    public string BundleName = "gameloadscene";
    public string SceneNameToLoad = "LoadScene";
    public string ManifestName = "Data";

    string assetBundlePath = "file::/E:/Users/Unity3D/AssetBundleVersion/Data/";

    // Use this for initialization
    IEnumerator Start()
    {
        using (WWW www = WWW.LoadFromCacheOrDownload(assetBundlePath + BundleName, 1))
        {
            yield return www;
            if (!string.IsNullOrEmpty(www.error))
            {
                Debug.Log(www.error);
                yield break;
            }
            Application.LoadLevel(SceneNameToLoad);
            yield return null;
            www.assetBundle.Unload(true);
        } // using()
    }
}
 


При первоначальной сборке первая версия бандла = 0, при повторной сборке бандла она становится равной 1 и инкрементируется дальше при последующих сборках.

Прикол в том, что во время экспериментов с многократными построениями бандлов можно быстро сбиться со счета версий.

Где именно указывается номер версии бандла в этих файлах я так и не смогла найти.

В текстовых файлах номер версии, по всей видимости, не задается, т.к. при повторной сборке бандлов для одной и той же сцены их содержимое (размер файла) не меняется.

Однако меняется размер бинарного файла Data на 1 байт.

Но это файл-то бинарный - и по его содержимому, открытому с помощью Notepad, не поймешь, где задается меняющийся номер версии бандла.

Поэтому мой вопрос заключается в следующем.

Каким образом можно вручную или программным способом узнать текущую версию собранного бандла?
Inessa
UNITрон
 
Сообщения: 160
Зарегистрирован: 13 мар 2013, 11:56

Re: Использование AssetBundles для загрузки обновлений

Сообщение DbIMok 31 окт 2016, 22:13

вот вялотекущая тема с незапамятных времен
наверное, проще было за это время написать свою систему (как вам и советуют выше), чем пользоваться этим черным ящиком
вот так версия участвует в процессе (m_u32_3):
Синтаксис:
Используется csharp
    public static WWW LoadFromCacheOrDownload(string url, int version, [DefaultValue("0")] uint crc)
    {
      Hash128 hash = new Hash128(0U, 0U, 0U, (uint) version);
      return WWW.LoadFromCacheOrDownload(url, hash, crc);
    }

  /// <summary>
  ///
  /// <para>
  /// Represent the hash value.
  /// </para>
  ///
  /// </summary>
  public struct Hash128
  {
    private uint m_u32_0;
    private uint m_u32_1;
    private uint m_u32_2;
    private uint m_u32_3;

    /// <summary>
    ///
    /// <para>
    /// Get if the hash value is valid or not. (Read Only)
    /// </para>
    ///
    /// </summary>
    public bool isValid
    {
      get
      {
        if ((int) this.m_u32_0 == 0 && (int) this.m_u32_1 == 0 && (int) this.m_u32_2 == 0)
          return (int) this.m_u32_3 != 0;
        return true;
      }
    }

    /// <summary>
    ///
    /// <para>
    /// Construct the Hash128.
    /// </para>
    ///
    /// </summary>
    /// <param name="u32_0"/><param name="u32_1"/><param name="u32_2"/><param name="u32_3"/>
    public Hash128(uint u32_0, uint u32_1, uint u32_2, uint u32_3)
    {
      this.m_u32_0 = u32_0;
      this.m_u32_1 = u32_1;
      this.m_u32_2 = u32_2;
      this.m_u32_3 = u32_3;
    }

    public static bool operator ==(Hash128 hash1, Hash128 hash2)
    {
      if ((int) hash1.m_u32_0 == (int) hash2.m_u32_0 && (int) hash1.m_u32_1 == (int) hash2.m_u32_1 && (int) hash1.m_u32_2 == (int) hash2.m_u32_2)
        return (int) hash1.m_u32_3 == (int) hash2.m_u32_3;
      return false;
    }

    public static bool operator !=(Hash128 hash1, Hash128 hash2)
    {
      return !(hash1 == hash2);
    }

    /// <summary>
    ///
    /// <para>
    /// Convert Hash128 to string.
    /// </para>
    ///
    /// </summary>
    public override string ToString()
    {
      return Hash128.Internal_Hash128ToString(this.m_u32_0, this.m_u32_1, this.m_u32_2, this.m_u32_3);
    }

    /// <summary>
    ///
    /// <para>
    /// Convert the input string to Hash128.
    /// </para>
    ///
    /// </summary>
    /// <param name="hashString"/>
    public static Hash128 Parse(string hashString)
    {
      Hash128 hash128;
      Hash128.INTERNAL_CALL_Parse(hashString, out hash128);
      return hash128;
    }

    [WrapperlessIcall]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void INTERNAL_CALL_Parse(string hashString, out Hash128 value);

    [WrapperlessIcall]
    [MethodImpl(MethodImplOptions.InternalCall)]
    internal static extern string Internal_Hash128ToString(uint d0, uint d1, uint d2, uint d3);

    public override bool Equals(object obj)
    {
      if (obj is Hash128)
        return this == (Hash128) obj;
      return false;
    }

    public override int GetHashCode()
    {
      return this.m_u32_0.GetHashCode() ^ this.m_u32_1.GetHashCode() ^ this.m_u32_2.GetHashCode() ^ this.m_u32_3.GetHashCode();
    }
  }
}
 

вуду магия!
ну а hash и crc можно получить через остатки этого класса
правильный вопрос - половина ответа. учитесь формулировать вопросы понятно.
Новости > _Telegram чат @unity3d_ru (11.6k/4.8k online) > _Telegram канал @unity_news (4.8k подписчиков) > Телеграм тема > "Спасибо"
Аватара пользователя
DbIMok
Адепт
 
Сообщения: 6372
Зарегистрирован: 31 июл 2009, 14:05

Re: Использование AssetBundles для загрузки обновлений

Сообщение jetyb 01 ноя 2016, 08:10

AssetBundles используют все нормальные люди для загрузки обновлений и DLC для свих игр.

Ну очевидно у этих нормальных людей с обновлениями через AssetBundles ничего не получается.
Кроме того, не все обновления ограничиваются заменой бандлов: если поменяется что-то из папки основного билда (скрипты например), как обновлять игру будете?
Самообновление файлов уже запущенной программы - это что-то за гранью.

Обновление делается отдельной программой по принципу системы контроля версий.
Каждый билд содержит информацию о его версии. Есть какая-то версия игры лежащая на сервере. С машины разработчика вы можете менять ее.
С машины пользователя запускается программа автообновления, меняющая все данные игры на данные соответствующие версии на сервере.
Ни о каких AssetBundles программа автообновления не знает и способна работать с чем угодно.
jetyb
Адепт
 
Сообщения: 1486
Зарегистрирован: 31 окт 2011, 17:21

Re: Использование AssetBundles для загрузки обновлений

Сообщение Inessa 01 ноя 2016, 23:09

jetyb писал(а):
Обновление делается отдельной программой по принципу системы контроля версий.
Каждый билд содержит информацию о его версии. Есть какая-то версия игры лежащая на сервере. С машины разработчика вы можете менять ее.
С машины пользователя запускается программа автообновления, меняющая все данные игры на данные соответствующие версии на сервере.
Ни о каких AssetBundles программа автообновления не знает и способна работать с чем угодно.


А вот с этого места - можно поподробнее?

1. Что за отдельная программа автообновления, которая работает по принципу системы контроля версий и запускается с машины пользователя?

2. Кто и по какому событию ее запускает?

3. Кто ее перед этим предварительно устанавливает и дает ей соответствующие права/полномочия?

4. Какие игры/программы, работающий по этому принципу, существуют (хотя бы несколько ссылок на соответствующие приложения)?


Поймите меня правильно, я ищу РЕАЛЬНОЕ архитектурное решение с соответствующим описанием, по которому я смогу программно реализовать предложенный подход.

Поэтому мой основной вопрос заключается в следующем.

Каким образом и при помощи какого программного механизма осуществляется инсталляция, запуск и процесс работы "отдельной программы автообновления, меняющей все данные игры на данные соответствующие версии на сервере"?

Я уточню, что речь о работе с мобильной платформой.

Заранее благодарна за конкретику в этом вопросе.
Inessa
UNITрон
 
Сообщения: 160
Зарегистрирован: 13 мар 2013, 11:56

Re: Использование AssetBundles для загрузки обновлений

Сообщение DbIMok 02 ноя 2016, 00:04

Inessa писал(а):при помощи какого программного механизма

AssetBundles
Inessa писал(а):Каким образом

в чем у вас затруднение?
предположим на сервере лежит ваш ассетбандль. вы запускаете маленькую сцену даунлоадера на устройстве, тот смотрит - ассетбандль(и) есть в persistentDataPath, какой он(и) версии (name_XXXX.assetbundle)? скачиваем список версий на сервере, сверяем, качаем те что нужно обновить быстро и параллельно или медленно и последовательно. выводим текущий прогресс.
либо можно разобраться в логике Cache. Проблема только в том, как узнать, какую версию просить на клиенте?
правильный вопрос - половина ответа. учитесь формулировать вопросы понятно.
Новости > _Telegram чат @unity3d_ru (11.6k/4.8k online) > _Telegram канал @unity_news (4.8k подписчиков) > Телеграм тема > "Спасибо"
Аватара пользователя
DbIMok
Адепт
 
Сообщения: 6372
Зарегистрирован: 31 июл 2009, 14:05

Re: Использование AssetBundles для загрузки обновлений

Сообщение Inessa 02 ноя 2016, 10:45

В общем ситуация теперь понятна.

Я принялась за реализацию описанного подхода, в ходе которой наткнулась на одну проблему.

Прежде чем копировать AssetBundles с сервера, необходимо проверить есть ли достаточно свободного места на внутренней памяти устройства.

Использую следующий код для получения этой информации.

Синтаксис:
Используется csharp
using UnityEngine;
using UnityEngine.UI;

public class MainScene : MonoBehaviour
{
    public const int ANDROID_API_18 = 18; // JELLY_BEAN_MR2

    void Start()
    {
        //#if UNITY_ANDROID
        using (var jc = new AndroidJavaClass("android.os.Environment"))
        {
            var file = jc.CallStatic<AndroidJavaObject>("getDataDirectory");
            var path = file.Call<string>("getAbsolutePath");
            var stat = new AndroidJavaObject("android.os.StatFs", path);
            if (getSDKInt() >= ANDROID_API_18)
            {
                var blocks = stat.Call<long>("getAvailableBlocksLong");
                var blockSize = stat.Call<long>("getBlockSizeLong");
                var freeSize = blocks * blockSize;
                Text text = GameObject.Find("Text_Available_Disk_Space").GetComponent<Text>();
                text.text = freeSize.ToString();
            }
            else
            {
                var blocks = stat.Call<long>("getAvailableBlocks");
                var blockSize = stat.Call<long>("getBlockSize");
                var freeSize = blocks * blockSize;
                Text text = GameObject.Find("Text_Available_Disk_Space").GetComponent<Text>();
                text.text = freeSize.ToString();
            }
            //#endif
        }
    }

    static int getSDKInt()
    {
        using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
        {
            return version.GetStatic<int>("SDK_INT");
        }
    }
}
 


В процессе запуска проекта с этим кодом ожидаемая информация о свободном месте на экран устройства не выводится.

В протоколе Logcat выводятся следующие ошибки:

AndroidJavaException: java.lang.NoSuchMethodError: no method with name='getAvailableBlocks' signature='()J' in class Landroid/os/StatFs;
at UnityEngine.AndroidJNISafe.CheckException () [0x00000] in <filename unknown>:0
at UnityEngine.AndroidJNISafe.GetMethodID (IntPtr obj, System.String name, System.String sig) [0x00000] in <filename unknown>:0
at UnityEngine._AndroidJNIHelper.GetMethodID (IntPtr jclass, System.String methodName, System.String signature, Boolean isStatic) [0x00000] in <filename unknown>:0


При запуске этого кода в редакторе Unity выводится ошибка:

- Exception: JNI: Init'd AndroidJavaClass with null ptr!

хотя у меня соответствующая часть кода обернута с помощью директивы #if UNITY_ANDROID.

Скажите, пожалуйста, по какой причине возникают эти ошибки?

Как их исправить, чтобы я смогла получить информацию о свободном месте на внутренней памяти устройства?
Inessa
UNITрон
 
Сообщения: 160
Зарегистрирован: 13 мар 2013, 11:56


Вернуться в Общие вопросы

Кто сейчас на конференции

Сейчас этот форум просматривают: Yandex [Bot] и гости: 8