Search Unity

Static Variables and Editor Reload Behavior

Discussion in 'Scripting' started by stonstad, Jan 18, 2019.

  1. stonstad

    stonstad

    Joined:
    Jan 19, 2018
    Posts:
    660
    I'm encountering some strange behavior with Unity 2018.3.x. I'm actually wondering if it's a possible scripting runtime or garbage collection bug -- I totally understand this is unlikely.

    I have a script that I attached to a game object through the editor prior to hitting play. The script stores a few variables that I reference frequently, and so I make a static reference available via a singleton design pattern.

    About six or seven times a day during my iterative game development the script just disappears. The first thing that happens is this error, seemingly at random:

    The referenced script (Unknown) on this Behavior is missing!

    And then when this happens I get hundreds of runtime exceptions (NullReferenceException) when the static instance variable is referenced (Singleton.Instance). I do not have any logic in place to destroy the game object or the script component.

    I think other scripts which inherit this same Singleton base class may exhibit the same random behavior, but I can't confirm that yet since I only recently identified the pattern of the errors.

    It seems to happen most when I leave the editor playing, surf the web and then return. It feels like a garbage collection quirk maybe?

    Here is the code (in totality) that is affected by this behavior.

    Code (CSharp):
    1.     public class SingletonMonoBehaviour<T> : MonoBehaviour where T : class
    2.     {
    3.         public static T Instance { get; private set; } = null;
    4.  
    5.         protected virtual void Awake()
    6.         {
    7.             Instance = this as T;
    8.         }
    9.     }
    Code (CSharp):
    1.     public class RuntimeSettings : SingletonMonoBehaviour<RuntimeSettings>
    2.     {
    3.         [Header("General")]
    4.         public bool LogInformation = false;
    5.  
    6.         [Header("Physics")]
    7.         public bool LogCollisions = false;
    8.         public bool LogTriggers = false;
    9.  
    10.         [Header("Events")]
    11.         public bool LogDestruction = false;
    12.         public bool HierarchyShowEffects = false;
    13.     }
    I have been mostly ignoring it because I can just stop the editor and hit play. The problem goes away until the next time it randomly happens. The game I'm running this in is somewhat substantial (assets, scripts, particles, physics).

    I started looking at memory usage to see if things are critically low -- but there's no indication that this is the case.

    Has anyone seen this behavior before?

    Thanks,
    Shaun
     
    marcospgp likes this.
  2. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,196
    Sounds like something's destroying your instance, and you're not making a new one. To diagnose, you might add an OnDestroy method and log when it occurs. I'm not sure that'll help much...

    I use a similar singleton-based approach, without any issues like you're describing. The main difference is that instead of creating the instance in Awake, I create is in the Instance getter if the instance is null.

    Code (CSharp):
    1. public static T Instance
    2.         {
    3.             get
    4.             {
    5.                 lock (_instanceLock)
    6.                 {
    7.                     if (_instance == null)
    8.                     {
    9.  
    10.                         if (!_isDestroyed)
    11.                         {
    12.                             var existingObject = GameObject.FindObjectOfType(typeof(T));
    13.                             if (existingObject != null)
    14.                             {
    15.                                 _instance = (T)existingObject;
    16.                             }
    17.  
    18.                             if (_instance == null)
    19.                             {
    20.                                 // Create Instance, unless we're shutting down
    21.                                 var prefabResourcePath = string.Format("{0}/{1}", ResourceConstants.DirectorResources.ResourceRoot, typeof(T).Name);
    22.                                 var prefab = Resources.Load<GameObject>(prefabResourcePath);
    23.                                 if (prefab == null)
    24.                                 {
    25.                                     throw new Exception(string.Format("Prefab not found: {0}", prefabResourcePath));
    26.                                 }
    27.  
    28.                                 var go = Instantiate(prefab);
    29.                                 DontDestroyOnLoad(go);
    30.                                 go.transform.SetParent(null);
    31.                                 _instance = (T)go.GetComponent(typeof(T));
    32.                             }
    33.                         }
    34.                     }
    35.  
    36.                     return _instance;
    37.                 }
    38.             }
    39.         }
     
    marcospgp likes this.
  3. stonstad

    stonstad

    Joined:
    Jan 19, 2018
    Posts:
    660
    @dgoyette That's a good idea. Here is what I did to catch the change:

    Code (CSharp):
    1.   public class SingletonMonoBehaviour<T> : MonoBehaviour where T : class
    2.     {
    3.         public static T Instance { get; private set; } = null;
    4.  
    5.         protected virtual void Awake()
    6.         {
    7.             Instance = this as T;
    8.         }
    9.  
    10.         protected virtual void LateUpdate()
    11.         {
    12.             if (Instance == null)
    13.                 Debug.Log("Instance " + typeof(T).Name + " is now null");
    14.         }
    15.  
    16.         protected virtual void OnDestroy()
    17.         {
    18.             Debug.Log("<color=red>Destroying " + typeof(T).Name);
    19.         }
    20.     }
    The result is interesting and I think it's suggestive of a bug in the Unity scripting runtime. Here's the subsequent order of events after modifying the script as shown above. Please bear in mind that this particular behavior was kicked off by alt-tabbing away from Unity and then returning --

    1 - Unity warns that a referenced script is now unknown and missing


    2 - Locations in my code which reference SingletonMonoBehaviour<T>.Instance now throw null reference exceptions.


    3 - Lastly, the script (which IS still running) shows that the singleton variable is now inexplicably null!


    OnDestroy is never called. But we know the script still exists (somehow) because LateUpdate is called.

    Unity 2018.3.1f1, .Net 4x Equivalent, .Net Scripting Backed, .Net 4.x API compat.

    @Tautvydas-Zilys Is this a thing? i.e. past bugs whereby a scripts somehow loses certain variable references?

    *edited for grammar.
     
    Last edited: Jan 20, 2019
    marcospgp likes this.
  4. nat42

    nat42

    Joined:
    Jun 10, 2017
    Posts:
    353
    The description of when the issue seems to occur makes me think of stuff being paged out and not being paged back in.

    Long shot: are you running software (or your builds) from a network drive?
     
  5. stonstad

    stonstad

    Joined:
    Jan 19, 2018
    Posts:
    660
    @nat42 Thanks for helping. The file system is local SSD. I only see the behavior with a particular project that is far along in production -- a pretty large game.
     
  6. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    The information you provided makes me think you're running into an issue with how the type initializations are handled, more specifically how they're handled when changes occur.

    You said / observed that:
    - The Script instances keep running
    - The variable turns to being null (or at least Unity's way of telling you it is "null" via the overloaded boolean operator)
    - This mainly or only happens when you hit play, leave the editor to do something, get back to it.

    If the instance was destroyed for some unknown reason, it wouldn't make sense to receive further calls to Update unless you had discovered a bug that leads to not unregistering the instance internally deep in the engine's core.

    So let's assumy that's rather unlikely, and skip this one for now.

    What stands out though is that you leave the editor, do something, and come back.

    This makes me think you're triggering a refresh / reload of the AppDomain, which implies that type initializations are executed again, statics are reset to their initial values (either defaults or from static field initializers), and so on. That'd also mean that you're dealing with the actual 'null' and not a destroyed instance in your static variable.

    You can verify whether that's the case by simply adding the "static constructor" and doing a log. Now, try to reproduce the issue and check whether the log appears another time in the console.

    These sorts of AppDomain reloads occur (for example) when source code changes (re-import / re-compilation), they may as well be triggered when other assets change and so on.

    It can still be useful to file a report, so that they might find a better way to either inform the user when play mode is active and code has changed, or at least they could delay the re-import of affected asset types / recompilation of source code.


    Anyway, that said, you might just want to stop the play mode when you might need to change something, as you would want to have the actual code (including changes) running when you've done some changes.
     
    marcospgp and stonstad like this.
  7. stonstad

    stonstad

    Joined:
    Jan 19, 2018
    Posts:
    660
    @Suddoha, you're awesome. Spot on, that was exactly the cause! Thank you for such a detailed and thoughtful response.

    I had no idea this behavior existed within Unity. I looked at the pros/cons of changing my singleton pattern (to allow recompile) vs just disabling the behavior. I went the route of disabling the editor feature via the preferences screen.



    "Recompile After Finished Playing"
     
    marcospgp and Suddoha like this.
  8. darthdeus

    darthdeus

    Joined:
    Oct 13, 2013
    Posts:
    81
    As a followup question, I'm a bit confused what is the cause of the "The referenced script (Unknown) on this Behaviour is missing!".

    I've played around with the static constructor and everything seems to be working as expected (using https://wiki.unity3d.com/index.php/Singleton). But I'm just confused which reference to what script is missing during the reload.
     
    marcospgp likes this.
  9. marcospgp

    marcospgp

    Joined:
    Jun 11, 2018
    Posts:
    194
    I'm in version 2020.2.1f1 and came across this issue. This should be made clearer in the documentation, as most resources online about creating singletons in Unity fail to mention it.

    @JuliaP_Unity This is what I was talking about regarding the UI Toolkit null reference error on play mode reload in the editor: it seems static fields get reset to null on reload and I had no idea nor had seen that anywhere before, not even in the "how to create a singleton in Unity" tutorials out there.

    I'm also getting these warnings when play mode reloads:

    upload_2021-1-25_19-39-9.png

    And if anyone comes across this, I managed to deal with this issue by moving the singleton code to OnEnable(), which is run when play mode reloads:

     
    Last edited: Jan 25, 2021
  10. imaxs

    imaxs

    Joined:
    Oct 31, 2020
    Posts:
    14
    Just use FindObjectOfType<T>(). Returns the first active loaded object of T type.

    Code (CSharp):
    1.  
    2. RuntimeSettings singleton = FindObjectOfType<RuntimeSettings>();
    3. .....
    4. Debug.Log(singleton.LogInformation ? "True" : "False")
    5.