Search Unity

Bug Cannot reparent children of an object in OnDestroy when scene changes

Discussion in 'Scripting' started by SurprisedPikachu, May 22, 2021.

  1. SurprisedPikachu

    SurprisedPikachu

    Joined:
    Mar 12, 2020
    Posts:
    84
    I have an script called "GlobalScript" and another called "InSceneScript".

    There's only one instance of ``GlobalScript`` and its object is marked as DontDestroyOnLoad.

    InSceneScript is a script which is attached to an object in the scene. There are a couple of children under InSceneScript object.

    Code (CSharp):
    1. public class InSceneScript : MonoBehaviour
    2. {
    3.     private GlobalScript _global;
    4.  
    5.     private void Awake()
    6.     {
    7.         _global = FindObjectOfType<GlobalScript>();
    8.      
    9.         // Invoke(nameof(ChangeScene), 5);
    10.         Invoke(nameof(DestroyThis), 5);
    11.     }
    12.  
    13.     private void ChangeScene()
    14.     {
    15.         SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    16.     }
    17.  
    18.     private void DestroyThis()
    19.     {
    20.         Destroy(gameObject);
    21.     }
    22.  
    23.     private void OnDestroy()
    24.     {
    25.         Debug.Log("OnDestroy");
    26.         while (transform.childCount > 0)
    27.         {
    28.             Transform child = transform.GetChild(0);
    29.             child.SetParent(_global.transform, false);
    30.         }
    31.     }
    32. }
    If I keep
    Code (csharp):
    1. Invoke(nameof(DestroyThis), 5);
    line enabled, when InSceneScript gets destroyed, all children get reparented to under GlobalScript.

    But if I turn off
    Code (csharp):
    1. Invoke(nameof(DestroyThis), 5);
    and turn on
    Code (csharp):
    1. Invoke(nameof(ChangeScene), 5);
    The children of InSceneScript get destroyed, regardless of the fact that they were reparented to under an object which is marked as DontDestroyOnLoad (GlobalScript).

    This seems like a bug to me (and I reported this: (Case 1337272)). But I wanted to create this thread to make sure that I'm not missing anything.
     
    Last edited: May 22, 2021
  2. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,325
    When unloading the scene there is no guarantee that the GameObject containing InSceneScript gets unloaded before its children, so transform.childCount can return 0 when OnDestroy is called or the children might already be marked for destruction before they are reparented.
     
  3. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,325
    You can avoid this by not using SceneManager.LoadScene directly to change the scene but a custom method which broadcasts an event just before the loading takes place.

    Code (CSharp):
    1. public static class SceneLoader
    2. {
    3.     public static event Action<string> LoadingScene;
    4.  
    5.     public static void LoadScene(string name)
    6.     {
    7.         if(LoadingScene != null)
    8.         {
    9.             LoadingScene(name);
    10.         }
    11.  
    12.         SceneManager.LoadScene(name);
    13.     }
    14. }
    Code (CSharp):
    1. public class InSceneScript : MonoBehaviour
    2. {
    3.     private GlobalScript _global;
    4.  
    5.     private void Awake()
    6.     {
    7.         SceneLoader.LoadingScene += OnLoadingScene;
    8.  
    9.         _global = FindObjectOfType<GlobalScript>();
    10.  
    11.          Invoke(nameof(ChangeScene), 5f);
    12.     }
    13.  
    14.     private void OnLoadingScene(string scene)
    15.     {
    16.         MoveChildren();
    17.     }
    18.  
    19.     private void MoveChildren()
    20.     {
    21.         Debug.Log($"Moving {transform.childCount} Children...");
    22.  
    23.         while(transform.childCount > 0)
    24.         {
    25.             Transform child = transform.GetChild(0);
    26.             child.SetParent(_global.transform, false);
    27.         }
    28.     }
    29.  
    30.     private void ChangeScene()
    31.     {
    32.         SceneLoader.LoadScene(SceneManager.GetActiveScene().name);
    33.     }
    34.  
    35.     private void OnDestroy()
    36.     {
    37.         SceneLoader.LoadingScene -= OnLoadingScene;
    38.     }
    39. }
    SceneManager also seems to have a sceneUnloaded event, but based on its naming at least it might get called too late to be usable here.
     
  4. SurprisedPikachu

    SurprisedPikachu

    Joined:
    Mar 12, 2020
    Posts:
    84
    I put logs in OnDestroy method to print child count. Everytime child count was correct and all the children got successfully reparented (the child count of GlobalScript increased). But after OnDestroy finished, all the children that moved got destroyed. So it's not a matter of order.

    It seems like unity deatroys everything mindlessly when scene changes which is inconsistent with normal destruction and that's why I think it's a bug

    Regarding your suggestion, unfortunately I don't have access to code which loads the scene.
    To give more context, This is a plugin code which uses an Object Pool that doesn't get destroyed between scenes.
    I need to return all the pooled items to the object pool when the script gets destroyed.
     
  5. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,325
    I don't think that is what's happening. It is more likely that the children have are already been marked to be destroyed, so even if you reparent them during OnDestroy they'll still get removed by the end of the frame. It is similar to how Object.Destroy doesn't fully destroy the target immediately but only after the end of current Update loop.
     
  6. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,325
    If you can't modify the code that handles scene loading then I'd try if SceneManager.activeSceneChanged or SceneManager.sceneUnloaded actually get called early enough before OnDestroy. Hopefully they are just named misleadingly and can help you here :D
     
  7. SurprisedPikachu

    SurprisedPikachu

    Joined:
    Mar 12, 2020
    Posts:
    84
    It is NOT similar to Object.Destroy. I tested it and this is the whole point of this post.
    If you call Object.Destroy on a gameobject, you can reparent children in OnDestroy call of that object.
    Children remain in Object.Destroy call. Children don't remain in scene change.

    Unfortunately moving children in both of those callbacks still produces the same problem.
     
  8. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,325
    The difference is that LoadScene affects all objects in the active scene, while in your test you only used Destroy on the single parent GameObject. It makes sense that the child GameObjects would remain unaffected in the latter scenario if they are moved away before the parent gets destroyed at the end of the Update loop.

    If in your test you instead used Object.Destroy on all the child GameObjects as well and then tried to relocate them to a different scene I assume that wouldn't work either.
     
  9. SurprisedPikachu

    SurprisedPikachu

    Joined:
    Mar 12, 2020
    Posts:
    84
    I understand what Unity did when they unload a scene. My problem with what Unity is doing is that it is inconsistent what you can and cannot do in OnDestroy of a gameobject.
    And to add insult to injury, there's no alternative to what I want to do. I cannot even do that in OnDisable. I have no problems if there was a callback from Unity which signaled the scene is going to change. But it doesn't have it. I have some pooled objects that I NEED to return to the pool. That's the point of pooling. But Unity is providing no way to do that.

    I think either destruction behavior needs to be consistent or an API needs to be added to allow moving of objects.
     
  10. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,325
    Yeah, that's a tough one, a sceneUnloading callback would really be useful to have here.

    The only options right now I can think of are:
    1. Provide a method which the plugin user has to manually call before scene loading for current scene objects to get pooled.
    2. Make all objects created by the Object Pool persist through scenes and then you can move them to the Object Pool during the sceneUnloaded event.