chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
347
Assets/Scripts/AssetBundleSystem/AssetBundleDownloadManager.cs
Normal file
347
Assets/Scripts/AssetBundleSystem/AssetBundleDownloadManager.cs
Normal file
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace CustomAssetBundleSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages AssetBundle downloads during LoadingScreen with UI feedback
|
||||
/// Checks cache, downloads if needed, handles offline mode
|
||||
/// </summary>
|
||||
public class AssetBundleDownloadManager : MonoBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
[SerializeField] private Slider loadingSlider;
|
||||
[SerializeField] private TextMeshProUGUI loadingText;
|
||||
[SerializeField] private TextMeshProUGUI hintsText;
|
||||
[SerializeField] private GameObject noInternetPanel; // Optional: Shows when internet required but not available
|
||||
|
||||
[Header("Download Settings")]
|
||||
[SerializeField] private string[] bundlesToPreload = new[] { "gamescene" };
|
||||
[SerializeField] private float minDisplayTime = 2f; // Minimum time to show loading (for UX)
|
||||
|
||||
[Header("Hints")]
|
||||
[SerializeField] private string[] downloadHints = new[]
|
||||
{
|
||||
"Downloading game content...",
|
||||
"Preparing assets for better performance...",
|
||||
"First time setup - this only happens once!",
|
||||
"Future launches will be instant!"
|
||||
};
|
||||
|
||||
[SerializeField] private string[] cachedHints = new[]
|
||||
{
|
||||
"Complete Weekly Missions to gain XP",
|
||||
"Complete a 20 Wins streak to level up in Rank",
|
||||
"Missions change every Week"
|
||||
};
|
||||
|
||||
// State
|
||||
private bool _isDownloading = false;
|
||||
private bool _downloadComplete = false;
|
||||
private bool _hasInternet = true;
|
||||
private RemoteAssetBundleManager _bundleManager;
|
||||
|
||||
// Events
|
||||
public event Action<bool> OnDownloadComplete; // true = success, false = failed
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_bundleManager = RemoteAssetBundleManager.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the download process - call this from LoadingScreen
|
||||
/// </summary>
|
||||
public IEnumerator StartDownloadProcess(Action<bool> onComplete = null)
|
||||
{
|
||||
if (_isDownloading)
|
||||
{
|
||||
Debug.LogWarning("[DownloadManager] Download already in progress");
|
||||
yield break;
|
||||
}
|
||||
|
||||
_isDownloading = true;
|
||||
_downloadComplete = false;
|
||||
float startTime = Time.time;
|
||||
|
||||
Debug.Log("[DownloadManager] Starting AssetBundle download process...");
|
||||
|
||||
// Step 1: Initialize RemoteAssetBundleManager (downloads catalog)
|
||||
yield return InitializeBundleSystem();
|
||||
|
||||
if (!_bundleManager.IsInitialized)
|
||||
{
|
||||
Debug.LogError("[DownloadManager] Failed to initialize bundle system");
|
||||
HandleDownloadFailed("Failed to connect to server");
|
||||
onComplete?.Invoke(false);
|
||||
OnDownloadComplete?.Invoke(false);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Step 2: Check if bundles are already cached
|
||||
bool allCached = CheckIfBundlesCached();
|
||||
|
||||
if (allCached)
|
||||
{
|
||||
Debug.Log("[DownloadManager] ✓ All bundles already cached! (Offline play available)");
|
||||
UpdateUI(1f, "Ready!", cachedHints);
|
||||
|
||||
// Show success for minimum display time
|
||||
float elapsed = Time.time - startTime;
|
||||
if (elapsed < minDisplayTime)
|
||||
{
|
||||
yield return new WaitForSeconds(minDisplayTime - elapsed);
|
||||
}
|
||||
|
||||
_downloadComplete = true;
|
||||
_isDownloading = false;
|
||||
onComplete?.Invoke(true);
|
||||
OnDownloadComplete?.Invoke(true);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Step 3: Need to download - check internet connectivity
|
||||
_hasInternet = Application.internetReachability != NetworkReachability.NotReachable;
|
||||
|
||||
if (!_hasInternet)
|
||||
{
|
||||
Debug.LogWarning("[DownloadManager] No internet connection and bundles not cached");
|
||||
HandleNoInternet();
|
||||
onComplete?.Invoke(false);
|
||||
OnDownloadComplete?.Invoke(false);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Step 4: Download bundles with progress updates
|
||||
Debug.Log($"[DownloadManager] Downloading {bundlesToPreload.Length} bundle(s)...");
|
||||
UpdateUI(0f, "Downloading game content", downloadHints);
|
||||
|
||||
bool downloadSuccess = true;
|
||||
int bundleIndex = 0;
|
||||
|
||||
foreach (string bundleName in bundlesToPreload)
|
||||
{
|
||||
bundleIndex++;
|
||||
Debug.Log($"[DownloadManager] Downloading bundle {bundleIndex}/{bundlesToPreload.Length}: {bundleName}");
|
||||
|
||||
bool bundleLoaded = false;
|
||||
float bundleProgress = 0f;
|
||||
|
||||
// Subscribe to progress updates
|
||||
void ProgressHandler(string name, float progress)
|
||||
{
|
||||
if (name == bundleName)
|
||||
{
|
||||
bundleProgress = progress;
|
||||
float overallProgress = (bundleIndex - 1f + progress) / bundlesToPreload.Length;
|
||||
UpdateUI(overallProgress, $"Downloading {bundleName}", downloadHints);
|
||||
}
|
||||
}
|
||||
|
||||
_bundleManager.OnBundleDownloadProgress += ProgressHandler;
|
||||
|
||||
// Start download
|
||||
yield return _bundleManager.LoadBundleAsync(bundleName, (bundle) =>
|
||||
{
|
||||
bundleLoaded = (bundle != null);
|
||||
});
|
||||
|
||||
_bundleManager.OnBundleDownloadProgress -= ProgressHandler;
|
||||
|
||||
if (!bundleLoaded)
|
||||
{
|
||||
Debug.LogError($"[DownloadManager] Failed to download bundle: {bundleName}");
|
||||
downloadSuccess = false;
|
||||
break;
|
||||
}
|
||||
|
||||
Debug.Log($"[DownloadManager] ✓ Bundle '{bundleName}' ready!");
|
||||
}
|
||||
|
||||
if (!downloadSuccess)
|
||||
{
|
||||
HandleDownloadFailed("Failed to download game content");
|
||||
onComplete?.Invoke(false);
|
||||
OnDownloadComplete?.Invoke(false);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Step 5: Success!
|
||||
UpdateUI(1f, "Download complete!", downloadHints);
|
||||
Debug.Log("[DownloadManager] ✓ All bundles downloaded and cached successfully!");
|
||||
|
||||
// Show success for minimum display time
|
||||
float totalElapsed = Time.time - startTime;
|
||||
if (totalElapsed < minDisplayTime)
|
||||
{
|
||||
yield return new WaitForSeconds(minDisplayTime - totalElapsed);
|
||||
}
|
||||
|
||||
_downloadComplete = true;
|
||||
_isDownloading = false;
|
||||
onComplete?.Invoke(true);
|
||||
OnDownloadComplete?.Invoke(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the bundle system (downloads catalog)
|
||||
/// </summary>
|
||||
private IEnumerator InitializeBundleSystem()
|
||||
{
|
||||
Debug.Log("[DownloadManager] Initializing bundle system...");
|
||||
UpdateUI(0.05f, "Connecting to server", downloadHints);
|
||||
|
||||
yield return _bundleManager.InitializeAsync();
|
||||
|
||||
if (_bundleManager.IsInitialized)
|
||||
{
|
||||
Debug.Log("[DownloadManager] ✓ Bundle system initialized");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if all required bundles are already cached
|
||||
/// </summary>
|
||||
private bool CheckIfBundlesCached()
|
||||
{
|
||||
foreach (string bundleName in bundlesToPreload)
|
||||
{
|
||||
if (!_bundleManager.IsBundleCached(bundleName))
|
||||
{
|
||||
Debug.Log($"[DownloadManager] Bundle '{bundleName}' not cached - download required");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update UI elements with progress and hints
|
||||
/// </summary>
|
||||
private void UpdateUI(float progress, string statusText, string[] hints)
|
||||
{
|
||||
if (loadingSlider != null)
|
||||
{
|
||||
loadingSlider.value = progress;
|
||||
}
|
||||
|
||||
if (loadingText != null)
|
||||
{
|
||||
loadingText.text = statusText;
|
||||
}
|
||||
|
||||
if (hintsText != null && hints != null && hints.Length > 0)
|
||||
{
|
||||
// Show random hint or cycle through them
|
||||
int hintIndex = Mathf.FloorToInt(progress * hints.Length);
|
||||
hintIndex = Mathf.Clamp(hintIndex, 0, hints.Length - 1);
|
||||
hintsText.text = hints[hintIndex];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle no internet connection scenario
|
||||
/// </summary>
|
||||
private void HandleNoInternet()
|
||||
{
|
||||
Debug.LogWarning("[DownloadManager] ⚠️ No internet connection!");
|
||||
|
||||
if (loadingText != null)
|
||||
{
|
||||
loadingText.text = "No Internet Connection";
|
||||
}
|
||||
|
||||
if (hintsText != null)
|
||||
{
|
||||
hintsText.text = "Please connect to the internet to download game content";
|
||||
}
|
||||
|
||||
if (noInternetPanel != null)
|
||||
{
|
||||
noInternetPanel.SetActive(true);
|
||||
}
|
||||
|
||||
// Don't proceed - block user until they have internet
|
||||
_isDownloading = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle download failure
|
||||
/// </summary>
|
||||
private void HandleDownloadFailed(string errorMessage)
|
||||
{
|
||||
Debug.LogError($"[DownloadManager] Download failed: {errorMessage}");
|
||||
|
||||
if (loadingText != null)
|
||||
{
|
||||
loadingText.text = "Download Failed";
|
||||
}
|
||||
|
||||
if (hintsText != null)
|
||||
{
|
||||
hintsText.text = errorMessage;
|
||||
}
|
||||
|
||||
_isDownloading = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if download is complete and successful
|
||||
/// </summary>
|
||||
public bool IsDownloadComplete()
|
||||
{
|
||||
return _downloadComplete;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retry button handler (for no internet panel)
|
||||
/// </summary>
|
||||
public void RetryDownload()
|
||||
{
|
||||
if (noInternetPanel != null)
|
||||
{
|
||||
noInternetPanel.SetActive(false);
|
||||
}
|
||||
|
||||
// Restart the download process
|
||||
StartCoroutine(StartDownloadProcess());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get cache status for UI display
|
||||
/// </summary>
|
||||
public string GetCacheStatus()
|
||||
{
|
||||
if (!_bundleManager.IsInitialized)
|
||||
{
|
||||
return "Not initialized";
|
||||
}
|
||||
|
||||
int cachedCount = 0;
|
||||
foreach (string bundleName in bundlesToPreload)
|
||||
{
|
||||
if (_bundleManager.IsBundleCached(bundleName))
|
||||
{
|
||||
cachedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cachedCount == bundlesToPreload.Length)
|
||||
{
|
||||
return "All content cached (Offline play available)";
|
||||
}
|
||||
else if (cachedCount > 0)
|
||||
{
|
||||
return $"{cachedCount}/{bundlesToPreload.Length} bundles cached";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "Download required";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89228db1021bc63ddaafaef772de5318
|
||||
181
Assets/Scripts/AssetBundleSystem/CustomAssetBundleSceneLoader.cs
Normal file
181
Assets/Scripts/AssetBundleSystem/CustomAssetBundleSceneLoader.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using CustomAssetBundleSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Example integration: Replace Addressables with custom AssetBundle system
|
||||
/// Drop-in replacement for your existing SceneLoader
|
||||
/// </summary>
|
||||
public class CustomAssetBundleSceneLoader : MonoBehaviour
|
||||
{
|
||||
[Header("UI References (Optional)")]
|
||||
public Slider progressSlider;
|
||||
public Text statusText;
|
||||
|
||||
[Header("Settings")]
|
||||
public bool initializeOnStart = true;
|
||||
|
||||
private bool isInitialized = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (initializeOnStart)
|
||||
{
|
||||
StartCoroutine(InitializeAndLoadGameScene());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the AssetBundle system and load Game scene
|
||||
/// Call this instead of Addressables.InitializeAsync()
|
||||
/// </summary>
|
||||
public IEnumerator InitializeAndLoadGameScene()
|
||||
{
|
||||
UpdateStatus("Initializing AssetBundle system...");
|
||||
|
||||
// Initialize the catalog
|
||||
yield return RemoteAssetBundleManager.Instance.InitializeAsync();
|
||||
|
||||
if (!RemoteAssetBundleManager.Instance.IsInitialized)
|
||||
{
|
||||
UpdateStatus("✗ Failed to initialize AssetBundle system");
|
||||
Debug.LogError("[CustomAssetBundleLoader] Initialization failed!");
|
||||
yield break;
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
UpdateStatus("✓ AssetBundle system initialized");
|
||||
|
||||
// Optional: Show catalog info
|
||||
var catalog = RemoteAssetBundleManager.Instance.GetCatalog();
|
||||
Debug.Log($"[CustomAssetBundleLoader] Catalog version: {catalog.version}");
|
||||
Debug.Log($"[CustomAssetBundleLoader] Available bundles: {catalog.bundles.Count}");
|
||||
|
||||
// Load the Game scene
|
||||
yield return LoadGameScene();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the Game scene from remote AssetBundle
|
||||
/// </summary>
|
||||
public IEnumerator LoadGameScene()
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
Debug.LogError("[CustomAssetBundleLoader] Not initialized! Call InitializeAndLoadGameScene() first.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
UpdateStatus("Loading Game scene...");
|
||||
|
||||
bool sceneLoaded = false;
|
||||
|
||||
// Subscribe to download progress
|
||||
RemoteAssetBundleManager.Instance.OnBundleDownloadProgress += (bundleName, progress) =>
|
||||
{
|
||||
UpdateProgress(progress);
|
||||
UpdateStatus($"Downloading {bundleName}: {progress * 100:F1}%");
|
||||
};
|
||||
|
||||
// Load the scene
|
||||
yield return RemoteAssetBundleManager.Instance.LoadSceneAsync("Game", (success) =>
|
||||
{
|
||||
sceneLoaded = success;
|
||||
});
|
||||
|
||||
if (sceneLoaded)
|
||||
{
|
||||
UpdateStatus("✓ Game scene loaded!");
|
||||
Debug.Log("[CustomAssetBundleLoader] ✓ Game scene loaded successfully!");
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateStatus("✗ Failed to load Game scene");
|
||||
Debug.LogError("[CustomAssetBundleLoader] ✗ Failed to load Game scene!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load any scene by name
|
||||
/// </summary>
|
||||
public void LoadScene(string sceneName)
|
||||
{
|
||||
StartCoroutine(LoadSceneCoroutine(sceneName));
|
||||
}
|
||||
|
||||
private IEnumerator LoadSceneCoroutine(string sceneName)
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
Debug.LogError("[CustomAssetBundleLoader] Not initialized!");
|
||||
yield break;
|
||||
}
|
||||
|
||||
UpdateStatus($"Loading {sceneName}...");
|
||||
|
||||
bool success = false;
|
||||
yield return RemoteAssetBundleManager.Instance.LoadSceneAsync(sceneName, (loaded) =>
|
||||
{
|
||||
success = loaded;
|
||||
});
|
||||
|
||||
if (success)
|
||||
{
|
||||
UpdateStatus($"✓ {sceneName} loaded!");
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateStatus($"✗ Failed to load {sceneName}");
|
||||
}
|
||||
}
|
||||
|
||||
#region UI Helpers
|
||||
|
||||
private void UpdateProgress(float progress)
|
||||
{
|
||||
if (progressSlider != null)
|
||||
{
|
||||
progressSlider.value = progress;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatus(string message)
|
||||
{
|
||||
if (statusText != null)
|
||||
{
|
||||
statusText.text = message;
|
||||
}
|
||||
Debug.Log($"[CustomAssetBundleLoader] {message}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API - Replace your existing SceneLoader calls
|
||||
|
||||
/// <summary>
|
||||
/// Check if the system is ready (replaces Addressables initialization check)
|
||||
/// </summary>
|
||||
public bool IsReady()
|
||||
{
|
||||
return RemoteAssetBundleManager.Instance.IsInitialized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get memory usage info
|
||||
/// </summary>
|
||||
public string GetMemoryInfo()
|
||||
{
|
||||
return RemoteAssetBundleManager.Instance.GetMemoryStats();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unload all bundles to free memory
|
||||
/// </summary>
|
||||
public void UnloadAllBundles()
|
||||
{
|
||||
RemoteAssetBundleManager.Instance.UnloadAllBundles(true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5a2bd36bf588caa4835bea95fcc3985
|
||||
663
Assets/Scripts/AssetBundleSystem/RemoteAssetBundleManager.cs
Normal file
663
Assets/Scripts/AssetBundleSystem/RemoteAssetBundleManager.cs
Normal file
@@ -0,0 +1,663 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17480a2e38229ac01a32a1cc37aa82a0
|
||||
85
Assets/Scripts/AssetBundleSystem/RemoteAssetCatalog.cs
Normal file
85
Assets/Scripts/AssetBundleSystem/RemoteAssetCatalog.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace CustomAssetBundleSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple JSON catalog structure for remote AssetBundles
|
||||
/// No Unity Addressables bugs, full control, lightweight
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class RemoteAssetCatalog
|
||||
{
|
||||
public string version = "1.0.0";
|
||||
public long timestamp;
|
||||
public string baseUrl = "https://addressable.uncaptured.co/AssetBundles/Android";
|
||||
public List<BundleEntry> bundles = new List<BundleEntry>();
|
||||
|
||||
[Serializable]
|
||||
public class BundleEntry
|
||||
{
|
||||
public string bundleName; // e.g., "gamescene"
|
||||
public string fileName; // e.g., "gamescene.bundle"
|
||||
public string hash; // MD5/CRC for verification
|
||||
public long size; // File size in bytes
|
||||
public List<string> scenes; // Scenes contained in this bundle
|
||||
public List<string> dependencies; // Other bundles this depends on
|
||||
|
||||
public string GetFullUrl(string baseUrl)
|
||||
{
|
||||
return $"{baseUrl}/{fileName}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find bundle entry by scene name
|
||||
/// </summary>
|
||||
public BundleEntry FindBundleByScene(string sceneName)
|
||||
{
|
||||
foreach (var bundle in bundles)
|
||||
{
|
||||
if (bundle.scenes != null && bundle.scenes.Contains(sceneName))
|
||||
{
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find bundle entry by bundle name
|
||||
/// </summary>
|
||||
public BundleEntry FindBundleByName(string bundleName)
|
||||
{
|
||||
return bundles.Find(b => b.bundleName == bundleName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all dependencies recursively
|
||||
/// </summary>
|
||||
public List<BundleEntry> GetAllDependencies(BundleEntry bundle, HashSet<string> visited = null)
|
||||
{
|
||||
if (visited == null) visited = new HashSet<string>();
|
||||
var result = new List<BundleEntry>();
|
||||
|
||||
if (bundle.dependencies == null || bundle.dependencies.Count == 0)
|
||||
return result;
|
||||
|
||||
foreach (var depName in bundle.dependencies)
|
||||
{
|
||||
if (visited.Contains(depName)) continue;
|
||||
visited.Add(depName);
|
||||
|
||||
var depBundle = FindBundleByName(depName);
|
||||
if (depBundle != null)
|
||||
{
|
||||
result.Add(depBundle);
|
||||
result.AddRange(GetAllDependencies(depBundle, visited));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b442d4ceb7eb5b9a78189a4b3dedd9bf
|
||||
Reference in New Issue
Block a user