chunk 1: core gameplay scripts scenes runtime assets

This commit is contained in:
2026-04-06 11:02:34 +03:00
parent fa0388bc79
commit 0d11a097d8
703 changed files with 2292651 additions and 0 deletions

View 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";
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 89228db1021bc63ddaafaef772de5318

View 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
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c5a2bd36bf588caa4835bea95fcc3985

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 17480a2e38229ac01a32a1cc37aa82a0

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b442d4ceb7eb5b9a78189a4b3dedd9bf