Fundamentos de los AssetBundles y las Actualizaciones en Caliente
Los AssetBundles son archivos comprimidos que permiten empaquetar recursos como modelos, texturas y audio para su carga dinámica. A diferencia de la carpeta Resources, que es de solo lectura en tiempo de ejecución y aumenta el tamaño inicial de la aplicación, los AssetBundles pueden almacenarse en rutas personalizadas y descargarse bajo demanda. Esto facilita las actualizaciones en caliente (hot updates), donde el cliente inicial contiene un conjunto mínimo de recursos y descarga el resto desde un servidor remoto comparando versiones o hashes de archivos.
Empaquetado y Gestión de Dependencias
Para generar estos pauqetes, se pueden utilizar herramientas oficiales como el AssetBundle Browser o scripts de compilación personalizados. Un aspecto crítico al trabajar con múltiples paquetes es la gestión de dependencias. Si un recurso en el Paquete A utiliza un material del Paquete B, cargar solo el Paquete A resultará en materiales faltantes. Para resolver esto, Unity genera un archivo de manifiesto principal que mapea todas las dependencias entre los distintos bundles, permitiendo cargar los requisitos previos automáticamente antes de instanciar el recurso objetivo.
Carga Básica y Liberación de Memoria
La API de Unity permite cargar recursos de forma síncrona o asíncrona. Es fundamental especificar el tipo de recurso mediante genéricos para evitar ambigüedades y asegurar un rendimiento óptimo durante la deserialización.
// Carga síncrona básica
AssetBundle bundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "mi_bundle"));
// Uso de genéricos para mayor seguridad de tipos
GameObject prefab = bundle.LoadAsset<gameobject>("MiPrefab");
Instantiate(prefab);
// Liberación de memoria
// 'true' destruye los objetos instanciados, 'false' solo descarga el bundle de memoria
bundle.Unload(false);
</gameobject>
Implementación de un Gestor Centralizado de AssetBundles
Para centralizar y optimizar la carga, es recomendable implementar un gestor que maneje el caché de los bundles, resuelva las dependencias a través del manifiesto y ofrezca métodos tanto síncronos como asíncronos. A continuación, se presenta una versión refactorizada y optimizada de un administrador de recursos que mejora la estructura original mediante el uso de patrones de diseño y manejo de rutas más robusto.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class BundleManager : MonoBehaviour
{
private static BundleManager _instance;
public static BundleManager Instance
{
get
{
if (_instance == null)
{
GameObject go = new GameObject("BundleManager");
_instance = go.AddComponent<bundlemanager>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
private AssetBundle _mainBundle;
private AssetBundleManifest _manifest;
private readonly Dictionary<string assetbundle=""> _loadedBundles = new Dictionary<string assetbundle="">();
private string BasePath => Application.streamingAssetsPath;
private string PlatformName
{
get
{
#if UNITY_IOS
return "iOS";
#elif UNITY_ANDROID
return "Android";
#else
return "Standalone";
#endif
}
}
private void EnsureMainBundleLoaded()
{
if (_mainBundle != null) return;
string mainBundlePath = Path.Combine(BasePath, PlatformName);
_mainBundle = AssetBundle.LoadFromFile(mainBundlePath);
if (_mainBundle == null)
{
Debug.LogError($"Failed to load main bundle from {mainBundlePath}");
return;
}
_manifest = _mainBundle.LoadAsset<assetbundlemanifest>("AssetBundleManifest");
}
private void LoadBundleWithDependencies(string bundleName)
{
EnsureMainBundleLoaded();
if (_loadedBundles.ContainsKey(bundleName)) return;
// Cargar dependencias primero
string[] dependencies = _manifest.GetAllDependencies(bundleName);
foreach (string dep in dependencies)
{
if (!_loadedBundles.ContainsKey(dep))
{
string depPath = Path.Combine(BasePath, dep);
AssetBundle depBundle = AssetBundle.LoadFromFile(depPath);
_loadedBundles.Add(dep, depBundle);
}
}
// Cargar el bundle objetivo
string targetPath = Path.Combine(BasePath, bundleName);
AssetBundle targetBundle = AssetBundle.LoadFromFile(targetPath);
_loadedBundles.Add(bundleName, targetBundle);
}
public T LoadAsset<t>(string bundleName, string assetName) where T : UnityEngine.Object
{
LoadBundleWithDependencies(bundleName);
T asset = _loadedBundles[bundleName].LoadAsset<t>(assetName);
if (asset is GameObject go)
{
return Instantiate(go) as T;
}
return asset;
}
public void LoadAssetAsync<t>(string bundleName, string assetName, Action<t> onComplete) where T : UnityEngine.Object
{
StartCoroutine(LoadAssetAsyncCoroutine(bundleName, assetName, onComplete));
}
private IEnumerator LoadAssetAsyncCoroutine<t>(string bundleName, string assetName, Action<t> onComplete) where T : UnityEngine.Object
{
LoadBundleWithDependencies(bundleName);
AssetBundleRequest request = _loadedBundles[bundleName].LoadAssetAsync<t>(assetName);
yield return request;
T asset = request.asset as T;
if (asset is GameObject go)
{
onComplete(Instantiate(go) as T);
}
else
{
onComplete(asset);
}
}
public void UnloadBundle(string bundleName)
{
if (_loadedBundles.TryGetValue(bundleName, out AssetBundle bundle))
{
bundle.Unload(false);
_loadedBundles.Remove(bundleName);
}
}
public void UnloadAllBundles()
{
foreach (var bundle in _loadedBundles.Values)
{
bundle.Unload(false);
}
_loadedBundles.Clear();
if (_mainBundle != null)
{
_mainBundle.Unload(false);
_mainBundle = null;
}
_manifest = null;
}
}
</t></t></t></t></t></t></t></assetbundlemanifest></string></string></bundlemanager>