667 lines
22 KiB
C#
667 lines
22 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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)
|
|
/// </summary>
|
|
public static class SceneLoader
|
|
{
|
|
/// <summary>
|
|
/// Wrapper abstraction that mimics AsyncOperation while backing either a SceneManager load or an Addressables handle.
|
|
/// </summary>
|
|
public sealed class SceneLoadHandle : CustomYieldInstruction
|
|
{
|
|
private readonly string _sceneName;
|
|
private readonly Action<SceneLoadHandle> _onCompleted;
|
|
private readonly LoadSceneMode _mode;
|
|
|
|
private readonly AsyncOperation _unityAsyncOp;
|
|
private AsyncOperationHandle<SceneInstance>? _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<SceneLoadHandle> onCompleted)
|
|
{
|
|
_sceneName = sceneName;
|
|
_mode = mode;
|
|
_allowSceneActivation = initialActivation;
|
|
_onCompleted = onCompleted;
|
|
}
|
|
|
|
internal SceneLoadHandle(string sceneName, LoadSceneMode mode, AsyncOperation unityAsyncOp, bool initialActivation, Action<SceneLoadHandle> 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<SceneInstance> addressableHandle, bool initialActivation, Action<SceneLoadHandle> 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<SceneInstance> 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<SceneInstance>? AddressableHandle => _addressableHandle;
|
|
|
|
private void OnAddressableLoaded(AsyncOperationHandle<SceneInstance> 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<SceneInstance> 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<IResourceLocator>? _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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<bool> EnsureAddressablesInitializedAsync()
|
|
{
|
|
// Quick check without lock first
|
|
if (_addressablesInitialized)
|
|
return true;
|
|
|
|
AsyncOperationHandle<IResourceLocator> 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<IResourceLocation> locations) && locations != null && locations.Count > 0)
|
|
{
|
|
address = candidate;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static IEnumerable<string> 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";
|
|
}
|
|
}
|