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. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

ScriptableObjects hold on to scene objects past scene destruction -- any workaround?

Discussion in 'Scripting' started by trzy, Jul 17, 2022.

  1. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    Hi,

    I'm using ScriptableObjects for things like events but they also make sense for holding collections of game objects at run time. For example, each time an enemy is spawned, raising an event and adding said enemy to a list is really handy.

    But, if the SO is referenced by a prefab, this seems to keep the SO alive past the end of the scene or the game stopping in the Editor, prompting an error message from Unity that certain GameObjects could not be destroyed.

    For example, consider this SO:

    Code (csharp):
    1.  
    2. [CreateAssetMenu(fileName = "Projectile Topic", menuName = "ScriptableObjects/ProjectileTopic", order = 1)]
    3. public partial class PlayerTopic : ScriptableObject
    4. {
    5.     private readonly List<IPlayerTopicSubscriber> _subscribers = new List<IPlayerTopicSubscriber>();
    6.     private readonly List<Player> _players = new List<Player>();
    7.  
    8.    // ... some code removed ...
    9.  
    10.     public void Add(Player player)
    11.     {
    12.         if (!_players.Contains(player))
    13.         {
    14.             _players.Add(player);
    15.             foreach (var subscriber in _subscribers)
    16.             {
    17.                 subscriber.OnPlayerJoined(player: player);
    18.             }
    19.         }
    20.     }
    21.  
    22.     public void Remove(Player player)
    23.     {
    24.         Debug.Assert(_players.Contains(player));
    25.         bool removed = _players.Remove(player);
    26.         if (removed)
    27.         {
    28.             foreach (var subscriber in _subscribers)
    29.             {
    30.                 subscriber.OnPlayerLeft(player: player);
    31.             }
    32.         }
    33.     }
    34. }
    35.  
    And let's say we have some MonoBehaviour with a PlayerTopic field. If we use that on a prefab, and wire up the field so that spawning the object instantiates it with the player topic already populated, it will end up preventing the SO from being disabled or destroyed when the scene finishes or when the game stops playing. The added Player objects will never be destroyed or disabled and hence will never call _playerTopic.Remove(), creating a circular reference that keeps both alive.

    The only way around this that I can think of is to forego linking SOs to prefabs and instead introducing either a service locator pattern (which I'm trying to avoid in the first place) or making sure the spawner always attaches the SOs at run-time (better but still disappointingly cumbersome).

    Any ideas?

    Thank you,

    Bart
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,780
    For SOs I always mark even my private fields as [NonSerialized], otherwise they persist from run to run.

    Alternately you can null out all your fields in OnEnable / OnDisable. NOTE: SOs only started receiving OnEnable/OnDisable calls when you play/stop about three years ago or so, not sure exactly when. Prior to that those methods were only called once per editor run.
     
  3. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    Thanks, I do use NonSerialized liberally as well but in this case, neither of these solutions work:

    1. I don't think the lists can even be serialized and only exist while the SO is alive.
    2. OnDisable() never fires when I stop playing the game. It only fires at the beginning before OnEnable(). Is it supposed to be called when the editor stops playing?
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,780
    Sigh... sure would be nice! I guess it is yet another wart edge case the way you describe it being called right before the next OnEnable.

    What about hooking this:

    https://docs.unity3d.com/ScriptReference/EditorApplication-playModeStateChanged.html

    ALSO, from your original post you say this:

    AFAIK, nothing about keeping a reference to a GameObject would prevent that GameObject from being destroyed. Are you sure there's not else something going on??
     
  5. trzy

    trzy

    Joined:
    Jul 2, 2016
    Posts:
    128
    I think you could be right. I just tried building a standalone repro and can't! The error that I was getting the other day was that Unity could not clean up some objects (and it would print which). It also suggested that perhaps objects were being spawned in an OnDestroy() handler, which was not the case.

    I was able to narrow it down to a single SO that, if I stopped registering with it, would rectify the error. Hmmm.
     
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,917
    Yeah even with NonSerialized, all SO values will persist even through domain reloads. Basically for as long as the SO hangs around in memory, the values persist; which in the case of the Editor is all the time. That's my experience at least.

    OnEnable and OnDisable for SO's are only called during domain reloads, so they can be used to reset values so long as you enable domain reloading on play mode. This behaviour is different at run time of course, where OnEnable is called as the SO enters memory, and OnDisable is called as it leaves memory.

    So to that end, I'm pretty careful about storing scene level references in SO's, and do it as little as possible. And for SO's with values I really need to reset on leaving play mode, I just an interface + static editor only class that finds all assets with this interface and gets them to reset themselves on leaving play mode.
     
    Kurt-Dekker likes this.
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,780
    Huh... that is counter to my experience, but I'm still a few years behind the latest Unity.