// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved. // This code can only be used under the standard Unity Asset Store End User License Agreement // A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms using System; using System.Collections; using System.Collections.Generic; using Doozy.Runtime.Common; using Doozy.Runtime.Common.Attributes; using Doozy.Runtime.Common.Events; using Doozy.Runtime.Common.Extensions; using Doozy.Runtime.Common.Utils; using Doozy.Runtime.Global; using Doozy.Runtime.Mody; using Doozy.Runtime.Reactor; using Doozy.Runtime.SceneManagement.ScriptableObjects; using Doozy.Runtime.Signals; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedMember.Local namespace Doozy.Runtime.SceneManagement { /// /// Loads any Scene either by scene name or scene build index and updates a Progressor to show the loading progress. /// It can also trigger a set of 'actions' when the scene started loading (at 0% load progress) and/or when the scene has been loaded (but not activated) (at 90% load progress) /// [AddComponentMenu("Doozy/Scene Management/Scene Loader")] public class SceneLoader : MonoBehaviour { #if UNITY_EDITOR [UnityEditor.MenuItem("GameObject/Doozy/Scene Management/Scene Loader", false, 9)] private static void CreateComponent(UnityEditor.MenuCommand menuCommand) { GameObjectUtils.AddToScene(false, true); } #endif public enum State { /// Idle mode Idle, /// Starting to load a scene LoadScene, /// Is in the process of loading a scene Loading, /// Has finished loading a scene, but has not activated it yet SceneLoaded, /// Is activating the loaded scene ActivatingScene } /// Stream category name public const string k_StreamCategory = "SceneManagement"; /// Stream name public const string k_StreamName = nameof(SceneLoader); public const GetSceneBy k_DefaultGetSceneBy = GetSceneBy.Name; public const LoadSceneMode k_DefaultLoadSceneMode = LoadSceneMode.Single; public const bool k_DefaultAutoSceneActivation = true; public const bool k_DefaultPreventLoadingSameScene = false; public const bool k_DefaultSelfDestructAfterSceneLoaded = false; public const float k_DefaultSceneActivationDelay = 0.2f; public const int k_DefaultBuildIndex = 0; public const string k_DefaultSceneName = ""; /// Database used to keep track of all the SceneLoaders [ClearOnReload] public static HashSet database { get; } = new HashSet(); [ClearOnReload] private static SignalStream s_stream; /// Signal stream for this component type public static SignalStream stream => s_stream ??= SignalsService.GetStream(k_StreamCategory, k_StreamName); /// Reference to the SceneManagement global settings public static SceneManagementSettings settings => SceneManagementSettings.instance; /// Debug flag public bool debug => DebugMode | settings.DebugMode; /// Enable relevant debug messages to be printed to the console public bool DebugMode; /// Invoked when a scene started loading public ModyEvent OnLoadScene = new ModyEvent(nameof(OnLoadScene)); /// /// Invoked when the scene has been loaded (the progress is at 0.9 (90%)) /// and has not been activated yet (the reset 0.1 (10%)). /// /// When loading a scene, Unity first loads the scene (load progress from 0% to 90%) /// and then activates it (load progress from 90% to 100%). It's a two state process. /// /// This action is triggered after the scene has been loaded /// and before its activation (at 90% load progress) /// public ModyEvent OnSceneLoaded = new ModyEvent(nameof(OnSceneLoaded)); /// /// Invoked after a scene was loaded and then activated. /// /// When loading a scene, Unity first loads the scene (load progress from 0% to 90%) /// and then activates it (load progress from 90% to 100%). It's a two state process. /// /// This action is triggered after the scene has been loaded and activated. /// public ModyEvent OnSceneActivated = new ModyEvent(nameof(OnSceneActivated)); /// Event triggered when an async operation is running and its progress has been updated. public FloatEvent OnProgressChanged = new FloatEvent(); /// Keeps track and manages the asyncOperation started when the scene loader begins to load a scene public AsyncOperation currentAsyncOperation { get; private set; } [SerializeField] private bool AllowSceneActivation = k_DefaultAutoSceneActivation; /// /// Allow Scenes to be activated as soon as it is ready. /// When loading a scene, Unity first loads the scene (load progress from 0% to 90%) and then activates it (load progress from 90% to 100%). It's a two state process. /// This option can stop the scene activation (at 90% load progress), after the scene has been loaded and is ready. /// Useful if you need to load several scenes at once and activate them in a specific order and/or at a specific time. /// public bool allowSceneActivation { get => AllowSceneActivation; set => AllowSceneActivation = value; } [SerializeField] private bool PreventLoadingSameScene = k_DefaultPreventLoadingSameScene; /// Prevent loading a scene that is already loaded. public bool preventLoadingSameScene { get => PreventLoadingSameScene; set => PreventLoadingSameScene = value; } /// Determines what load method this SceneLoader will use by default if the load scene method is called without any parameters public GetSceneBy GetSceneBy = k_DefaultGetSceneBy; /// Determines how the new scene is loaded by this SceneLoader if the load scene method is called without any parameters public LoadSceneMode LoadSceneMode = k_DefaultLoadSceneMode; [SerializeField] private List Progressors; /// Progressors updated when the scene loader progress changes. public List progressors => Progressors ?? (Progressors = new List()); /// If an async operation is running, it returns the current load progress (float between 0 and 1) public float progress { get => m_Progress; private set { // Debugger.Log($"progress: {value}"); m_Progress = value; progressors.ForEach(p => p.PlayToProgress(value)); OnProgressChanged?.Invoke(value); } } /// /// Sets for how long will the SceneLoader wait, after a scene has been loaded, before it starts the scene activation process (works only if AllowSceneActivation is enabled). /// /// When loading a scene, Unity first loads the scene (load progress from 0% to 90%) and then activates it (load progress from 90% to 100%). It's a two state process. /// /// This delay is after the scene has been loaded and before its activation (at 90% load progress) /// public float SceneActivationDelay = k_DefaultSceneActivationDelay; /// Index of the Scene in the Build Settings to load (when GetSceneBy is set to GetSceneBy.BuildIndex) public int SceneBuildIndex = k_DefaultBuildIndex; /// Name or path of the Scene to load (when GetSceneBy is set to GetSceneBy.Name) public string SceneName = k_DefaultSceneName; /// Mark this SceneLoader to self destruct (to destroy itself) after it loads a Scene public bool SelfDestructAfterSceneLoaded = k_DefaultSelfDestructAfterSceneLoaded; private State m_CurrentState = State.Idle; /// Current state the SceneLoader is in public State currentState { get => m_CurrentState; private set { bool stateChanged = m_CurrentState != value; m_CurrentState = value; if (stateChanged) stream?.SendSignal(new SceneLoaderSignalData(this)); } } private bool m_LoadInProgress; //keeps track if a scene load process is currently running private bool m_SceneLoadedAndReady; //mark that the scene has not been loaded (load progress has not reached 90%) private bool m_ActivatingScene; //TRUE when a scene is being activated private float m_SceneLoadedAndReadyTime; // private float m_Progress; //updated when an async operation is running (float between 0 and 1) private void Awake() => database.Add(this); private void OnEnable() { database.Remove(null); ResetProgress(); } private void OnDestroy() { database.Remove(null); database.Remove(this); } private void Update() { if (currentAsyncOperation == null) return; float calculatedProgress = Mathf.Clamp01(currentAsyncOperation.progress / 0.9f); //update load progress [0, 0.9] > [0, 1] if (Math.Abs(progress - calculatedProgress) > 0.0001f) progress = calculatedProgress; if (debug && !m_ActivatingScene & !m_SceneLoadedAndReady) Log($"Load progress: {Mathf.Round(progress * 100)}%"); if (!m_SceneLoadedAndReady & !m_ActivatingScene) currentState = State.Loading; // ReSharper disable once CompareOfFloatsByEqualityOperator if (!m_SceneLoadedAndReady && currentAsyncOperation.progress == 0.9f) // Loading completed { if (debug) Log($"Scene finished loading and is ready to be activated."); OnSceneLoaded?.Execute(); currentState = State.SceneLoaded; m_SceneLoadedAndReady = true; //mark that the scene has been loaded and is now ready to be activated (bool needed to stop LoadBehavior.OnSceneLoaded.Invoke(gameObject) from executing more than once) m_SceneLoadedAndReadyTime = Time.realtimeSinceStartup; } if (m_SceneLoadedAndReady && !m_ActivatingScene && AllowSceneActivation) { if (SceneActivationDelay < 0) SceneActivationDelay = 0; //sanity check if (SceneActivationDelay >= 0 && Time.realtimeSinceStartup - m_SceneLoadedAndReadyTime > SceneActivationDelay) ActivateLoadedScene(); } if (m_ActivatingScene) currentState = State.ActivatingScene; if (!currentAsyncOperation.isDone) return; if (debug) Log($"Loaded scene has been activated."); OnSceneActivated?.Execute(); m_LoadInProgress = false; currentAsyncOperation = null; currentState = State.Idle; if (SelfDestructAfterSceneLoaded) Coroutiner.Start(SelfDestruct()); } /// /// Check if the scene with the given name or path is loaded. /// /// Scene name public static bool IsSceneLoaded(string sceneName) { for (int i = 0; i < SceneManager.sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); if (scene.name == sceneName) return true; } return false; } /// /// Check if the scene with the given build index is loaded. /// /// Scene build index public static bool IsSceneLoaded(int sceneBuildIndex) { for (int i = 0; i < SceneManager.sceneCount; i++) { Scene scene = SceneManager.GetSceneAt(i); if (scene.buildIndex == sceneBuildIndex) return true; } return false; } /// /// Activate the current loaded scene. /// /// Works only if the SceneLoader has loaded a scene and its AllowSceneActivation option is set to false. /// /// This method enables the 'allowSceneActivation' for the CurrentAsyncOperation that has been paused at 90%. /// /// When loading a scene, Unity first loads the scene (load progress from 0% to 90%) and then activates it (load progress from 90% to 100%). It's a two state process. /// /// This method is meant to be used for after the scene has been loaded and before its activation (at 90% load progress). /// public SceneLoader ActivateLoadedScene() { if (currentAsyncOperation == null) return this; //no load process is running if (debug) Log($"Activating Scene..."); m_ActivatingScene = true; currentState = State.ActivatingScene; currentAsyncOperation.allowSceneActivation = true; return this; } /// Loads the Scene, with the current settings public SceneLoader LoadScene() { // ReSharper disable once SwitchStatementMissingSomeCases switch (GetSceneBy) { case GetSceneBy.Name: if (preventLoadingSameScene && IsSceneLoaded(SceneName)) return this; SceneManager.LoadScene(SceneName, LoadSceneMode); break; case GetSceneBy.BuildIndex: if (preventLoadingSameScene && IsSceneLoaded(SceneBuildIndex)) return this; SceneManager.LoadScene(SceneBuildIndex, LoadSceneMode); break; } return this; } /// Loads the Scene, with the current settings, asynchronously in the background public SceneLoader LoadSceneAsync() { // ReSharper disable once SwitchStatementMissingSomeCases switch (GetSceneBy) { case GetSceneBy.Name: if (preventLoadingSameScene && IsSceneLoaded(SceneName)) return this; LoadSceneAsync(SceneName, LoadSceneMode); break; case GetSceneBy.BuildIndex: if (preventLoadingSameScene && IsSceneLoaded(SceneBuildIndex)) return this; LoadSceneAsync(SceneBuildIndex, LoadSceneMode); break; } return this; } /// Loads a Scene asynchronously in the background, by its index in Build Settings /// Index, in the Build Settings, of the Scene to load /// If LoadSceneMode.Single then all current Scenes will be unloaded before activating the newly loaded scene public SceneLoader LoadSceneAsync(int sceneBuildIndex, LoadSceneMode mode) { if (preventLoadingSameScene && IsSceneLoaded(sceneBuildIndex)) return this; currentAsyncOperation = SceneManager.LoadSceneAsync(sceneBuildIndex, mode); StartSceneLoad(); return this; } /// Loads a Scene asynchronously in the background, by its name in Build Settings /// Name or path of the Scene to load /// If LoadSceneMode.Single then all current Scenes will be unloaded before activating the newly loaded scene public SceneLoader LoadSceneAsync(string sceneName, LoadSceneMode mode) { if (preventLoadingSameScene && IsSceneLoaded(sceneName)) return this; currentAsyncOperation = SceneManager.LoadSceneAsync(sceneName, mode); StartSceneLoad(); return this; } /// Loads the given scene asynchronously in the background. /// Scene to load /// If LoadSceneMode.Single then all current Scenes will be unloaded before activating the newly loaded scene public SceneLoader LoadSceneAsync(Scene scene, LoadSceneMode mode) { if (preventLoadingSameScene && IsSceneLoaded(scene.name)) return this; currentAsyncOperation = SceneManager.LoadSceneAsync(scene.name, mode); StartSceneLoad(); return this; } /// Loads a Scene asynchronously in the background, by its index in Build Settings, with the LoadSceneMode.Additive setting /// Index, in the Build Settings, of the Scene to load public SceneLoader LoadSceneAsyncAdditive(int sceneBuildIndex) => LoadSceneAsync(sceneBuildIndex, LoadSceneMode.Additive); /// Loads a Scene asynchronously in the background, by its name in Build Settings, with the LoadSceneMode.Additive setting /// Name or path of the Scene to load public SceneLoader LoadSceneAsyncAdditive(string sceneName) => LoadSceneAsync(sceneName, LoadSceneMode.Additive); /// Loads the given scene asynchronously in the background, with the LoadSceneMode.Additive setting. /// Scene to load public SceneLoader LoadSceneAsyncAdditive(Scene scene) => LoadSceneAsync(scene, LoadSceneMode.Additive); /// Loads a Scene asynchronously in the background, by its index in Build Settings, with the LoadSceneMode.Single setting /// Index, in the Build Settings, of the Scene to load public SceneLoader LoadSceneAsyncSingle(int sceneBuildIndex) => LoadSceneAsync(sceneBuildIndex, LoadSceneMode.Single); /// Loads a Scene asynchronously in the background, by its name in Build Settings, with the LoadSceneMode.Single setting /// Name or path of the Scene to load public SceneLoader LoadSceneAsyncSingle(string sceneName) => LoadSceneAsync(sceneName, LoadSceneMode.Single); /// Loads the given scene asynchronously in the background, with the LoadSceneMode.Single setting. /// Scene to load public SceneLoader LoadSceneAsyncSingle(Scene scene) => LoadSceneAsync(scene, LoadSceneMode.Single); /// Set the AllowSceneActivation that that allows for a Scene to be activated as soon as it is ready /// Allow Scenes to be activated as soon as it is ready public SceneLoader SetAllowSceneActivation(bool whenReadyAllowSceneActivation) { allowSceneActivation = whenReadyAllowSceneActivation; return this; } /// Set the GetSceneBy value, that determines what load method this SceneLoader will use by default /// Load method this SceneLoader will use if the load scene method is called without any parameters public SceneLoader SetLoadSceneBy(GetSceneBy getSceneBy) { GetSceneBy = getSceneBy; return this; } /// Set the LoadSceneMode value, that determines how the new scene is loaded by this SceneLoader /// Load mode used when loading a scene public SceneLoader SetLoadSceneMode(LoadSceneMode loadSceneMode) { LoadSceneMode = loadSceneMode; return this; } /// Add a Progressor reference that will get updates when this SceneLoader loads a scene /// The Progressor that will get updates when this SceneLoader loads a scene public SceneLoader AddProgressor(Progressor progressor) { if (progressor == null) return this; progressors.RemoveNulls(); if (progressors.Contains(progressor)) return this; progressors.Add(progressor); return this; } /// Remove a Progressor reference that would get updates when this SceneLoader loads a scene /// The Progressor that would get updates when this SceneLoader loads a scene public SceneLoader RemoveProgressor(Progressor progressor) { if (progressor == null) return this; progressors.RemoveNulls(); if (!progressors.Contains(progressor)) return this; progressors.Remove(progressor); return this; } /// Clear the Progressor references that would get updates when this SceneLoader loads a scene public SceneLoader ClearProgressors() { progressors.Clear(); return this; } /// Set the activation delay that determines how long will the SceneLoader wait, after a scene has been loaded, before it starts the scene activation process (works only if AllowSceneActivation is enabled) /// How long will the SceneLoader wait, after a scene has been loaded, before it starts the scene activation process public SceneLoader SetSceneActivationDelay(float sceneActivationDelay) { SceneActivationDelay = sceneActivationDelay; return this; } /// Set the SceneBuildIndex, in the Build Settings, of the Scene to load /// Index, in the Build Settings, of the Scene to load public SceneLoader SetSceneBuildIndex(int sceneBuildIndex) { SceneBuildIndex = sceneBuildIndex; return this; } /// Set the SceneName, name or path, of the Scene to load /// Name or path of the Scene to load public SceneLoader SetSceneName(string sceneName) { SceneName = sceneName; return this; } /// Set this SceneLoader to self destruct (to destroy itself) after it loads a Scene /// Should this SceneLoader self destruct? public SceneLoader SetSelfDestructAfterSceneLoaded(bool selfDestruct) { SelfDestructAfterSceneLoaded = selfDestruct; return this; } /// Sets the scene load Progress to zero private void ResetProgress() { progressors.RemoveNulls(); progressors.ForEach(p => p.SetProgressAtZero()); progress = 0; } private void StartSceneLoad() { ResetProgress(); OnLoadScene?.Execute(); currentState = State.LoadScene; currentAsyncOperation.allowSceneActivation = false; //update the scene activation mode m_LoadInProgress = true; //mark that a scene load process is running m_SceneLoadedAndReady = false; //mark that the scene has not been loaded (load progress has not reached 90%) m_ActivatingScene = false; } private IEnumerator AsynchronousLoad(string sceneName, LoadSceneMode mode) { // yield return null; ResetProgress(); OnLoadScene?.Execute(); currentAsyncOperation = SceneManager.LoadSceneAsync(sceneName, mode); if (currentAsyncOperation == null) yield break; currentAsyncOperation.allowSceneActivation = false; //update the scene activation mode m_LoadInProgress = true; //mark that a scene load process is running bool sceneLoadedAndReady = false; //mark that the scene has not been loaded (load progress has not reached 90%) bool activatingScene = false; // while (!currentAsyncOperation.isDone) while (m_LoadInProgress) { //if (currentAsyncOperation == null) yield break; // [0, 0.9] > [0, 1] progress = Mathf.Clamp01(currentAsyncOperation.progress / 0.9f); //update load progress if (debug && !activatingScene) Log($"Load progress: {Mathf.Round(progress * 100)}%"); // Loading completed // ReSharper disable once CompareOfFloatsByEqualityOperator if (!sceneLoadedAndReady && currentAsyncOperation.progress == 0.9f) { // progress = 1f; if (debug) Log($"Scene is ready to be activated."); OnSceneLoaded.Execute(); sceneLoadedAndReady = true; //mark that the scene has been loaded and is now ready to be activated (bool needed to stop LoadBehavior.OnSceneLoaded.Invoke(gameObject) from executing more than once) } if (sceneLoadedAndReady && !activatingScene) { if (SceneActivationDelay < 0) SceneActivationDelay = 0; //sanity check if (SceneActivationDelay > 0) yield return new WaitForSecondsRealtime(SceneActivationDelay); if (AllowSceneActivation) { ActivateLoadedScene(); activatingScene = true; } } if (currentAsyncOperation.isDone) { if (debug) Log($"Scene has been activated."); m_LoadInProgress = false; // currentAsyncOperation = null; if (SelfDestructAfterSceneLoaded) Coroutiner.Start(SelfDestruct()); } yield return null; } } private IEnumerator AsynchronousLoad(int sceneBuildIndex, LoadSceneMode mode) { // yield return null; ResetProgress(); OnLoadScene?.Execute(); currentAsyncOperation = SceneManager.LoadSceneAsync(sceneBuildIndex, mode); if (currentAsyncOperation == null) yield break; currentAsyncOperation.allowSceneActivation = false; //update the scene activation mode m_LoadInProgress = true; //mark that a scene load process is running bool sceneLoadedAndReady = false; //mark that the scene has not been loaded (load progress has not reached 90%) bool activatingScene = false; // while (!currentAsyncOperation.isDone) while (m_LoadInProgress) { //if (currentAsyncOperation == null) yield break; // [0, 0.9] > [0, 1] progress = Mathf.Clamp01(currentAsyncOperation.progress / 0.9f); //update load progress if (debug && !activatingScene) Log($"Load progress: {Mathf.Round(progress * 100)}%"); // Loading completed // ReSharper disable once CompareOfFloatsByEqualityOperator if (!sceneLoadedAndReady && currentAsyncOperation.progress == 0.9f) { // progress = 1f; if (debug) Log($"Scene is ready to be activated."); OnSceneLoaded?.Execute(); sceneLoadedAndReady = true; //mark that the scene has been loaded and is now ready to be activated (bool needed to stop LoadBehavior.OnSceneLoaded.Invoke(gameObject) from executing more than once) } if (sceneLoadedAndReady && !activatingScene && AllowSceneActivation) { if (SceneActivationDelay < 0) SceneActivationDelay = 0; //sanity check if (SceneActivationDelay > 0) yield return new WaitForSecondsRealtime(SceneActivationDelay); ActivateLoadedScene(); activatingScene = true; } if (currentAsyncOperation.isDone) { if (debug) Log("[" + name + "] Scene has been activated."); m_LoadInProgress = false; // currentAsyncOperation = null; if (SelfDestructAfterSceneLoaded) { Coroutiner.Start(SelfDestruct()); } } yield return null; } } private IEnumerator SelfDestruct() { yield return null; Destroy(gameObject); } /// /// Activates all the loaded scenes for all the SceneLoaders that have scenes ready to be activated. /// /// A scene is ready to be activated if the load progress is at 0.9 (90%). /// public static void ActivateLoadedScenes() { if (settings.DebugMode) Log($"Activate Loaded Scenes", null); database.Remove(null); foreach (SceneLoader sceneLoader in database) sceneLoader.ActivateLoadedScene(); } /// Creates a new GameObject with a SceneLoader script attached and then returns the reference to the newly created script /// Sets a parent for the newly created GameObject public static SceneLoader GetLoader(Transform parent = null) { SceneLoader loader = new GameObject(nameof(SceneLoader)).AddComponent(); if (parent != null) { loader.transform.SetParent(parent); return loader; } DontDestroyOnLoad(loader); //make sure this game object is not destroyed when loading a new scene return loader; } private void Log(string message) => Log($"[{name}] {message}", this); private static void Log(string message, Object context) => Debugger.Log($"({nameof(SceneLoader)}) {message}", context); } }