using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.ResourceLocations; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.SceneManagement; using UnityEngine.AddressableAssets.ResourceLocators; /// /// Centralized scene loader that supports: /// - Built-in scenes (always tries first) /// - Remote Addressables (old system - fallback) /// - Custom AssetBundles (new system - Editor: built-in, Android: remote bundles) /// public static class SceneLoader { /// /// Wrapper abstraction that mimics AsyncOperation while backing either a SceneManager load or an Addressables handle. /// public sealed class SceneLoadHandle : CustomYieldInstruction { private readonly string _sceneName; private readonly Action _onCompleted; private readonly LoadSceneMode _mode; private readonly AsyncOperation _unityAsyncOp; private AsyncOperationHandle? _addressableHandle; private AsyncOperation _activationOperation; private bool _allowSceneActivation; private bool _activationRequested; private bool _completed; // Constructor for pending initialization (no operation yet) internal SceneLoadHandle(string sceneName, LoadSceneMode mode, bool initialActivation, Action onCompleted) { _sceneName = sceneName; _mode = mode; _allowSceneActivation = initialActivation; _onCompleted = onCompleted; } internal SceneLoadHandle(string sceneName, LoadSceneMode mode, AsyncOperation unityAsyncOp, bool initialActivation, Action onCompleted) { _sceneName = sceneName; _mode = mode; _unityAsyncOp = unityAsyncOp; _onCompleted = onCompleted; if (_unityAsyncOp != null) { _unityAsyncOp.allowSceneActivation = initialActivation; _unityAsyncOp.completed += _ => Complete(); } else { Debug.LogError($"[SceneLoader] Unity AsyncOperation is null for '{sceneName}'."); Complete(); } } internal SceneLoadHandle(string sceneName, LoadSceneMode mode, AsyncOperationHandle addressableHandle, bool initialActivation, Action onCompleted) { _sceneName = sceneName; _mode = mode; _addressableHandle = addressableHandle; _allowSceneActivation = initialActivation; _onCompleted = onCompleted; if (!addressableHandle.IsValid()) { Debug.LogError($"[SceneLoader] Addressables handle is invalid for '{sceneName}'."); Complete(); return; } addressableHandle.Completed += OnAddressableLoaded; if (initialActivation) { TryBeginActivation(); } } // Update this handle with an Addressables operation internal void SetAddressableHandle(AsyncOperationHandle addressableHandle) { _addressableHandle = addressableHandle; if (!addressableHandle.IsValid()) { Debug.LogError($"[SceneLoader] Addressables handle is invalid for '{_sceneName}'."); Complete(); return; } addressableHandle.Completed += OnAddressableLoaded; if (_allowSceneActivation) { TryBeginActivation(); } } public override bool keepWaiting => !isDone; public string SceneName => _sceneName; public bool allowSceneActivation { get { if (_unityAsyncOp != null) { return _unityAsyncOp.allowSceneActivation; } return _allowSceneActivation; } set { if (_unityAsyncOp != null) { _unityAsyncOp.allowSceneActivation = value; return; } if (_allowSceneActivation == value) return; _allowSceneActivation = value; if (value) { TryBeginActivation(); } } } public float progress { get { if (_unityAsyncOp != null) { return _unityAsyncOp.progress; } if (!_addressableHandle.HasValue || !_addressableHandle.Value.IsValid()) return 0f; float baseProgress = _addressableHandle.Value.PercentComplete; if (_activationOperation != null) { return Mathf.Lerp(Mathf.Min(baseProgress, 0.9f), 1f, _activationOperation.progress); } if (!_allowSceneActivation && baseProgress >= 0.9f) { return 0.9f; } return baseProgress; } } public bool isDone { get { if (_unityAsyncOp != null) { return _unityAsyncOp.isDone; } if (_activationOperation != null) { return _activationOperation.isDone; } if (!_addressableHandle.HasValue || !_addressableHandle.Value.IsValid()) { return _completed; } if (!_allowSceneActivation) { return false; } var handle = _addressableHandle.Value; if (handle.Status == AsyncOperationStatus.Failed) { return true; } if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result.Scene.IsValid() && handle.Result.Scene.isLoaded) { return true; } return false; } } public AsyncOperationHandle? AddressableHandle => _addressableHandle; private void OnAddressableLoaded(AsyncOperationHandle handle) { if (handle.Status == AsyncOperationStatus.Failed) { Debug.LogError($"[SceneLoader] Addressables failed to load '{_sceneName}': {handle.OperationException?.Message}"); Complete(); return; } if (_allowSceneActivation) { BeginActivation(handle); } } private void TryBeginActivation() { if (!_addressableHandle.HasValue || !_addressableHandle.Value.IsValid()) return; if (_activationRequested) return; var handle = _addressableHandle.Value; if (handle.Status == AsyncOperationStatus.Succeeded) { BeginActivation(handle); } } private void BeginActivation(AsyncOperationHandle handle) { if (_activationRequested) return; _activationRequested = true; try { _activationOperation = handle.Result.ActivateAsync(); if (_activationOperation != null) { _activationOperation.completed += _ => { if (_mode == LoadSceneMode.Single) { SceneManager.SetActiveScene(handle.Result.Scene); } Complete(); }; } else { if (_mode == LoadSceneMode.Single) { SceneManager.SetActiveScene(handle.Result.Scene); } Complete(); } } catch (Exception ex) { Debug.LogException(ex); Complete(); } } internal void Complete() { if (_completed) return; _completed = true; _onCompleted?.Invoke(this); } } private static SceneLoadHandle _currentHandle; private static string _currentSceneName; private static bool _addressablesInitialized; private static AsyncOperationHandle? _initializationHandle; private static readonly object _addressablesInitLock = new(); // Custom AssetBundle system (new system - replaces Addressables) private static bool _customAssetBundleInitialized; public static bool IsLoading => _currentHandle != null && !_currentHandle.isDone; public static string CurrentSceneName => _currentSceneName; public static SceneLoadHandle CurrentOperation => _currentHandle; /// /// Load a scene once, preferring build scenes but falling back to custom AssetBundles or Addressables. /// For "Game" scene: Uses built-in in Editor, remote AssetBundle in Android build. /// public static SceneLoadHandle LoadSceneAsyncOnce( string sceneName, LoadSceneMode mode = LoadSceneMode.Single, bool allowSceneActivation = true) { if (string.IsNullOrEmpty(sceneName)) { Debug.LogError("SceneLoader: sceneName is null or empty."); return null; } if (IsLoading) { if (string.Equals(_currentSceneName, sceneName, StringComparison.Ordinal)) { Debug.LogWarning($"SceneLoader: '{sceneName}' is already loading. Returning existing handle."); return _currentHandle; } Debug.LogWarning($"SceneLoader: A different scene ('{_currentSceneName}') is already loading. Ignoring new request '{sceneName}'."); return null; } _currentSceneName = sceneName; // DISABLED: AssetBundle loading for Game scene // Uncomment the block below to re-enable custom AssetBundle loading on Android /* #if !UNITY_EDITOR // CRITICAL FIX: On Android, ALWAYS use custom AssetBundle system for Game scene // Don't check Application.CanStreamedLevelBeLoaded - it's unreliable! if (sceneName == "Game") { Debug.Log("[SceneLoader] Android build detected - Loading Game scene via custom AssetBundle system..."); var initHandle = new SceneLoadHandle(sceneName, mode, allowSceneActivation, OnHandleCompleted); _currentHandle = initHandle; if (GameManager.Instance != null) { GameManager.Instance.StartCoroutine(LoadGameSceneViaCustomAssetBundle(mode, allowSceneActivation, initHandle)); } else { Debug.LogError("[SceneLoader] GameManager.Instance is null! Cannot load via custom AssetBundles."); ResetState(); return null; } return initHandle; } #endif */ // 1. Try built-in scene first (works in Editor for development) if (Application.CanStreamedLevelBeLoaded(sceneName)) { var asyncOp = SceneManager.LoadSceneAsync(sceneName, mode); if (asyncOp == null) { Debug.LogError($"SceneLoader: SceneManager returned null AsyncOperation for '{sceneName}'."); ResetState(); return null; } asyncOp.allowSceneActivation = allowSceneActivation; _currentHandle = new SceneLoadHandle(sceneName, mode, asyncOp, allowSceneActivation, OnHandleCompleted); Debug.Log($"[SceneLoader] Started built-in async load for '{sceneName}'."); return _currentHandle; } // 2. For other scenes not in build: Use Addressables (fallback) Debug.Log($"[SceneLoader] Scene '{sceneName}' not in build settings."); // DISABLED: Addressables fallback - make sure all scenes are in Build Settings // Uncomment the block below to re-enable Addressables fallback loading /* #if !UNITY_EDITOR // This should not be reached for Game scene anymore (handled above) Debug.LogWarning($"[SceneLoader] Scene '{sceneName}' not found in build settings and not Game scene!"); #endif // 3. Fallback: Try Addressables for other scenes Debug.Log($"[SceneLoader] Attempting Addressables load for '{sceneName}'..."); var addressablesHandle = new SceneLoadHandle(sceneName, mode, allowSceneActivation, OnHandleCompleted); _currentHandle = addressablesHandle; if (GameManager.Instance != null) { GameManager.Instance.StartCoroutine(InitializeAndLoadAddressableScene(sceneName, mode, allowSceneActivation, addressablesHandle)); } else { Debug.LogError("[SceneLoader] GameManager.Instance is null! Cannot start coroutine for Addressables init."); ResetState(); return null; } return addressablesHandle; */ // Since AssetBundles/Addressables are disabled, log error and return null Debug.LogError($"[SceneLoader] Scene '{sceneName}' is NOT in Build Settings! Add it to Build Settings to load."); ResetState(); return null; } private static void OnHandleCompleted(SceneLoadHandle handle) { if (handle != null) { var addrHandleNullable = handle.AddressableHandle; if (addrHandleNullable.HasValue) { var addrHandle = addrHandleNullable.Value; if (addrHandle.IsValid() && addrHandle.Status == AsyncOperationStatus.Failed) { Debug.LogError($"SceneLoader: Addressables load failed for '{handle.SceneName}'."); } } } _currentHandle = null; _currentSceneName = null; } // Coroutine to load Game scene via custom AssetBundle system (Android builds only) private static IEnumerator LoadGameSceneViaCustomAssetBundle( LoadSceneMode mode, bool allowSceneActivation, SceneLoadHandle handle) { Debug.Log("[SceneLoader] Initializing custom AssetBundle system..."); // Initialize custom AssetBundle system if not already done if (!_customAssetBundleInitialized) { yield return CustomAssetBundleSystem.RemoteAssetBundleManager.Instance.InitializeAsync(); if (CustomAssetBundleSystem.RemoteAssetBundleManager.Instance.IsInitialized) { _customAssetBundleInitialized = true; Debug.Log("[SceneLoader] ✓ Custom AssetBundle system initialized!"); } else { Debug.LogError("[SceneLoader] ✗ Failed to initialize custom AssetBundle system!"); handle.Complete(); yield break; } } // Load the Game scene from AssetBundle Debug.Log("[SceneLoader] Loading Game scene from remote AssetBundle..."); bool sceneLoaded = false; yield return CustomAssetBundleSystem.RemoteAssetBundleManager.Instance.LoadSceneAsync("Game", (success) => { sceneLoaded = success; }); if (sceneLoaded) { Debug.Log("[SceneLoader] ✓ Game scene loaded successfully from AssetBundle!"); } else { Debug.LogError("[SceneLoader] ✗ Failed to load Game scene from AssetBundle!"); } // Complete the handle handle.Complete(); } // Coroutine to handle async Addressables initialization without blocking main thread private static System.Collections.IEnumerator InitializeAndLoadAddressableScene( string sceneName, LoadSceneMode mode, bool allowSceneActivation, SceneLoadHandle handle) { Debug.Log($"[SceneLoader] Coroutine: Starting async Addressables init for '{sceneName}'..."); // Start the async init task var initTask = EnsureAddressablesInitializedAsync(); // Wait until the task completes without blocking while (!initTask.IsCompleted) { yield return null; } // Check result bool initSuccess = false; try { initSuccess = initTask.Result; } catch (Exception ex) { Debug.LogError($"[SceneLoader] Addressables initialization threw exception: {ex.Message}"); Debug.LogException(ex); } if (!initSuccess) { Debug.LogError($"[SceneLoader] Addressables init failed. Cannot load '{sceneName}'."); ResetState(); yield break; } Debug.Log("[SceneLoader] Addressables initialized. Locating scene address..."); if (!TryLocateSceneAddress(sceneName, out var address)) { Debug.LogError($"[SceneLoader] Scene '{sceneName}' not found in Addressables catalog."); ResetState(); yield break; } Debug.Log($"[SceneLoader] Loading scene '{sceneName}' from address '{address}'..."); var addressableHandle = Addressables.LoadSceneAsync(address, mode, activateOnLoad: false); handle.SetAddressableHandle(addressableHandle); } private static void ResetState() { _currentHandle = null; _currentSceneName = null; } internal static async System.Threading.Tasks.Task EnsureAddressablesInitializedAsync() { // Quick check without lock first if (_addressablesInitialized) return true; AsyncOperationHandle handle; // Lock only for checking/starting initialization lock (_addressablesInitLock) { if (_addressablesInitialized) return true; if (!_initializationHandle.HasValue || !_initializationHandle.Value.IsValid()) { _initializationHandle = Addressables.InitializeAsync(); } handle = _initializationHandle.Value; if (!handle.IsValid()) { Debug.LogError("[SceneLoader] Addressables initialization returned an invalid handle."); _initializationHandle = null; return false; } } // Await OUTSIDE the lock to avoid CS1996 error try { await handle.Task; // Capture status immediately after await, BEFORE any lock or release bool succeeded = false; AsyncOperationStatus status = AsyncOperationStatus.None; string operationException = null; if (handle.IsValid()) { status = handle.Status; succeeded = status == AsyncOperationStatus.Succeeded; // Capture exception details if available if (handle.OperationException != null) { operationException = handle.OperationException.ToString(); } } // Lock again to update shared state and release lock (_addressablesInitLock) { if (handle.IsValid()) { Addressables.Release(handle); } _initializationHandle = null; if (succeeded) { _addressablesInitialized = true; Debug.Log("[SceneLoader] Addressables initialized successfully (async)."); return true; } // Log detailed failure information Debug.LogError($"[SceneLoader] Addressables initialization failed. Status: {status}"); if (!string.IsNullOrEmpty(operationException)) { Debug.LogError($"[SceneLoader] Addressables OperationException: {operationException}"); } else { Debug.LogWarning("[SceneLoader] No OperationException available. Check catalog URL, network connectivity, and Addressables build settings."); } return false; } } catch (Exception ex) { lock (_addressablesInitLock) { if (_initializationHandle.HasValue && _initializationHandle.Value.IsValid()) { Addressables.Release(_initializationHandle.Value); } _initializationHandle = null; } Debug.LogError($"[SceneLoader] Addressables async init exception: {ex.GetType().Name} - {ex.Message}"); Debug.LogError($"[SceneLoader] Full exception details:\n{ex.ToString()}"); Debug.LogException(ex); return false; } } private static bool TryLocateSceneAddress(string sceneName, out string address) { address = null; foreach (var candidate in EnumerateAddressCandidates(sceneName)) { foreach (var locator in Addressables.ResourceLocators) { if (locator.Locate(candidate, typeof(SceneInstance), out IList locations) && locations != null && locations.Count > 0) { address = candidate; return true; } } } return false; } private static IEnumerable EnumerateAddressCandidates(string sceneName) { if (string.IsNullOrWhiteSpace(sceneName)) yield break; yield return sceneName; if (!sceneName.EndsWith(".unity", StringComparison.OrdinalIgnoreCase)) { yield return sceneName + ".unity"; } yield return $"Assets/Scenes/{sceneName}.unity"; yield return $"Assets/{sceneName}.unity"; } }