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

Question Initializing a static class after each scene load?

Discussion in 'Scripting' started by anszwa, Jul 4, 2023.

  1. anszwa

    anszwa

    Joined:
    Jul 30, 2012
    Posts:
    39
    Is there a way to initialize a static class every time a new scene loads without needing an extra script to access the class?

    My concern is that I want to create an object pool in this class that is created new at the beginning of each scene. I am not entirely satisfied with the approaches I am familiar with, such as creating the pool in the constructor when calling it for the first time or call an Initialization function from an other script.

    What I found is the attribute:RuntimeInitializeOnLoadMethodAttribute, but here too I can only call the class once at the beginning of the application. Is there some similar way with calling a method after every scene switch?

    Simply put, I'm looking for a Start() Function for a class that isn't assigned to a GameObject.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,558
    There are scene-loaded delegates you can hook in the SceneManager class.

    Be careful monkeying around with this non-mainstream timing of things. You may be unpleasantly surprised with WHEN things happen. Just use lots of Debug.Log() statements to understand calling and execution order.

    Here is some timing diagram help:

    https://docs.unity3d.com/Manual/ExecutionOrder.html
     
    anszwa likes this.
  3. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    3,899
    [InitializeOnLoad] is the attribute you put above the class definition, then implement a static constructor for that class. This will run after every domain reload.

    But for scene reloads there are callbacks too, like activeSceneChanged.
     
    anszwa and Kurt-Dekker like this.
  4. anszwa

    anszwa

    Joined:
    Jul 30, 2012
    Posts:
    39
    Thanks for the answer. but does this approach not also assume that you have a script that is in the scene and inherits from monobehavior?
     
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,558
    You're not going to get away from having SOMETHING in some scene somewhere.

    That's how Unity's dependency injection works: the scene (specifically scene zero) tells Unity what classes to instantiate.

    From one tiny class you can do whatever else you need, if you insist on minimal use of the super-powerful scene mechanism.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,558
    I take it back. You can have a zero-scene game if you like.

    Code (csharp):
    1. using UnityEngine;
    2.  
    3. // @kurtdekker - for the purists who don't want ANY scenes in project
    4.  
    5. public class RunmeJohnny : MonoBehaviour
    6. {
    7.     [RuntimeInitializeOnLoadMethod]
    8.     static void StartWithNoScenes()
    9.     {
    10.         Camera cam = new GameObject("Camera").AddComponent<Camera>();
    11.         cam.transform.position = new Vector3(0, 0, -10);
    12.         cam.orthographic = false;
    13.         cam.fieldOfView = 60.0f;
    14.  
    15.         GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
    16.         var runme = cube.AddComponent<RunmeJohnny>();
    17.         runme.cube = cube;
    18.         // NOTE: the material on this script won't be included
    19.         // in the build because... you didn't have a scene!
    20.         // You would need to get a material on this cube or
    21.         // else it will be hot pink.
    22.     }
    23.  
    24.     // injected by the above
    25.     GameObject cube;
    26.  
    27.     private void Update()
    28.     {
    29.         float angle = Time.time * 100.0f;
    30.  
    31.         cube.transform.rotation = Quaternion.Euler(0, angle, 0);
    32.     }
    33. }
     
    anszwa likes this.
  7. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    280
    It is certainly possible, just not exactly straightforward, particularly for users who aren't already familiar with the traditional Unity way of doing things.
     
    anszwa likes this.
  8. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    280
    Also, it's not exactly zero scene because there will always be an active scene when the code runs, whether it be the active scene in the editor, or the lone, non-addressable scene in the build settings. So, there really is no escaping it. Even if you experiment with RuntimeInitializeOnLoadMethod order, there are times when the first scene is not loaded, but it doesn't complain if you instantiate objects, they just won't be there when the first scene in the build settings does load. So, there is always some kind of "scene" there.
     
    anszwa and Kurt-Dekker like this.
  9. anszwa

    anszwa

    Joined:
    Jul 30, 2012
    Posts:
    39
    Thank you. I don't intend to have nothing in the scene anyway. I'll explain briefly what I have in mind or already have.

    Only in the project folder:
    Code (CSharp):
    1. public static class AudioManager
    2. {
    3.     static AudioManager
    4.     {
    5.         CreateObjectPool();
    6.     }
    7.  
    8.     public static void PlaySound(Audioclip clip)
    9.     {
    10.         AudioSource source = GetSourceFromPool()
    11.         source.clip = clip;
    12.         source.Play();
    13.     }
    14. }
    In Hierarchy:
    Code (CSharp):
    1. public class Player : MonoBehaviour
    2. {
    3.     public void Update()
    4.     {
    5.         if (getHit) AudioManager.PlaySound(hitSound);
    6.  
    7.         if (isAttacking) AudioManager.PlaySound(attackSound);
    8.     }
    9. }

    At the moment the object pool is only created for me when the class is used for the first time.
    My wish would be to have a complete audio manager object pool that is just a script without any dependencies.
     
  10. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,558
    That's easy. You can do that all day long.

    ULTRA-simple static solution to a GameManager:

    https://forum.unity.com/threads/i-need-to-save-the-score-when-the-scene-resets.1168766/#post-7488068

    https://gist.github.com/kurtdekker/50faa0d78cd978375b2fe465d55b282b

    OR for a more-complex "lives as a MonoBehaviour or ScriptableObject" solution...

    Simple Singleton (UnitySingleton):

    Some super-simple Singleton examples to take and modify:

    Simple Unity3D Singleton (no predefined data):

    https://gist.github.com/kurtdekker/775bb97614047072f7004d6fb9ccce30

    Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

    https://gist.github.com/kurtdekker/2f07be6f6a844cf82110fc42a774a625

    These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance!

    The above solutions can be modified to additively load a scene instead, BUT scenes do not load until end of frame, which means your static factory cannot return the instance that will be in the to-be-loaded scene. This is a minor limitation that is simple to work around.

    If it is a GameManager, when the game is over, make a function in that singleton that Destroys itself so the next time you access it you get a fresh one, something like:

    Code (csharp):
    1. public void DestroyThyself()
    2. {
    3.    Destroy(gameObject);
    4.    Instance = null;    // because destroy doesn't happen until end of frame
    5. }
    There are also lots of Youtube tutorials on the concepts involved in making a suitable GameManager, which obviously depends a lot on what your game might need.

    OR just make a custom ScriptableObject that has the shared fields you want for the duration of many scenes, and drag references to that one ScriptableObject instance into everything that needs it. It scales up to a certain point.

    And finally there's always just a simple "static locator" pattern you can use on MonoBehaviour-derived classes, just to give global access to them during their lifecycle.

    WARNING: this does NOT control their uniqueness.

    WARNING: this does NOT control their lifecycle.

    Code (csharp):
    1. public static MyClass Instance { get; private set; }
    2.  
    3. void OnEnable()
    4. {
    5.   Instance = this;
    6. }
    7. void OnDisable()
    8. {
    9.   Instance = null;     // keep everybody honest when we're not around
    10. }
    Anyone can get at it via
    MyClass.Instance.
    , but only while it exists.

    If you really insist on a barebones C# singleton, here's a highlander (there can only be one):

    https://gist.github.com/kurtdekker/b860fe6734583f8dc70eec475b1e7163
     
    anszwa likes this.
  11. anszwa

    anszwa

    Joined:
    Jul 30, 2012
    Posts:
    39
    I really appreciate all the effort, but that's not quite what I want to know or what I'm looking for.

    I am familiar with most of the concepts related to singletons, events, etc. anyway. What I would like to have is less a manager and more an object pool, which should be initialized before it is actually used for the first time, and preferably not from another script. Here is my approach of an AudioManager object pool:


    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. public static class AudioManager
    5. {
    6.     private static List<AudioSource> audioSourcesPool;
    7.     private static GameObject audioManagerObject;
    8.     private static int poolSize = 10;
    9.  
    10.     public static void InitializePool()
    11.     {
    12.         audioManagerObject = GameObject.Find("AudioManager");
    13.         if (!audioManagerObject) audioManagerObject = new GameObject("AudioManager");
    14.  
    15.         audioSourcesPool = new List<AudioSource>();
    16.  
    17.         for (int i = 0; i < poolSize; i++)
    18.         {
    19.             audioSourcesPool.Add(CreateNewAudioSource());
    20.         }
    21.     }
    22.  
    23.     public static void PlaySound(AudioClip clip)
    24.     {
    25.         if (audioManagerObject == null) InitializePool();
    26.  
    27.         AudioSource audioSource = GetAudioSourceFromPool();
    28.  
    29.         audioSource.clip = clip;
    30.  
    31.         audioSource.Play();
    32.     }
    33.  
    34.     static AudioSource GetAudioSourceFromPool()
    35.     {
    36.         for (int i = 0; i < audioSourcesPool.Count; i++)
    37.         {
    38.             if (!audioSourcesPool[i].isPlaying) return audioSourcesPool[i];
    39.         }
    40.  
    41.         AudioSource newAudioSource = CreateNewAudioSource();
    42.         audioSourcesPool.Add(newAudioSource);
    43.  
    44.         return newAudioSource;
    45.     }
    46.  
    47.     static AudioSource CreateNewAudioSource()
    48.     {
    49.         GameObject audioSourceObject = new GameObject("SFX");
    50.         audioSourceObject.transform.SetParent(audioManagerObject.transform);
    51.  
    52.         AudioSource audioSource = audioSourceObject.AddComponent<AudioSource>();
    53.  
    54.         return audioSource;
    55.     }
    56. }
    My question is, can I start the InitializePool() function from within this class?
     
  12. kdchabuk

    kdchabuk

    Joined:
    Feb 7, 2019
    Posts:
    47
    A static constructor?

    Code (CSharp):
    1.         static AudioManager()
    2.         {
    3.             SceneManager.activeSceneChanged += (scene0, scene1) => InitializePool();
    4.         }
    edit: or run at a specific part of the player loop, after scene load, e.g. using async UniTask.
     
    Last edited: Jul 4, 2023
  13. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    3,899
    Callbacks registered through a static class like SceneManager or EditorApplication work for every class that registers them, including classes NOT derived from UnityEngine.Object.
     
  14. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,558
    kdchabuk likes this.
  15. anszwa

    anszwa

    Joined:
    Jul 30, 2012
    Posts:
    39
    Yes I am sure. I'm targeting mobile platforms and with smooth camera movements I'm seeing noticeable stuttering when instantiating more complex objects. Object pooling completely solves the problem for me. Of course I could also optimize elsewhere, but that was the quickest fix to continue without problems and the profiler confirmed this again. And just so that the pooling doesn't affect my workflow, I asked this question here. But thanks for the warning ;)
     
  16. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Last edited: Jul 5, 2023
  17. anszwa

    anszwa

    Joined:
    Jul 30, 2012
    Posts:
    39
    Yes, as mentioned at the top of this thread, I'm already using this and it works exactly as I want after launching the application. my question is if there is the same thing only that it is called after every loading of a new scene.
     
  18. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Did you miss this immediately after your first post?

     
    Kurt-Dekker likes this.
  19. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,558
    Yes....

    First you get the delegate.

    Then you get the powah!!
     
  20. Skiriki

    Skiriki

    Joined:
    Aug 30, 2013
    Posts:
    66
    One thing to keep in mind is that the InitializeOnLoad attributes and the scene load events will fire on main thread, but if your code would ever call the static PlaySound() function from something other than the main thread, while your canary GameObject has been deleted through a scene unload and the scene load events you're listening to haven't fired yet, you'll get an exception for trying to create a GameObject on a thread that is not the main thread.

    If that might be an issue for you, you can sidestep that issue by creating the object via a coroutine started on the thread and creating the object after the first yield Return null, and check for initialization via either the Objects presence, or the running coroutine (e.g. via a bool set true in the coroutine on starting it and false after creating the object, or by keeping and clearing a reference to the coroutine.)
     
    anszwa likes this.
  21. anszwa

    anszwa

    Joined:
    Jul 30, 2012
    Posts:
    39
    No, i didn´t miss the deleate, but I was hoping that there is a "simpler" solution, like the InitializeOnLoad attribute. But thank you for your help.