Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Scene Bootstrapper Architecture

Discussion in 'Scripting' started by KarimAbdelHamid, Mar 7, 2021.

  1. KarimAbdelHamid

    KarimAbdelHamid

    Joined:
    May 1, 2018
    Posts:
    12
    I've created a Persistent Scene, which:
    - On Awake, loads the Main Menu scene
    - When you press Start Game, unloads the main menu + loads the HUD and specific level scene

    This works well enough, but I'm tired of having to go back to the Persistent Scene every time I want to start the game properly, and just want to be able to start directly from the level scene I'm working on.

    I have a solution in my head, but I want to know if anyone has a better solution, or if the solution I have is what they used.
    - Have a Bootstrapper GameObject in every level scene
    - The Bootstrapper loads a modified persistent scene with a different persistent script, which does not force loading to main menu.
    - The Persistent Script sets its own scene as the active one, sets the other loaded additively, and loads the UI/HUD scene.
    - The Persistent Script then sets up the relevant Game Managers, etc.

    This is the solution I've got so far, but I can't find anything useful on Google. Everything about the Persistent Scene technique just explains that without a fix for the issue I have. Thanks for your help.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,336
    You can cause the editor to start playing in any arbitrary scene you like... hie thee to google to learn how.

    Here's one set of answers:

    https://answers.unity.com/questions/441246/editor-script-to-make-play-always-jump-to-a-start.html

    Scroll down to the one called
    PlayFromTheFirstScene
    about halfway down.

    ALSO... another super-cool thing is additive scene loading (and unloading) so you can chop your scenes up into small logical parts.

    https://forum.unity.com/threads/right-way-for-performance-divide-scene.1023673/#post-6630961
    https://forum.unity.com/threads/right-way-for-performance-divide-scene.1023673/#post-6754330

    https://forum.unity.com/threads/problem-with-canvas-ui-prefabs.1039075/#post-6726169

    A multi-scene loader thingy:

    https://pastebin.com/Vecczt5Q

    My typical Scene Loader:

    https://gist.github.com/kurtdekker/862da3bc22ee13aff61a7606ece6fdd3

    Other notes on additive scene loading:

    https://forum.unity.com/threads/removing-duplicates-on-load-scene.956568/#post-6233406

    Timing of scene loading:

    https://forum.unity.com/threads/fun...ject-in-the-second-scene.993141/#post-6449718
     
  3. KarimAbdelHamid

    KarimAbdelHamid

    Joined:
    May 1, 2018
    Posts:
    12
    Thanks Kurt, I really appreciate your thorough reply. I've already been using additive scene loading. My main concern is that I kind of want to just inject the scene that manages everything when I'm playtesting another scene. Or load that scene and redirect it to the current scene. I don't just want to go back to the main menu when I'm testing other scenes because this slows things down. That's why I was trying to find a way to go to a different scene (or load it additively and set it as the main one), and then come back to the scene the designer started on.
     
  4. KarimAbdelHamid

    KarimAbdelHamid

    Joined:
    May 1, 2018
    Posts:
    12
    Here's the way I ended up doing it. If anyone has any better ideas that would be great!
    Bootstrapper.cs (found in any level scene):
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.SceneManagement;
    3.  
    4. namespace ArcheryGame {
    5.     public class Bootstrapper : MonoBehaviour {
    6.         public void Awake() {
    7.             int persistentSceneIndex = (int)Scenes.PERSISTENT_SCENE;
    8.             if (!SceneManager.GetSceneByBuildIndex(persistentSceneIndex).isLoaded) {
    9.                 SceneManager.LoadScene(persistentSceneIndex, LoadSceneMode.Additive);
    10.             }
    11.         }
    12.     }
    13. }
    PersistentManager.cs (found in the persistent scene, which includes all managers)
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.SceneManagement;
    5.  
    6. namespace MyGame {
    7.     public class PersistentManager : MonoBehaviour {
    8.         [SerializeField]
    9.         private GameObject loadingScreen;
    10.         private List<AsyncOperation> scenesLoading = new List<AsyncOperation>();
    11.  
    12.         private void Awake() {
    13.             Scene activeScene = SceneManager.GetActiveScene();
    14.             if (activeScene.buildIndex == (int)Scenes.PERSISTENT_SCENE) {
    15.                 // Fresh start, go to main menu scene
    16.                 SceneManager.LoadScene((int)Scenes.MAIN_MENU_SCENE, LoadSceneMode.Additive);
    17.             }
    18.             else {
    19.                 // The persistent scene has been loaded from another scene
    20.                 BootstrappedFromScene();
    21.             }
    22.         }
    23.  
    24.         private void BootstrappedFromScene() {
    25.             GetComponent<Manager.PlayerManager>().SetupGame();
    26.             SceneManager.LoadScene((int)Scenes.HUD_SCENE, LoadSceneMode.Additive);
    27.             loadingScreen.SetActive(false);
    28.         }
    29.  
    30.         public void HostGame() {
    31.             loadingScreen.SetActive(true);
    32.  
    33.             int sceneId = (int)Scenes.SANDBOX_SCENE;
    34.  
    35.             scenesLoading.Add(SceneManager.UnloadSceneAsync((int)Scenes.MAIN_MENU_SCENE));
    36.             scenesLoading.Add(SceneManager.LoadSceneAsync((int)Scenes.HUD_SCENE, LoadSceneMode.Additive));
    37.             scenesLoading.Add(SceneManager.LoadSceneAsync(sceneId, LoadSceneMode.Additive));
    38.  
    39.             StartCoroutine(GetSceneLoadProgress(sceneId));
    40.         }
    41.  
    42.         public void LeaveGame() {
    43.             int sceneId = (int)Scenes.SANDBOX_SCENE;
    44.  
    45.             scenesLoading.Add(SceneManager.LoadSceneAsync((int)Scenes.MAIN_MENU_SCENE, LoadSceneMode.Additive));
    46.             scenesLoading.Add(SceneManager.UnloadSceneAsync((int)Scenes.HUD_SCENE));
    47.             scenesLoading.Add(SceneManager.UnloadSceneAsync(sceneId));
    48.          
    49.             int activeScene = (int)Scenes.MAIN_MENU_SCENE;
    50.             StartCoroutine(GetSceneLoadProgress(activeScene));
    51.         }
    52.  
    53.         public IEnumerator GetSceneLoadProgress(int activeSceneIndex) {
    54.             for (int i = 0; i < scenesLoading.Count; ++i) {
    55.                 while (!scenesLoading[i].isDone) {
    56.                     yield return null;
    57.                 }
    58.             }
    59.  
    60.             loadingScreen.SetActive(false);
    61.             Scene activeScene = SceneManager.GetSceneByBuildIndex(activeSceneIndex);
    62.             SceneManager.SetActiveScene(activeScene);
    63.             if (activeSceneIndex != (int)Scenes.MAIN_MENU_SCENE) {
    64.                 // Call setup game function
    65.             }
    66.         }
    67.     }
    68. }
    69.  
     
  5. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,057
    You can use the attribute
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    to execute code before the scene loads.
    Check if the bootstrap scene is loaded or not. If it isn't then store the initial scene you want to load in a variable. Set the bootstrap scene using
    EditorSceneManager.playModeStartScene
    . Then after the bootstrap is done, you can load your initial scene additionally.
     
    snailcat221 likes this.
  6. Xesk

    Xesk

    Joined:
    Dec 26, 2017
    Posts:
    15
    Kind of tried what MaskedMouse is suggesting. I'm not sure I fully understood his idea, but my approach is not working.
    As it seems,
    EditorSceneManager.playModeStartScene
    runs before
    RuntimeInitializeLoadType.BeforeSceneLoad
    , so what it ends happening is that the Bootstrapper scene is loaded twice, since that is the scene that gets cached in
    playableScenePath
    .

    Of course this would be different if the path is defined beforehand, but having to manually write the scene path that I want to load everytime is not what I'm looking for.

    Code (CSharp):
    1.  
    2. using UnityEditor;
    3. using UnityEditor.SceneManagement;
    4. using UnityEngine;
    5. using UnityEngine.SceneManagement;
    6.  
    7. namespace Ugly
    8. {
    9.     [InitializeOnLoad]
    10.     public class Bootstrapper
    11.     {
    12.         static string playableScenePath;
    13.         static Bootstrapper()
    14.         {
    15.             EditorSceneManager.playModeStartScene = AssetDatabase.LoadAssetAtPath<SceneAsset>(EditorBuildSettings.scenes[0].path);
    16.         }
    17.  
    18.         [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    19.         private static void CacheScene()
    20.         {
    21.          
    22.             playableScenePath = EditorSceneManager.GetActiveScene().path;
    23.             Debug.LogWarning("STORE SCENE" + playableScenePath);
    24.         }
    25.  
    26.         [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    27.         private static void LoadScenePostBoot()
    28.         {
    29.             SceneManager.LoadScene(playableScenePath, LoadSceneMode.Additive);
    30.             Debug.LogWarning("LOAD STORED SCENE" + playableScenePath);
    31.         }
    32.     }
    33. }
    34.  
     
  7. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,057
    I tried salvaging a bit from an old project. I think I changed it later on after my comment.
    This project uses a plugin for await extensions. So it will not work if you copy and paste it.
    But you can adapt it to make it work. You could change it to coroutines.

    But it looks like this:
    Code (CSharp):
    1. public static class BootstrapLoader
    2. {
    3.         /// Default bootscene index to load to after bootstrap is done
    4.         private static int bootSceneIndex = 1;
    5.      
    6. #if UNITY_EDITOR
    7.         [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    8.         public static void BootstrapSceneCheck()
    9.         {
    10.             // Check if bootstrap is loaded
    11.             if (IsBootstrapLoaded()) return;
    12.          
    13.             // Bootstrap is not loaded
    14.             // Store the wanted scene index
    15.             bootSceneIndex = SceneManager.GetActiveScene().buildIndex;
    16.  
    17.             // Load bootstrap scene
    18.             SceneManager.LoadScene(0, LoadSceneMode.Single);
    19.         }
    20. #endif
    21.  
    22.         private static bool IsBootstrapLoaded()
    23.         {
    24.             // Go through currently loaded scenes
    25.             for (int i = 0; i < SceneManager.sceneCount; i++)
    26.             {
    27.                 // Bootstrap scene is at build index 0
    28.                 if (SceneManager.GetSceneAt(i).buildIndex == 0)
    29.                     return true;
    30.             }
    31.  
    32.             return false;
    33.         }
    34.      
    35.         [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    36.         public static async void InitializeBootstrap()
    37.         {
    38.             try
    39.             {
    40.                 // Initialize all bootstrap components one by one
    41.                 await InitializeBootstraps();
    42.              
    43.                 // Bootstraps loaded, now load the wanted scene
    44.                 await SceneManager.LoadSceneAsync(bootSceneIndex, LoadSceneMode.Additive);
    45.                 SceneManager.SetActiveScene(SceneManager.GetSceneByBuildIndex(bootSceneIndex));
    46.             }
    47.             catch (Exception e)
    48.             {
    49.                 Debug.LogError($"[CRITICAL] Could not initialize\n{e.Message}");
    50.             }
    51.         }
    52.      
    53.         private static async Task InitializeBootstraps()
    54.         {
    55.             // Get the bootstraps from the bootstrap scene
    56.             var bootstraps = GetBootstraps();
    57.  
    58.             // Go through the bootstraps
    59.             foreach (var bootstrap in bootstraps)
    60.             {
    61.                 var stopwatch = new Stopwatch();
    62.                 // Initialize bootstrap
    63.                 Debug.Log($"[{bootstrap.gameObject.name}] Initializing -> {bootstrap.GetType().Name} - Order {bootstrap.Callorder}", bootstrap.gameObject);
    64.                 try
    65.                 {
    66.                     stopwatch.Start();
    67.                     await bootstrap.Initialize();
    68.                     stopwatch.Stop();
    69.                  
    70.                     Debug.Log($"[{bootstrap.GetType().Name}] Initialized in {stopwatch.ElapsedMilliseconds}ms");
    71.                 }
    72.                 catch (Exception e)
    73.                 {
    74.                     Debug.LogError($"Could not initialize ({bootstrap.gameObject.name}) - {bootstrap.GetType().Name}", bootstrap.gameObject);
    75.                     throw;
    76.                 }
    77.             }
    78.         }
    79.      
    80.         private static List<IBootstrap> GetBootstraps()
    81.         {
    82.             return SceneManager.GetSceneByBuildIndex(0).GetRootGameObjects()
    83.                 .Where(gameObject => gameObject != null)
    84.                 .SelectMany(gameObject => gameObject.GetComponentsInChildren<IBootstrap>())
    85.                 .OrderBy(x => x.Callorder).ToList();
    86.         }
    87. }
    88.  
    89.  
    90. public interface IBootstrap
    91. {
    92.     int Callorder { get; }
    93.     GameObject gameObject { get; }
    94.  
    95.     Task Initialize();
    96. }
    Code (CSharp):
    1. public class PreloadAssets : MonoBehaviour, IBootstrap
    2. {
    3.     public int Callorder { get; } = 1;
    4.  
    5.     public async Task Initialize()
    6.     {
    7.         var dependencyLoad = Addressables.DownloadDependenciesAsync("Preload");
    8.         await new WaitUntil(() => dependencyLoad.IsDone);
    9.     }
    10. }
     
    Last edited: Feb 3, 2023