Files
DeviantMobile-Rohan/Assets/Scripts/AssetBundleSystem/RemoteAssetBundleManager.cs

664 lines
26 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngine.Profiling;
namespace CustomAssetBundleSystem
{
/// <summary>
/// Custom AssetBundle manager - replaces Unity Addressables
/// Full manual control, no Unity 6 bugs, lightweight and fast
/// </summary>
public class RemoteAssetBundleManager : MonoBehaviour
{
private static RemoteAssetBundleManager _instance;
public static RemoteAssetBundleManager Instance
{
get
{
if (_instance == null)
{
var go = new GameObject("[RemoteAssetBundleManager]");
_instance = go.AddComponent<RemoteAssetBundleManager>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
// Configuration
[Header("Remote Catalog Settings")]
public string catalogUrl = "https://assetbundle.uncaptured.co/catalog.json";
public int downloadTimeout = 900; // seconds (15 minutes) for large bundles
public int maxRetries = 3;
[Tooltip("If progress does not change for this many seconds, abort and retry.")]
public float downloadStallTimeout = 45f;
[Tooltip("Base delay between retries (multiplied by attempt index).")]
public float retryBackoffSeconds = 2f;
[Header("Memory Management")]
[Tooltip("Unloads the scene bundle and its dependencies once the scene is loaded to free GPU/CPU memory (recommended on mobile).")]
public bool unloadSceneBundlesAfterLoad = true;
[Tooltip("Calls Resources.UnloadUnusedAssets and GC.Collect before and after heavy scene loads.")]
public bool runMemoryCleanupAroundSceneLoad = true;
[Tooltip("Logs runtime memory usage after cleanup steps.")]
public bool logMemoryStats = false;
// State
private RemoteAssetCatalog _catalog;
private Dictionary<string, AssetBundle> _loadedBundles = new Dictionary<string, AssetBundle>();
private Dictionary<string, Coroutine> _activeDownloads = new Dictionary<string, Coroutine>();
private bool _isInitialized = false;
// Events
public event Action<bool, string> OnCatalogLoaded;
public event Action<string, float> OnBundleDownloadProgress;
public event Action<string, bool> OnBundleLoaded;
void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
#region Initialization
/// <summary>
/// Initialize the system by downloading the remote catalog
/// </summary>
public IEnumerator InitializeAsync()
{
if (_isInitialized)
{
Debug.Log("[AssetBundleManager] Already initialized");
yield break;
}
Debug.Log($"[AssetBundleManager] Downloading catalog from: {catalogUrl}");
bool success = false;
string errorMessage = "";
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
using (UnityWebRequest www = UnityWebRequest.Get(catalogUrl))
{
www.timeout = downloadTimeout;
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
try
{
string jsonText = www.downloadHandler.text;
_catalog = JsonUtility.FromJson<RemoteAssetCatalog>(jsonText);
if (_catalog != null && _catalog.bundles != null)
{
Debug.Log($"[AssetBundleManager] ✓ Catalog loaded successfully!");
Debug.Log($" Version: {_catalog.version}");
Debug.Log($" Base URL: {_catalog.baseUrl}");
Debug.Log($" Bundles: {_catalog.bundles.Count}");
_isInitialized = true;
success = true;
break;
}
else
{
errorMessage = "Catalog JSON is invalid or empty";
}
}
catch (Exception ex)
{
errorMessage = $"JSON parsing failed: {ex.Message}";
Debug.LogError($"[AssetBundleManager] {errorMessage}");
}
}
else
{
errorMessage = $"Download failed: {www.error}";
Debug.LogWarning($"[AssetBundleManager] Attempt {attempt}/{maxRetries}: {errorMessage}");
if (attempt < maxRetries)
{
yield return new WaitForSeconds(2f); // Wait before retry
}
}
}
}
OnCatalogLoaded?.Invoke(success, errorMessage);
if (!success)
{
Debug.LogError($"[AssetBundleManager] ✗ Failed to load catalog after {maxRetries} attempts: {errorMessage}");
}
}
public bool IsInitialized => _isInitialized;
#endregion
#region Bundle Loading
/// <summary>
/// Load an AssetBundle by name (with dependencies)
/// </summary>
public IEnumerator LoadBundleAsync(string bundleName, Action<AssetBundle> onComplete = null)
{
if (!_isInitialized)
{
Debug.LogError("[AssetBundleManager] Not initialized! Call InitializeAsync() first.");
onComplete?.Invoke(null);
yield break;
}
// Check if already loaded
if (_loadedBundles.TryGetValue(bundleName, out AssetBundle cachedBundle))
{
Debug.Log($"[AssetBundleManager] Bundle '{bundleName}' already loaded (using cached)");
onComplete?.Invoke(cachedBundle);
yield break;
}
// Find bundle entry in catalog
var bundleEntry = _catalog.FindBundleByName(bundleName);
if (bundleEntry == null)
{
Debug.LogError($"[AssetBundleManager] Bundle '{bundleName}' not found in catalog!");
onComplete?.Invoke(null);
yield break;
}
// Load dependencies first
if (bundleEntry.dependencies != null && bundleEntry.dependencies.Count > 0)
{
Debug.Log($"[AssetBundleManager] Loading {bundleEntry.dependencies.Count} dependencies for '{bundleName}'...");
foreach (var depName in bundleEntry.dependencies)
{
if (!_loadedBundles.ContainsKey(depName))
{
bool depLoaded = false;
yield return LoadBundleAsync(depName, (bundle) => { depLoaded = bundle != null; });
if (!depLoaded)
{
Debug.LogError($"[AssetBundleManager] Failed to load dependency '{depName}' for '{bundleName}'");
onComplete?.Invoke(null);
yield break;
}
}
}
}
// Download and load the bundle WITH CACHING
string bundleUrl = bundleEntry.GetFullUrl(_catalog.baseUrl);
// Check if bundle is already cached
bool isCached = Caching.IsVersionCached(bundleUrl, Hash128.Parse(bundleEntry.hash));
if (isCached)
{
Debug.Log($"[AssetBundleManager] ✓ Bundle '{bundleName}' found in cache (INSTANT LOAD)");
}
else
{
Debug.Log($"[AssetBundleManager] Downloading bundle: {bundleUrl}");
Debug.Log($" Size: {bundleEntry.size / 1024f / 1024f:F2} MB");
}
AssetBundle downloadedBundle = null;
string lastError = null;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
if (Application.internetReachability == NetworkReachability.NotReachable)
{
lastError = "No internet connection detected";
Debug.LogWarning($"[AssetBundleManager] Internet not reachable while downloading '{bundleName}'. Attempt {attempt}/{maxRetries}.");
yield return new WaitForSeconds(retryBackoffSeconds * attempt);
continue;
}
using (UnityWebRequest www = UnityWebRequestAssetBundle.GetAssetBundle(bundleUrl, Hash128.Parse(bundleEntry.hash), 0))
{
www.timeout = downloadTimeout;
www.disposeDownloadHandlerOnDispose = true;
var operation = www.SendWebRequest();
float lastProgress = -1f;
float stalledTimer = 0f;
// Report download progress (only shows progress if downloading)
while (!operation.isDone)
{
float progress = Mathf.Clamp01(www.downloadProgress);
if (!isCached)
{
OnBundleDownloadProgress?.Invoke(bundleName, progress);
}
if (!isCached && downloadStallTimeout > 0f)
{
if (Mathf.Approximately(progress, lastProgress))
{
stalledTimer += Time.unscaledDeltaTime;
if (stalledTimer >= downloadStallTimeout)
{
Debug.LogWarning($"[AssetBundleManager] Download stalled for '{bundleName}' (progress {progress:P0}) after {stalledTimer:F1}s. Aborting attempt {attempt}/{maxRetries}.");
www.Abort();
break;
}
}
else
{
lastProgress = progress;
stalledTimer = 0f;
}
}
yield return null;
}
if (www.result == UnityWebRequest.Result.Success)
{
downloadedBundle = DownloadHandlerAssetBundle.GetContent(www);
if (downloadedBundle != null)
{
_loadedBundles[bundleName] = downloadedBundle;
if (isCached)
{
Debug.Log($"[AssetBundleManager] ✓ Bundle '{bundleName}' loaded from cache!");
}
else
{
Debug.Log($"[AssetBundleManager] ✓ Bundle '{bundleName}' downloaded and cached!");
Debug.Log($" Cached location: {Caching.defaultCache.path}");
}
OnBundleLoaded?.Invoke(bundleName, true);
onComplete?.Invoke(downloadedBundle);
yield break;
}
lastError = "Bundle extraction returned null";
Debug.LogError($"[AssetBundleManager] ✗ Failed to extract bundle from download: {bundleName}");
}
else
{
lastError = string.IsNullOrEmpty(www.error) ? www.result.ToString() : www.error;
Debug.LogWarning($"[AssetBundleManager] Attempt {attempt}/{maxRetries} failed for '{bundleName}': {lastError}");
}
}
if (attempt < maxRetries)
{
float delay = retryBackoffSeconds * attempt;
Debug.Log($"[AssetBundleManager] Retrying '{bundleName}' in {delay:F1}s...");
yield return new WaitForSeconds(delay);
}
}
Debug.LogError($"[AssetBundleManager] ✗ Failed to download bundle '{bundleName}': {lastError}");
OnBundleLoaded?.Invoke(bundleName, false);
onComplete?.Invoke(null);
yield break;
}
/// <summary>
/// Load a scene from a remote AssetBundle by scene name
/// </summary>
public IEnumerator LoadSceneAsync(string sceneName, Action<bool> onComplete = null)
{
if (!_isInitialized)
{
Debug.LogError("[AssetBundleManager] Not initialized! Call InitializeAsync() first.");
onComplete?.Invoke(false);
yield break;
}
// Find which bundle contains this scene
var bundleEntry = _catalog.FindBundleByScene(sceneName);
if (bundleEntry == null)
{
Debug.LogError($"[AssetBundleManager] Scene '{sceneName}' not found in any bundle!");
onComplete?.Invoke(false);
yield break;
}
Debug.Log($"[AssetBundleManager] Scene '{sceneName}' found in bundle '{bundleEntry.bundleName}'");
var dependencyEntries = _catalog.GetAllDependencies(bundleEntry);
if (runMemoryCleanupAroundSceneLoad)
{
yield return RunMemoryCleanupAsync($"[AssetBundleManager] Pre-load memory cleanup before '{sceneName}'");
}
// Load the bundle (and dependencies)
bool bundleLoaded = false;
yield return LoadBundleAsync(bundleEntry.bundleName, (bundle) => { bundleLoaded = bundle != null; });
if (!bundleLoaded)
{
Debug.LogError($"[AssetBundleManager] Failed to load bundle for scene '{sceneName}'");
onComplete?.Invoke(false);
yield break;
}
// CRITICAL FIX: Load scene in SINGLE mode to avoid Unity bug with additive loading
// The bug causes scene loading to hang at 0.2% when using Additive mode from AssetBundles
Debug.Log($"[AssetBundleManager] Loading scene '{sceneName}' from bundle (SINGLE mode to avoid Unity hang bug)...");
// Extra instrumentation: timestamp, loaded scenes, memory
Debug.Log($"[AssetBundleManager] StartTime: {DateTime.UtcNow:O}");
Debug.Log($"[AssetBundleManager] Current loaded scenes: {SceneManager.sceneCount}");
for (int si = 0; si < SceneManager.sceneCount; si++)
{
var s = SceneManager.GetSceneAt(si);
Debug.Log($"[AssetBundleManager] Scene[{si}] name={s.name} isLoaded={s.isLoaded}");
}
var sceneLoadOperation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
if (sceneLoadOperation == null)
{
Debug.LogError($"[AssetBundleManager] Failed to start scene load: {sceneName}");
onComplete?.Invoke(false);
yield break;
}
// Allow scene to activate immediately (no manual activation needed)
sceneLoadOperation.allowSceneActivation = true;
// Wait for scene to load with timeout protection and stall detection
float timeout = 30f; // 30 second timeout
float elapsed = 0f;
// Track progress samples to detect stalls (common symptom: stuck at ~0.2)
float lastProgress = -1f;
float progressUnchangedTime = 0f;
const float stallThresholdSeconds = 8f; // if progress unchanged for this long, treat as stall
bool attemptedRetry = false;
while (!sceneLoadOperation.isDone && elapsed < timeout)
{
float progress = sceneLoadOperation.progress; // 0..1
// Log only when progress changes or at periodic intervals to avoid log spam
if (Mathf.Approximately(progress, lastProgress))
{
progressUnchangedTime += Time.deltaTime;
}
else
{
lastProgress = progress;
progressUnchangedTime = 0f;
Debug.Log($"[AssetBundleManager] Scene load progress: {progress * 100:F1}% (elapsed {elapsed:F2}s)");
}
// If we detect a stall near 0.2 (historical hang), attempt a single retry
if (!attemptedRetry && progress <= 0.21f && progressUnchangedTime > stallThresholdSeconds)
{
Debug.LogWarning($"[AssetBundleManager] Detected scene-load stall at {progress * 100:F1}% for {progressUnchangedTime:F1}s. Attempting one retry...");
// Attempt to unload bundle and retry once after small delay
try
{
string retryBundle = bundleEntry.bundleName;
if (_loadedBundles.ContainsKey(retryBundle))
{
Debug.Log($"[AssetBundleManager] Unloading bundle '{retryBundle}' to attempt retry...");
UnloadBundle(retryBundle, true);
}
}
catch (Exception ex)
{
Debug.LogException(ex);
}
// wait a short moment and restart the load operation
yield return new WaitForSeconds(0.5f);
attemptedRetry = true;
elapsed = 0f;
lastProgress = -1f;
progressUnchangedTime = 0f;
Debug.Log("[AssetBundleManager] Starting retry of SceneManager.LoadSceneAsync...");
sceneLoadOperation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
if (sceneLoadOperation == null)
{
Debug.LogError($"[AssetBundleManager] Retry failed to start scene load: {sceneName}");
onComplete?.Invoke(false);
yield break;
}
sceneLoadOperation.allowSceneActivation = true;
}
elapsed += Time.deltaTime;
yield return null;
}
if (elapsed >= timeout && !sceneLoadOperation.isDone)
{
Debug.LogError($"[AssetBundleManager] Scene load timed out after {timeout} seconds! progress={sceneLoadOperation.progress * 100:F1}% attemptedRetry={attemptedRetry}");
// dump some diagnostic info
Debug.Log($"[AssetBundleManager] Loaded bundles: {string.Join(",", GetLoadedBundleNames())}");
Debug.Log($"[AssetBundleManager] Memory stats: {GetMemoryStats()}");
onComplete?.Invoke(false);
yield break;
}
Debug.Log($"[AssetBundleManager] ✓ Scene '{sceneName}' loaded successfully in {elapsed:F2} seconds! attemptedRetry={attemptedRetry}");
if (unloadSceneBundlesAfterLoad)
{
yield return null; // ensure the scene has a frame to settle before we release bundles
ReleaseSceneBundles(bundleEntry, dependencyEntries);
}
if (runMemoryCleanupAroundSceneLoad)
{
yield return RunMemoryCleanupAsync($"[AssetBundleManager] Post-load memory cleanup after '{sceneName}'");
}
onComplete?.Invoke(true);
}
#endregion
#region Cache Management
/// <summary>
/// Check if a bundle is cached on disk
/// </summary>
public bool IsBundleCached(string bundleName)
{
if (!_isInitialized || _catalog == null)
return false;
var bundleEntry = _catalog.FindBundleByName(bundleName);
if (bundleEntry == null)
return false;
string bundleUrl = bundleEntry.GetFullUrl(_catalog.baseUrl);
return Caching.IsVersionCached(bundleUrl, Hash128.Parse(bundleEntry.hash));
}
/// <summary>
/// Get cache information
/// </summary>
public void GetCacheInfo()
{
Debug.Log($"[AssetBundleManager] Cache Info:");
Debug.Log($" Cache path: {Caching.defaultCache.path}");
Debug.Log($" Cache space used: {Caching.defaultCache.spaceOccupied / 1024f / 1024f:F2} MB");
Debug.Log($" Max cache size: {Caching.defaultCache.maximumAvailableStorageSpace / 1024f / 1024f / 1024f:F2} GB");
Debug.Log($" Caching ready: {Caching.ready}");
}
/// <summary>
/// Clear all cached bundles (forces re-download on next load)
/// </summary>
public bool ClearCache()
{
bool success = Caching.ClearCache();
if (success)
{
Debug.Log($"[AssetBundleManager] ✓ Cache cleared successfully!");
}
else
{
Debug.LogError($"[AssetBundleManager] ✗ Failed to clear cache");
}
return success;
}
/// <summary>
/// Clear only old versions of cached bundles (keeps current version)
/// </summary>
public void CleanupOldCachedVersions()
{
bool success = Caching.ClearAllCachedVersions(Caching.defaultCache.path);
if (success)
{
Debug.Log($"[AssetBundleManager] ✓ Cleaned up old cached versions");
}
else
{
Debug.LogWarning($"[AssetBundleManager] Failed to cleanup old cached versions");
}
}
#endregion
#region Memory Management
/// <summary>
/// Unload a specific bundle
/// </summary>
public void UnloadBundle(string bundleName, bool unloadAllLoadedObjects = false)
{
if (_loadedBundles.TryGetValue(bundleName, out AssetBundle bundle))
{
bundle.Unload(unloadAllLoadedObjects);
_loadedBundles.Remove(bundleName);
Debug.Log($"[AssetBundleManager] Unloaded bundle: {bundleName}");
}
}
/// <summary>
/// Unload all bundles
/// </summary>
public void UnloadAllBundles(bool unloadAllLoadedObjects = false)
{
foreach (var kvp in _loadedBundles)
{
kvp.Value.Unload(unloadAllLoadedObjects);
}
_loadedBundles.Clear();
Debug.Log($"[AssetBundleManager] Unloaded all bundles");
}
/// <summary>
/// Get memory usage stats
/// </summary>
public string GetMemoryStats()
{
long totalMemory = 0;
foreach (var kvp in _loadedBundles)
{
// AssetBundle doesn't expose size directly, but we can estimate
totalMemory += kvp.Value.GetAllAssetNames().Length * 1024; // Rough estimate
}
return $"Loaded Bundles: {_loadedBundles.Count}, Estimated Memory: {totalMemory / 1024f / 1024f:F2} MB";
}
private void ReleaseSceneBundles(RemoteAssetCatalog.BundleEntry rootBundle, List<RemoteAssetCatalog.BundleEntry> dependencyEntries)
{
if (rootBundle == null)
return;
var bundlesToRelease = new List<RemoteAssetCatalog.BundleEntry> { rootBundle };
if (dependencyEntries != null)
{
bundlesToRelease.AddRange(dependencyEntries);
}
foreach (var entry in bundlesToRelease)
{
if (entry == null)
continue;
if (_loadedBundles.ContainsKey(entry.bundleName))
{
Debug.Log($"[AssetBundleManager] Releasing bundle '{entry.bundleName}' after scene load");
UnloadBundle(entry.bundleName, false);
}
}
}
private IEnumerator RunMemoryCleanupAsync(string reason)
{
Debug.Log(reason);
// Give the UI a frame to update before we start releasing memory heavy objects.
yield return null;
yield return Resources.UnloadUnusedAssets();
GC.Collect();
if (logMemoryStats)
{
Debug.Log($"[AssetBundleManager] Runtime memory usage: {GetRuntimeMemoryStats()}");
}
}
private string GetRuntimeMemoryStats()
{
long allocated = Profiler.GetTotalAllocatedMemoryLong();
long reserved = Profiler.GetTotalReservedMemoryLong();
long unused = Profiler.GetTotalUnusedReservedMemoryLong();
return $"Allocated: {allocated / (1024f * 1024f):F1} MB | Reserved: {reserved / (1024f * 1024f):F1} MB | Unused Reserved: {unused / (1024f * 1024f):F1} MB | Bundles Cached: {GetMemoryStats()}";
}
#endregion
#region Catalog Info
/// <summary>
/// Get the loaded catalog (null if not initialized)
/// </summary>
public RemoteAssetCatalog GetCatalog() => _catalog;
/// <summary>
/// Check if a bundle is loaded
/// </summary>
public bool IsBundleLoaded(string bundleName) => _loadedBundles.ContainsKey(bundleName);
/// <summary>
/// Get list of all loaded bundle names
/// </summary>
public List<string> GetLoadedBundleNames() => new List<string>(_loadedBundles.Keys);
#endregion
void OnDestroy()
{
UnloadAllBundles(true);
}
}
}