using System; using System.Collections.Generic; using StansAssets.SceneManagement.Build; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.SceneManagement; namespace StansAssets.SceneManagement { /// /// Scene loading operation status. /// public enum OperationStatus { Unknown, Fail, Success } /// /// Scene load operation arguments. /// public struct SceneLoadOperationArgs { /// /// Loaded scene instance. /// public Scene Scene; /// /// Scene loading operation status. /// public OperationStatus Status; } /// /// Provides methods to perform additive scenes loading. /// public static class AdditiveScenesLoader { /// /// Event is fired when scene was unloaded. /// The reason why it has type and not is because when working with addressable /// ofter we have scene with no name and no metadata since those scene are not included in the build. /// /// Let us know if you would like to have more metadata inside. /// public static event Action SceneUnloaded = delegate { }; /// /// Event is fired when new scene was loaded. /// public static event Action SceneLoaded = delegate { }; static readonly List s_AdditiveScenes = new List(); static readonly List s_AdditiveScenesInstances = new List(); static readonly Dictionary s_LoadSceneOperations = new Dictionary(); static readonly Dictionary>> s_LoadSceneRequests = new Dictionary>>(); static readonly Dictionary> s_UnloadSceneCallbacks = new Dictionary>(); static AdditiveScenesLoader() { SceneManager.sceneLoaded += AdditiveSceneLoaded; SceneManager.sceneUnloaded += SceneUnloadComplete; } /// /// Load Scene Additively by it's build index. /// /// Build index of he scene to be loaded. /// Load Completed callback. /// public static IAsyncOperation LoadAdditively(int sceneBuildIndex, Action loadCompleted = null) { return LoadAdditively(string.Empty, sceneBuildIndex, loadCompleted); } /// /// Load Scene Additively by it's name. /// Method will bypass scene configuration availability check /// Name of the scene to be loaded. /// Load Completed callback. /// public static IAsyncOperation LoadAdditively(string sceneName, Action loadCompleted = null) { if (ValidateScene(sceneName) == false) { throw new ArgumentException($"Build Configuration doesn't contain scene: {sceneName}." + $"\nTo load a scene please add it to platform specific collection or Default scenes."); } if (IsSceneAddressable(sceneName)) { return LoadAddressableAdditively(sceneName, loadCompleted); } return LoadAdditively(sceneName, -1, loadCompleted); } static IAsyncOperation LoadAdditively(string sceneName, int buildIndex, Action loadCompleted = null) { if (buildIndex != -1) { // Our loaded / load requests scenes cache is using string key // so we need to make sceneName from index just to make sure our cache will work. sceneName = buildIndex.ToString(); } if (TryGetLoadedScene(sceneName, out var loadedScene)) { loadCompleted?.Invoke(new SceneLoadOperationArgs { Scene = loadedScene, Status = OperationStatus.Success }); return s_LoadSceneOperations[sceneName]; } if (!s_LoadSceneRequests.ContainsKey(sceneName)) { var callbacks = new List>(); if (loadCompleted != null) callbacks.Add(loadCompleted); var loadAsyncOperation = buildIndex != -1 ? SceneManager.LoadSceneAsync(buildIndex, LoadSceneMode.Additive) : SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); var asyncWrapper = new AsyncOperationWrapper(sceneName, loadAsyncOperation); s_LoadSceneRequests.Add(sceneName, callbacks); s_LoadSceneOperations.Add(sceneName, asyncWrapper); return asyncWrapper; } if (loadCompleted != null) { var callbacks = s_LoadSceneRequests[sceneName] ?? new List>(); callbacks.Add(loadCompleted); s_LoadSceneRequests[sceneName] = callbacks; } return s_LoadSceneOperations[sceneName]; } static IAsyncOperation LoadAddressableAdditively(string sceneName, Action loadCompleted = null) { AddressablesLogger.Log($"[ADDRESSABLES] LoadAddressableAdditively call: {sceneName}"); if (TryGetLoadedScene(sceneName, out var loadedScene)) { AddressablesLogger.Log($"[ADDRESSABLES] LoadAddressableAdditively already loaded: {sceneName}"); loadCompleted?.Invoke(new SceneLoadOperationArgs { Scene = loadedScene, Status = OperationStatus.Success }); return s_LoadSceneOperations[sceneName]; } if (!s_LoadSceneRequests.ContainsKey(sceneName)) { AddressablesLogger.Log($"[ADDRESSABLES] LoadAddressableAdditively start loading: {sceneName}"); var callbacks = new List>(); if (loadCompleted != null) callbacks.Add(loadCompleted); var loadAsyncOperation = Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Additive); var asyncWrapper = new AsyncOperationHandleWrapper(sceneName, loadAsyncOperation); asyncWrapper.OnComplete += AdditiveAddressableSceneLoaded; s_LoadSceneRequests.Add(sceneName, callbacks); s_LoadSceneOperations.Add(sceneName, asyncWrapper); return asyncWrapper; } if (loadCompleted != null) { var callbacks = s_LoadSceneRequests[sceneName] ?? new List>(); callbacks.Add(loadCompleted); s_LoadSceneRequests[sceneName] = callbacks; } AddressablesLogger.Log($"[ADDRESSABLES] LoadAddressableAdditively already loading: {sceneName}"); return s_LoadSceneOperations[sceneName]; } /// /// Use GetSceneAsyncOperation to retrieve info about scene load progress. /// /// Name of the scene. /// Additive scene or `null` if scene load was never requested. public static IAsyncOperation GetSceneAsyncOperation(string sceneName) { return s_LoadSceneOperations.ContainsKey(sceneName) ? s_LoadSceneOperations[sceneName] : null; } /// /// Use GetSceneAsyncOperation to retrieve info about scene load progress. /// /// Build index of he scene. /// Additive scene or `null` if scene load was never requested. public static IAsyncOperation GetSceneAsyncOperation(int sceneBuildIndex) { return GetSceneAsyncOperation(sceneBuildIndex.ToString()); } /// /// Unload scene. /// The scene to be loaded. /// Unload Completed callback. /// public static void Unload(Scene scene, Action unloadCompleted = null) { Unload(scene.name, unloadCompleted); } /// /// Destroys all GameObjects associated with the given Scene and removes the Scene from the SceneManager. /// Name of the scene to be loaded. /// Unload Completed callback. /// public static void Unload(string sceneName, Action unloadCompleted = null) { if (IsSceneAddressable(sceneName)) { UnloadAddressable(sceneName, unloadCompleted); } else { Unload(sceneName, -1, unloadCompleted); } } /// /// Destroys all GameObjects associated with the given Scene and removes the Scene from the SceneManager. /// Build index of he scene. /// Unload Completed callback. /// public static void Unload(int sceneBuildIndex, Action unloadCompleted = null) { Unload(string.Empty, sceneBuildIndex, unloadCompleted); } static void Unload(string sceneName, int buildIndex, Action unloadCompleted = null) { if (!s_UnloadSceneCallbacks.ContainsKey(sceneName)) { var callbacks = new List(); if (unloadCompleted != null) callbacks.Add(unloadCompleted); s_UnloadSceneCallbacks.Add(sceneName, callbacks); for (var i = 0; i < s_AdditiveScenes.Count; i++) { if (s_AdditiveScenes[i].name == sceneName) { s_AdditiveScenes.Remove(s_AdditiveScenes[i]); break; } } if (buildIndex != -1) { SceneManager.UnloadSceneAsync(buildIndex); } else { SceneManager.UnloadSceneAsync(sceneName); } } else { var callbacks = s_UnloadSceneCallbacks[sceneName]; if (unloadCompleted != null) { if (callbacks == null) callbacks = new List(); callbacks.Add(unloadCompleted); } } } static void UnloadAddressable(string sceneName, Action unloadCompleted = null) { AddressablesLogger.Log($"[ADDRESSABLES] UnloadAddressable call: {sceneName}"); if (!s_UnloadSceneCallbacks.ContainsKey(sceneName)) { AddressablesLogger.Log($"[ADDRESSABLES] UnloadAddressable unload start: {sceneName}"); var callbacks = new List(); if (unloadCompleted != null) callbacks.Add(unloadCompleted); s_UnloadSceneCallbacks.Add(sceneName, callbacks); SceneInstance sceneInstance = default; bool sceneFound = false; for (var i = 0; i < s_AdditiveScenesInstances.Count; i++) { AddressablesLogger.Log($"[ADDRESSABLES] UnloadAddressable searching Scene: {s_AdditiveScenesInstances[i].Scene.name}"); if (s_AdditiveScenesInstances[i].Scene.name == sceneName) { AddressablesLogger.Log($"[ADDRESSABLES] UnloadAddressable Scene found: {s_AdditiveScenesInstances[i].Scene.name}"); sceneFound = true; sceneInstance = s_AdditiveScenesInstances[i]; s_AdditiveScenesInstances.Remove(sceneInstance); s_AdditiveScenes.Remove(sceneInstance.Scene); break; } } AddressablesLogger.Log("[ADDRESSABLES] UnloadAddressable Addressables.UnloadSceneAsync Scene: " + (sceneInstance.Scene.name ?? "NULL")); if (sceneFound) { var addressableSceneUnloader = new AddressableSceneUnloader(sceneInstance); addressableSceneUnloader.Unload(AddressableSceneUnloaded); } else { AddressablesLogger.LogWarning(s_LoadSceneOperations.ContainsKey(sceneName) ? $"You are trying to unload {sceneName} scene, but it's loading is not complete yet!" : $"You are trying to unload {sceneName} scene, but it wasn't loaded!"); } } else { AddressablesLogger.Log($"[ADDRESSABLES] UnloadAddressable unload already started: {sceneName}"); var callbacks = s_UnloadSceneCallbacks[sceneName]; if (unloadCompleted != null) { if (callbacks == null) callbacks = new List(); callbacks.Add(unloadCompleted); } } } /// /// Method to find currently loaded scene by it's name. /// /// Scene name. /// Found scene. If methods returns `false` scene value will be set to `default`. /// Returns `true` if scene was found among currently loaded scenes, `false` otherwise. public static bool TryGetLoadedScene(string sceneName, out Scene scene) { foreach (var additivelyLoadedScene in s_AdditiveScenes) { if (additivelyLoadedScene.name == sceneName) { scene = additivelyLoadedScene; return true; } } foreach (var additivelyLoadedScene in s_AdditiveScenesInstances) { if (additivelyLoadedScene.Scene.name == sceneName) { scene = additivelyLoadedScene.Scene; return true; } } scene = default; return false; } static void AdditiveSceneLoaded(Scene scene, LoadSceneMode mode) { // Skip addressable scenes if (IsSceneAddressable(scene.name)) { return; } ProcessSceneLoadSuccess(scene); } static void AdditiveAddressableSceneLoaded(IAsyncOperation asyncOperation) { AddressablesLogger.Log($"[ADDRESSABLES] AdditiveAddressableSceneLoaded Status: {asyncOperation.Status}," + "Scene: " + (asyncOperation.Status == OperationStatus.Unknown ? asyncOperation.SceneName : "NULL")); if (asyncOperation.Status == OperationStatus.Success) { s_AdditiveScenesInstances.Add(asyncOperation.SceneInstance); ProcessSceneLoadSuccess(asyncOperation.SceneInstance.Scene); } else { ProcessSceneLoadFail(asyncOperation.SceneName); } } static void ProcessSceneLoadFail(string sceneName) { DispatchSceneLoadRequests(sceneName, new SceneLoadOperationArgs { Status = OperationStatus.Fail }); } static void ProcessSceneLoadSuccess(Scene scene) { s_AdditiveScenes.Add(scene); DispatchSceneLoadRequests(scene.name, new SceneLoadOperationArgs { Scene = scene, Status = OperationStatus.Success }); SceneLoaded.Invoke(scene, LoadSceneMode.Additive); } static void DispatchSceneLoadRequests(string sceneName, SceneLoadOperationArgs args) { if (s_LoadSceneRequests.TryGetValue(sceneName, out var callbacks)) { s_LoadSceneRequests.Remove(sceneName); foreach (var callback in callbacks) callback(args); } } static void SceneUnloadComplete(Scene scene) { // Skip addressable scenes if (IsSceneAddressable(scene.name)) { return; } ProcessSceneUnLoad(scene.name); } static void AddressableSceneUnloaded(AddressableSceneUnloaderResult result) { AddressablesLogger.Log($"[ADDRESSABLES] AddressableSceneUnloaded Status: {result.AsyncOperationHandle.Status}, Scene: {result.SceneName}"); ProcessSceneUnLoad(result.SceneName); } static void ProcessSceneUnLoad(string sceneName) { s_LoadSceneOperations.Remove(sceneName); if (s_UnloadSceneCallbacks.TryGetValue(sceneName, out var callbacks)) { s_UnloadSceneCallbacks.Remove(sceneName); foreach (var callback in callbacks) callback(); } SceneUnloaded.Invoke(new SceneInfo(sceneName)); } static bool IsSceneAddressable(string sceneName) { var conf = BuildConfigurationSettings.Instance.Configuration; if (Application.isEditor && !conf.UseAddressablesInEditor) { return false; } return BuildConfigurationSettings.Instance.Configuration.IsSceneAddressable(sceneName); } static bool ValidateScene(string sceneName) { // TODO let's reconsider the validation /* if (!Application.isEditor && BuildConfigurationSettings.Instance.HasValidConfiguration) { return BuildConfigurationSettings.Instance.Configuration.HasScene(sceneName); }*/ return true; } } static class AddressablesLogger { public static readonly bool Verbose = true; public static void Log(string msg) { if (Verbose) { Debug.Log(msg); } } public static void LogWarning(string msg) { Debug.LogWarning(msg); } } }