Files

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