Search Unity

Resolved Start is runing BEFORE OnEnable !

Discussion in 'Scripting' started by lassade, Nov 2, 2022.

  1. lassade

    lassade

    Joined:
    Jan 27, 2013
    Posts:
    127
    Hi,

    I have these 2 scritps:
    Code (CSharp):
    1. [DefaultExecutionOrder(int.MinValue + 100)]
    2. public class Scrambler : MonoBehaviour
    3. {
    4.     void Start()
    5.     {
    6.         foreach(var obj in Obj.all)
    7.         {
    8.             // do some stuff with the objects
    9.         }
    10.     }
    11. }
    12.  
    13. [DefaultExecutionOrder(int.MinValue)]
    14. public class Obj : MonoBehaviour
    15. {
    16.     public static List<Obj> all = new List<Obj>;
    17.  
    18.     void OnEnable() { all.Add(this); }
    19.     void OnDestroy() { all.Remove(this); }
    20. }
    I have many GameObjects with the `Obj` script but only 1 `Scrambler` the issue I'm having is that some `Obj.OnEnable` are been called after `Scrambler.Start`. What is going on?

    OBS: This only happens when I load the scene using `Addressables.LoadSceneAsync`
     
    Last edited: Nov 2, 2022
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    What is loading these addressables? Are you properly awaiting for them to finish loading?
     
  3. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,638
    What makes you think that? How do you know what order they are happening?

    By the way, do you realize that everytime an Obj calls this:

    public static List<Obj> all = new List<Obj>;

    It's just throwing away the list stored in "all" and creating a new one, so no matter what happens "all" will only ever have one item in it and that will be whatever Obj was created last.

    You are never adding a new Obj to the list, you're just replacing the list over and over.
     
    Last edited: Nov 2, 2022
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    That's static... it should just happen once.

    I believe C# doesn't guarantee WHEN static field initializers it will happen, but it does guarantee:

    - it will happen only once
    - it will happen before anyone can use it
     
    Bunny83 likes this.
  5. lassade

    lassade

    Joined:
    Jan 27, 2013
    Posts:
    127
    Yes, but that should not matter, the scripts only run when the scene is loaded.

    Is like Kurt said, it only happens once and before anyone can use it.

    I use `Debug.Log` to log the order each funcion is been called. I just remove it from the code for the sake of simplicity.
     
  6. lassade

    lassade

    Joined:
    Jan 27, 2013
    Posts:
    127
    Sorry I got a bit ahead of my self and yes I can only reproduce this behaviour when loading the scene with addressables.

    But now I can't reproduce the error anymore ... :|
     
  7. lassade

    lassade

    Joined:
    Jan 27, 2013
    Posts:
    127
    Yeah I see what I'm doing wrong:

    1. I load the scene
    2. Obj.OnEnable is called
    3. Scrambler.Start runs it will disable some of the Obj's removing it from the list
    4. Some script that I forgot existed re-enables a few of these Obj's back, braking everything

    :)
     
  8. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    You should probably hide the collection (make the List<T> private) and provide an API on Scrambler such as

    RegisterMe()
    UnregisterMe()

    etc.

    That way Scrambler can be smart and say "Whoa nelly, I ain't done stuff with YOU yet..."

    You may wish also to move away from statics, perhaps something like this:

    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!

    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.

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

    https://gist.github.com/kurtdekker/b860fe6734583f8dc70eec475b1e7163

    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. }
     
    Bunny83 likes this.
  9. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,999
    Well, C# can't guarantee when field initializers run because they may never run at all. However they are always run before the static constructor runs (if there is any). The static constructor would also not run at all, if the class is never used at all. However, any access to the class would initialize the class and that happens at the moment you (or Unity) first access(es) the class. The static type initialization is actually thread safe. So if multiple threads access / use the same type at the same time for the first time, all threads except one would be blocked until the initialization has finished. You can try this with a test like this:

    Code (CSharp):
    1. public class MyType
    2. {
    3.     public static int e = Init();
    4.     public static int b = e;
    5.     public static int Init()
    6.     {
    7.         Debug.Log("Init start " + System.Threading.Thread.CurrentThread.ManagedThreadId);
    8.         System.Threading.Thread.Sleep(1000);
    9.         Debug.Log("Init finished " + System.Threading.Thread.CurrentThread.ManagedThreadId );
    10.         return 42;
    11.     }
    12. }
    13.  
    14. // [ ... ]
    15.  
    16.     void Start()
    17.     {
    18.         var t1 = new System.Threading.Thread(ThreadTest);
    19.         var t2 = new System.Threading.Thread(ThreadTest);
    20.         Debug.Log("Start: before");
    21.         t1.Start();
    22.         t2.Start();
    23.         Debug.Log("Start: after");
    24.     }
    25.  
    Static initialization also makes sure that chained references are resolved in the right order. We assign the result of Init to "e" and then "e" to "b". This does behave correctly. Note that we can actually see that the thread that is executing the Init method is kinda random. So one of the two threads does the initialization. I artificially delayed the init by 1 second. Both threads are blocked until the initialization has finished. The code provided does not even run the first log inside the thread because the class used in the method body seems to be initialized right in the beginning, even though the first use is in the second log statement. However moving the second log statement into a sub method, the first log does immediately print while the logs in the sub routine would get delayed.

    You can also occationally observe that the initialization is done in a certain thread (see the thread id) but the other thread may finish the log execution first. Well, that's just normal threading. The point I wanted to make is that the initialization is thread safe and is guaranteed to have finished before the class is ever used.

    You should not rely on a particular order when it comes to static initialization since the exact point when a class is initialized depends on the way the runtime / jitter has laid out the code. Like in my example as soon as we enter the thread the initialization happens, but that may not always be the case.