Search Unity

Resolved Using MonoBehabivour constructor

Discussion in 'Scripting' started by unity_002B0B4F42CE4236AC3A, Feb 21, 2024.

  1. unity_002B0B4F42CE4236AC3A

    unity_002B0B4F42CE4236AC3A

    Joined:
    Feb 5, 2024
    Posts:
    2
    Is it safe to use constructor for classes inheriting MonoBehaviour?
    I'm aware that non of engine initialization for the object is done at this point. I only need a reference to the object that is consistent and does not change. Basically I'm just trying to get a reference to object created an assign it to some static readonly variable. As far as I have tested this works no problem but I'm kind of new to unity so I want to confirm.

    Also I'm aware of other possible solutions (Lazy loading/assignment inside awake function/execution order change/ etc.) but I want know if constructor is usable or not. If not please give some example elaborating on the scenario it does not work.

    Basically Im trying to do something like this code(The class is supposed to be singleton)
    Code (CSharp):
    1.     protected MyMonoBehaviourClass()
    2.     {
    3.         Assert.IsNull(_Instance, " instance already exists");
    4.  
    5.         _Instance = this;
    6.     }
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,911
    IMHO there's pretty much zero reason to use a constructor in a monobehaviour when we have Awake, OnEnable, and Start.

    Your example code could just be in Awake.
     
  3. unity_002B0B4F42CE4236AC3A

    unity_002B0B4F42CE4236AC3A

    Joined:
    Feb 5, 2024
    Posts:
    2
    Well, yes it could be but there is a problem. the _Instance reference variable would be null if for some reason it is accessed after object construction but before awake is called. Obviously I can rearrange execution order for that to ensure that it is always initialized before other objects but there is a problem there as well. this is supposed to be a generic singleton abstract class. If I resort to using execution order rearrangement I should do this manually for all the classes that inherit this. Though execution order change does guarantee that the objects are initialized in that order too and not just the existence but I don't really need that guarantee.

    Another option would be lazy loading the reference using a property (or getter) but then ensuring that only one instance of the object exists becomes complicated as I need to do the assignment in both the getter and awake function and then inside the awake check for singleton constraint and handle any case where additional objects are created(I'm planning to handle multiple object creation cases inside the constructor as well). Also if getter is accessed by two different threads before awake is called in some rare race conditions I might end up destroying the object that was assigned to _instance while leaving the object that was not assigned and to manage that I need to do a == null check inside the getter/property instead of just if (_Instance) which means additional overhead.

    So, basically to avoid all these complicated stuff I would like to use the constructor if possible...
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,911
    Well as far as I'm aware, the behaviour of monobehaviour constructors is undocumented and undefined. To that end I personally would not rely on it, as it could potentially change at any point. And you won't be able to complain about the change as it is, as I noted, undocumented and undefined.

    From my understanding it happens during serialisation, which runs more often than you think, and is also a threaded process.

    Then just don't do this. Awake is for self initialisation. Initialisation that depends on other objects being ready happens in Start/OnEnable. It's a simple rule to follow that avoids these problems.

    No singleton should be this complicated. This feels like either massive over-engineering, or a defective singleton pattern.

    Personally my singletons just lazy load, and only lazy load. They also do not get included in any scenes. They lazy load, usually instancing themselves, and then hitting themselves with
    DontDestroyOnLoad
    .

    And that's only when I'm using a singleton monobehaviour. Most of the time a static class is all I need. Or I use a singleton pattern scriptable object, so it doesn't depend on a scene at all.
     
  5. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 5, 2024
    Posts:
    479
    Constructors not for you to use, it is for the Unity Engine to initialize your objects. They are called every time Unity is deserialize or create your Component for any reason.

    Capture.PNG
    This is ONE time run in the editor. And here is the code:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class Test : MonoBehaviour
    4. {
    5.     private static int _counter;
    6.  
    7.     public Test()
    8.     {
    9.         _counter++;
    10.         Debug.Log($"Constructor for Test number {_counter}");
    11.     }
    12. }
    Out of the three, two calls go before play, one AFTER play (when the editor scene reinitializes).

    Also called when you create a game object with your component on it, but without domain reload, so whatever was your state last time, the constructor will be called with it. Without entering play mode(!).

    This is dangerous and should not be used at all. Stick to the standard and recommended way to handling components.

    All of these on top of any possible "Domain reload off"-setting. So that's further complicate this thing.
     
  6. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,330
    It's theoretically possibly to use a constructor for this. But because of the reason that @Lurking-Ninja pointed out, it's not a great option if the singleton object goes through the deserialization process. Three different instances get loaded as part of the deserialization process, and dealing with them would get hacky:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3.  
    4. public abstract class Singleton<TDerived> : MonoBehaviour where TDerived : Singleton<TDerived>
    5. {
    6.     static readonly List<TDerived> instances = new(3); // <- might need to use a thread safe collection here instead
    7.  
    8.     public static TDerived GetInstance()
    9.     {
    10.         for(int i = instances.Count - 1; i >= 0; i--)
    11.         {
    12.             var instance = instances[i];
    13.  
    14.             // Remove references to prefab asset and temporary
    15.             // deserialization-related object that no longer exist.
    16.             if(IsNullOrPrefab(instance))
    17.             {
    18.                 instances.RemoveAt(i);
    19.             }
    20.         }
    21.  
    22.         return instances.Count == 1 ? instances[0] : null;
    23.     }
    24.  
    25.     static bool IsNullOrPrefab(TDerived instance) => instance == null || !instance.gameObject.scene.IsValid();
    26.  
    27.     public Singleton() => instances.Add(this);
    28.     ~Singleton() => instances.Remove(this);
    29. }
    If the singleton was created at runtime via new
    GameObject.AddComponent<TDerived>()
    , then only one instance actually gets created, and then this would be a more viable pattern.

    You could also use ISerializationCallbackReceiver.OnAfterDeserialize instead of the constructor, but that would still also get called more than once, so it wouldn't make much of a difference.

    You might also need to make your code thread safe, if you use a constructor or OnAfterDeserialize to assign anything to static variables, because the deserialization process takes place on a background thread :confused:

    Personally I do use constructors in components from time to time, but mostly only to initialize readonly member variables.


    A simpler solution would be to create an AssetPostprocessor that detects when a MonoScript for a singleton-derived type gets imported and sets its script execution order setting to a very low value automatically.
     
    Last edited: Feb 21, 2024
  7. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,330
    This is the go-to way to implement the singleton pattern in the Unity world. It's not that complicated, and you only need to write the code once.

    You can't use things like FindObjectOfType or Destroy outside the main thread anyways, so if you use lazy loading, you can't ever call the singleton accessor from outside the main thread. So you don't need to worry about race conditions in that case.
     
    Last edited: Feb 21, 2024
  8. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,942
    The straightforward way to create a singleton in Unity is as follows:

    Code (CSharp):
    1. static YourSingleton _instance;
    2.  
    3. public static YourSingleton Instance{
    4. get{
    5. if (_instance == null)
    6.     _instance = new GameObject(„YourSingleton“, typeof(YourSingleton)).GetComponent<YourSingleton>();
    7.  
    8. return_instance;
    9. }}
     
  9. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    730
    .AddComponent
     
  10. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,330
    Technically speaking, that is not an implementation of the singleton pattern, but just a static property with lazy loading.

    It's missing the key functionality of preventing extraneous instances from being created by other classes:
    With a plain old C# class implementing the singleton pattern is super easy - just make the constructor private and initialize a public static property with an instance:
    Code (CSharp):
    1. public class Singleton
    2. {
    3.     public static Singleton Instance { get; } = new();
    4.  
    5.     private Singleton() { }
    6. }
    But with a component a little bit more code is needed to get the same functionality. For example:
    Code (CSharp):
    1. [ExecuteAlways]
    2. public class Singleton : MonoBehaviour
    3. {
    4.     static bool creatingSingleton;
    5.  
    6.     public static Singleton Instance { get; }
    7.  
    8.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    9.     static void CreateSingleton()
    10.     {
    11.         creatingSingleton = true;
    12.         Instance = new GameObject("Singleton").AddComponent<Singleton>();
    13.         DontDestroyOnLoad(instance);
    14.     }
    15.  
    16.     void Awake()
    17.     {
    18.         if(!creatingSingleton)
    19.         {
    20.             DestroyImmediate(this);
    21.             return;
    22.         }
    23.  
    24.         creatingSingleton = false;
    25.     }
    26. }
     
    Last edited: Feb 22, 2024