Search Unity

Reference is lost for a prefab in ExecuteInEditMode (project provided!)

Discussion in 'Scripting' started by Waarten, Dec 9, 2020.

  1. Waarten

    Waarten

    Joined:
    Aug 24, 2018
    Posts:
    38
    I'm working on a prefab which spawns a 'throwaway' child, both in play mode and in edit mode through [ExecuteInEditMode].

    Code (CSharp):
    1. [ExecuteInEditMode]
    2. public class CubeScript : MonoBehaviour
    3. {
    4.     public GameObject childObject;
    5.     void Start()
    6.     {
    7.         if (childObject == null)
    8.         {
    9.             childObject = new GameObject();
    10.             childObject.transform.parent = transform;
    11.         }
    12.     }
    13. }
    This game object is a prefab, placed in a scene. The idea is that it always spawns a child if it has none. This child is dynamically generated instead of saved in the prefab. See it as derived state from the prefab, based on any dynamic context. I want to maintain this derived state between play and edit mode to avoid computationally expensive recreation. The parent object stores a reference to this child in the childObject variable. This last bit is important.

    When starting play mode, if there was a child in the scene hierarchy, it is kept during play mode. This is how Unity works. However, the reference (in childObject) is lost!

    The above code therefore cannot find the child, and will make an additional one. We now have 2 duplicates, which is unintended. I'm tying to find out _why_ the reference was lost.

    Now, something similar happens when toggling back to edit mode (exiting play mode). Edit mode is entered without the childObject reference, so a new one (3rd child) is created. Fortunately, any play mode state is reset, so the 2nd child was already deleted. We still have 2 duplicate children remaining (child #1 and #3).

    I already tracked the issue to the underlying issue that this childObject reference is not serialized into the scene data. While the object's existence itself is serialized, the reference from parent to child is not. Now the confusing thing is that whether this childObject reference is lost or not, depends on how/when the childObject reference is set. If in edit mode I manually set the (generated) reference to 'None' in the inspector and then assign it again. The reference will be serialized into the scene data. This can be verified by saving the scene and checking the file on disk. If however the childObject reference stems from an object that was generated at Unity startup or entering of edit mode by exiting play mode, the reference is not serialized in the scene. Not even when it is manually saved.

    If the parent is not a prefab, everything works fine. But somewhere on the intersection between the object lifecycle, prefabs and ExecuteInEditMode, a discrepancy emerges between _creating_ GameObjects and _keeping a reference_ to it. My suspicion is that auto-generating the child-object in Start() (or any other Unity message) does not properly mark the object as dirty for serialization, preventing saving of the reference. However I'm weary of getting into editor-specific scripting (SerializedObjects or EditorSceneManager.MarkSceneDirty), because this code is designed to be editor-agnostic: it simply states that this prefab should always have a dynamically generated child if it has none. No matter edit mode or play mode.

    My question is: how do I make sure that childObject reference is properly integrated in the scene file at all times so that I do not end up with duplicates?

    Please note that I'm not looking to solve a just the latter part, but instead I'm trying to understand what is going on and whether this is intended Unity behavior. Any further insights are very welcome!

    I'm using Unity 2020.1.10f1.
    A project file with reproduces the above behavior is here:
    https://drive.google.com/file/d/1g6Vx55NFGM-QMPjM7-Gi4M07pN4qmCfX/view?usp=sharing

    Thanks a lot,
     
    ManuelKers likes this.
  2. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,066
    This is because it's a prefab and not a regular scene object. Prefabs are not saved as part of the scene and instead the object and reference have to be converted to prefab overrides. In editor code, Unity doesn't handle everything automatically for you and you have to use the right APIs or tell Unity when something has changed. Otherwise, changes can end up in limbo and cause weird behavior like the one you're seeing.

    You can see that when the script creates and assigns the child, the property will not be marked as an override (bold text in the inspector). However, if you assign the property manually, Unity will properly create a prefab override for it.

    You can call EditorUtility.SetDirty on your script instance to tell Unity it has changed, so that it can create an override.

    But since the crated object isn't supposed to be saved, it might be better to set its hideFlags to HideFlags.DontSave, which will prevent it from being saved/serialized as part of the scene/prefab and will also prevent the duplicates form appearing.

    Note that these kind of Unity tweaks are usually much trickier than it might seem at first. You'll likely run into issues and editor code idiosyncracies that might not make it worthwhile. I recently did something similar, where I'd instantiate prefabs as previews and had to implement quite a few workarounds to make it work cleanly and also ran into a few bugs. The editor code might not expect you using it that way and these code paths are usually not used/tested as well as the more common Unity APIs.
     
    Waarten likes this.
  3. Waarten

    Waarten

    Joined:
    Aug 24, 2018
    Posts:
    38
    Hello Adrian,

    Thanks, that gives me more insight in how this behavior is intended. I was not aware of HideFlags, which might be useful!

    As you might already have been expecting I'm coming from a different engine/paradigm. For my sanity, I'll heed your advice not to stray too far from the path that Unity provides.

    Thanks for your reply!