Обработка зависимостей между AssetBundles
AssetBundles становятся зависимыми от других, когда их объекты ссылаются на объекты в другом AssetBundle. Если объект, на который сделана ссылка, не назначен ни одному AssetBundle, Unity встроит его в зависимый AssetBundles во время сборки. Если несколько AssetBundles ссылаются на один и тот же неназначенный объект, каждый AssetBundle содержит свою собственную копию, увеличивая использование памяти.
Чтобы загрузить зависимый AssetBundles, убедитесь, что зависимости загружены в память до доступа к их зависимому AssetBundles. Например, если Bundle 1 содержит материал, который ссылается на текстуру в Bundle 2, загрузите Bundle 2 в память до доступа к материалу из Bundle 1. Unity не разрешает зависимости автоматически. Для управления зависимостями во время выполнения вы можете использовать AssetBundleManifest. Дополнительные сведения см. в Загрузка ассетов из AssetBundles.
Как Unity отслеживает ссылки
Unity отслеживает ссылки между AssetBundles следующим образом:
- Зависимости на уровне манифеста: Зависимости между AssetBundles находятся в корневом файле
.manifestпапки сборки и доступны черезAssetBundleManifest.GetAllDependencies. -
Ссылки низкого уровня SerializedFile: Прямые ссылки на объекты между AssetBundles появляются в
SerializedFileЭти низкоуровневые ссылки не записывают имя файла AssetBundle, а только имена серийных файлов внутри AssetBundles. Они работают только тогда, когда соответствующий файл AssetBundles уже загружен. Например:Materialвbundle0ссылается наShaderвbundle1.- Файл
SerializedFileвнутриbundle0содержит в заголовке таблицу внешних ссылок. В этой таблице есть запись, указывающая на путьSerializedFileвbundle1. m_Shader.m_FileIDобъектаMaterialзаписывает индекс внешней таблицы ссылок, соответствующей SerializedFile внутриbundle1.
- AssetBundle объект: Объект
AssetBundleконсолидирует информацию о зависимостях в своихm_Containerиm_PreloadTable. Когда запрашивается загрузка ассета,m_PreloadTableобеспечивает идентификацию для загрузки всех необходимых объектов, включая объекты из других AssetBundles. Это может включать очень большие структуры данных для AssetBundles со многими ассетами и зависимостями.
Представление типа скрипта в AssetBundles
MonoScript объект в Unity представляет собой MonoBehaviour-производный класс. Это касается и классов, производных от ScriptableObject. MonoScript объект записывает сборку, пространство имён и класс в виде строк, однозначно определяющих тип.
Когда Unity сериализирует MonoBehaviour объекта, он регистрирует GUID и LFID из MonoScript класс, который затем напрямую записывает имя класса.
При строительстве AssetBundles Unity включает MonoScript объекты для каждого класса, производного от MonoBehaviour, который входит в сборку. Эти MonoScript объекты могут находиться внутри того же SerializedFile, что и MonoBehavior (локальная ссылка), или во внешнем Сериализованном файле. В обоих случаях MonoScripts ссылается точно таким же механизмом, как и другие объекты. прямые ссылки между объектами.
Следующие действия могут привести к изменению данных MonoScript:
- Перемещение файла скрипта в расположение, которое находится под другим файл определения сборки.
- Изменение имени файла определения сборки, содержащего класс.
- Добавление или изменение пространства имён класса.
- Изменение имени класса.
Когда эти изменения произойдут, перестроите AssetBundles в вашем проекте.
Избегайте дублирования информации в AssetBundles
По умолчанию Unity не оптимизирует дубликаты данных в AssetBundles. Например, если два AssetBundles содержат префаб, ссылающийся на один и тот же неназначенный материал, Unity встраивает копию материала в оба AssetBundles. Это увеличивает размер установки, использование памяти выполнения и влияет на пакетирование, поскольку Unity рассматривает каждую копию как уникальную.
Назначайте общие ресурсы общему AssetBundle, чтобы избежать дублирования. Во время сборки Unity автоматически включает зависимости в назначенный AssetBundle. Это значительно уменьшает размер других AssetBundles. Например:
- Извлечение общего Материала и его зависимостей в
modulesmaterialsAssetBundle. - Префаб AssetBundles затем только ссылаются на
modulesmaterialsAssetBundle, уменьшая их размер.
Дополнительные сведения см. в Избежание дублирования активов.
Загрузка в режиме выполнения
При использовании общего AssetBundle для общих ассетов загрузите его в память до загрузки AssetBundles, которые зависят от него. В следующем примере общий Материал загружается правильно, потому что его AssetBundle (materialsAB) загружается первым:
using System.IO;
using UnityEngine;
public class InstantiateAssetBundles : MonoBehaviour
{
void Start()
{
// Load the AssetBundles
AssetBundle materialsAB = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, Path.Combine("AssetBundles", "modulesmaterials")));
AssetBundle moduleAB = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, Path.Combine("AssetBundles", "example-prefab")));
// Check for errors
if (materialsAB == null || moduleAB == null)
{
Debug.Log("Failed to load AssetBundle!");
return;
}
GameObject prefab = moduleAB.LoadAsset<GameObject>("example-prefab");
// Instantiate the prefab
Instantiate(prefab);
}
}
AssetBundle разгрузка
Правильно управляйте зависимостями при разгрузке AssetBundles, чтобы предотвратить сбои или неопределенное поведение. Зависящий AssetBundles не должен оставаться загруженным после разгрузки его зависимостей. Перезагрузка зависимостей отдельно также может вызвать проблемы. Когда загружается AssetBundle, он создает структуры данных, которые конкретно указывают на объекты внутри зависимого AssetBundles. Если ссылка на AssetBundle разгружается и затем перегружается, его объекты назначаются новому InstanceIDs и не подключаются к зависимому AssetBundle, что может привести к сбоям или ошибкам сериализации.
Чтобы избежать этого, следите за зависимостями и никогда не выгружайте AssetBundle, если на него ссылается другой AssetBundle, если только вы также не выгружаете ссылающийся AssetBundle. Реализация системы подсчета ссылок является безопасным способом управления выгрузкой AssetBundle.
Пример стратегии разгрузки: подсчет ссылки
Внедрить систему подсчета ссылок для отслеживания и безопасной разгрузки AssetBundles только тогда, когда они больше не используются.
Следующий пример отслеживает зависимости и безопасно выгружает неиспользуемый AssetBundles:
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class AssetBundleManager
{
// Path to the directory containing AssetBundles
private string assetBundlesDirectory;
// The AssetBundleManifest containing dependency information
private AssetBundleManifest assetBundleManifest;
// Reference counts for loaded AssetBundles
private Dictionary<string, int> assetBundleReferenceCounts = new Dictionary<string, int>();
// Loaded AssetBundles cache
private Dictionary<string, AssetBundle> loadedAssetBundles = new Dictionary<string, AssetBundle>();
// Initializes the AssetBundleManager with the manifest and directory path
public void Initialize(string manifestBundlePath, string assetBundlesDirectory)
{
this.assetBundlesDirectory = assetBundlesDirectory;
AssetBundle manifestBundle = AssetBundle.LoadFromFile(manifestBundlePath);
assetBundleManifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
manifestBundle.Unload(false);
}
// Loads an AssetBundle and its dependencies, incrementing reference counts
public AssetBundle LoadBundle(string bundlePath)
{
AssetBundle bundle = LoadAssetBundleIfNotLoaded(bundlePath);
IncrementReferenceCount(bundle.name);
string[] dependencyBundleNames = assetBundleManifest.GetAllDependencies(bundle.name);
foreach (string dependency in dependencyBundleNames)
{
string dependencyBundlePath = GetAssetBundlePathFromName(dependency);
LoadAssetBundleIfNotLoaded(dependencyBundlePath);
IncrementReferenceCount(dependency);
}
return bundle;
}
// Loads an AssetBundle if it is not already loaded
private AssetBundle LoadAssetBundleIfNotLoaded(string bundlePath)
{
if (!loadedAssetBundles.TryGetValue(bundlePath, out AssetBundle bundle))
{
// For simplicity, this example only shows the case of synchronous loading, but support for
// LoadFromFileAsync() and the other load methods could also be added with similar code.
bundle = AssetBundle.LoadFromFile(bundlePath);
if (bundle == null)
{
throw new System.Exception($"Failed to load AssetBundle at path {bundlePath}");
}
loadedAssetBundles.Add(bundlePath, bundle);
}
return bundle;
}
// Unloads an AssetBundle and its dependencies if their reference counts reach zero
public void UnloadBundle(AssetBundle bundle)
{
string[] dependencyBundleNames = assetBundleManifest.GetAllDependencies(bundle.name);
DecrementReferenceCount(bundle.name);
foreach (string dependency in dependencyBundleNames)
{
DecrementReferenceCount(dependency);
}
List<string> bundlesToUnload = new List<string>();
foreach (KeyValuePair<string, AssetBundle> loadedBundleEntry in loadedAssetBundles)
{
if (assetBundleReferenceCounts[loadedBundleEntry.Value.name] <= 0)
{
bundlesToUnload.Add(loadedBundleEntry.Key);
}
}
foreach (string bundlePath in bundlesToUnload)
{
loadedAssetBundles[bundlePath].Unload(true);
loadedAssetBundles.Remove(bundlePath);
}
}
// Gets the full path of an AssetBundle given its name
private string GetAssetBundlePathFromName(string name)
{
return Path.Combine(assetBundlesDirectory, name);
}
// Increments the reference count for a given AssetBundle
private void IncrementReferenceCount(string bundleName)
{
if (assetBundleReferenceCounts.ContainsKey(bundleName))
{
assetBundleReferenceCounts[bundleName]++;
}
else
{
assetBundleReferenceCounts[bundleName] = 1;
}
}
// Decrements the reference count for a given AssetBundle
private void DecrementReferenceCount(string bundleName)
{
if (assetBundleReferenceCounts.ContainsKey(bundleName))
{
assetBundleReferenceCounts[bundleName]--;
}
else
{
string errorMessage = $"Attempted to decrement reference count for non-existent bundle: {bundleName}";
throw new KeyNotFoundException(errorMessage);
}
}
}
Примечание: При использовании LZ4 сжатых и несжатых AssetBundles, AssetBundle.LoadFromFile только загружает каталог содержимого в память, но не само содержимое. Чтобы проверить, происходит ли это, используйте пакет Память Profiler для проверки использования памяти.