Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

MonoBehaviour references are lost on Undo

Discussion in 'Editor & General Support' started by Cobo3, Nov 22, 2018.

  1. Cobo3

    Cobo3

    Joined:
    Jun 16, 2013
    Posts:
    65
    When deleting a MonoBehaviour and then undoing its deletion, all references to it are lost.

    I have a very small simple project. I have the following two scripts:
    Sin título.png

    With a scene setup like this:
    Sin título2.png
    When I delete "GameObject B" (or just remove the component "ScriptB") and then Undo its deletion, a NullReferenceException is thrown inside the Update method, because the variable "reference" is null; even though the inspector still shows the proper link.

    It's important to note that this only occurs with references to MonoBehaviours. At least, I was not able to reproduce with other type of Components.

    This is a simple representation of the bug, which is preventing me from working on more complex scenarios/setups.

    Does anyone have any workaround or solution to this issue?

    PD: Attached is a simple .unitypackage with the previous setup.
     

    Attached Files:

    Flying_Banana likes this.
  2. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,158
    Hi. Could you file a bug report so we can investigate?
     
  3. Cobo3

    Cobo3

    Joined:
    Jun 16, 2013
    Posts:
    65
    Done!
     
    karl_jones likes this.
  4. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    I was just about to submit a bug report for this. I am seeing this issue in 2019.1.12f1.

    I guess it is not fixed yet? Can we have a link to the issue on the issue tracker? A status update? I don't want to waste QA's time with a duplicate report.
     
  5. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,158
  6. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Oof. Yes I can imagine internally it might be very a hard problem to solve. I guess there is some sort of desync between the serialised data and scene representation of that data? It is a real shame. Is there any more details on the Undo system improvements?
     
  7. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    In the spirit of moving on and getting stuff done - how do you suggest we go about writing editor tools that reference other things to make sure we are ctrl+z safe?

    My use case is pretty simple. I have a BezierCurveManager object, which has a list of Point's, and between the points it generates bezier LineSegments. I have other scripts that can reference the Points or Segments (Gameobject+Behaviours's in the scene) and do something with that. For example, I could write a tool that takes a list of LineSegments, and lofts a rope mesh along the LineSegments. A RopeMeshGenerator. It observes the LineSegments, and whatever other dependancies go into forming that Rope Mesh. If it detects some change, it recreates its mesh. It is a very straight forward setup. But if I delete a line segment or a point, and press ctrl+z, my 'RopeMeshGenerator' is broken now. The LineSegment it was referencing is null because of this bug.

    So what is the 'correct' way to do this?

    The way I am doing is by using ExecuteAlways, and having the RopeMeshGenerator ask the LineSegments if they are 'dirty' (Custom code), and if so, the RopeMeshGenerator recreates the mesh.

    Using SerialisableObject seems to be the way forward, but I only get a SO for custom editors, and I won't have the RopeMeshGenerator's Editor open at the time - because I have the LineSegment selected as I delete it. The RopeMeshGenerator's Editor is not 'persistant'. So I cant run this kind of logic in the editor script.
     
  8. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,158
    Im not entirely sure how your system works but if you have a system that is referencing the data and using it to generate some other data then you could try doing it all inside of a single undo operation.

    Code (csharp):
    1. var group = Undo.GetCurrentGroup();
    2. Undo.RecordObject(curve, "Change curve");
    3. // Change the curve properties
    4.  
    5. Undo.RecordObject(objectThatReferencesTheCurve, "Generate mesh from curve");
    6. // Generate a new mesh
    7.  
    8. Undo.CollapseUndoOperations(group);
    If you do this and the generated data is also serializable then you wont need to do anything when an Undo occurs as everything will be reverted.

    You just need to know what objects are referencing the curve. You could add a list of subscribers to the curve who get notifed of changes or try using the AssetDatabase to find out what assets are referencing it.
     
  9. Billy4184

    Billy4184

    Joined:
    Jul 7, 2014
    Posts:
    6,006
    I see this bug was produced using Unity 5 in the issue tracker, and the verdict was 'Won't Fix'. I am still getting it in Unity 2018. Has this bug been fixed in any Unity version?

    I have an editor script that deletes a GameObject A in the scene (using Undo.DestroyObjectImmediate) which has a component referenced by another component on GameObject B in the scene.

    Upon performing Undo, the GameObject B appears to have a proper reference to GameObject A, but the reference is actually null. Even manually re-dragging GameObject A into GameObject B's reference it continues to be null.

    This seems to be an incredibly ugly bug since it not only renders Undo useless for restoring references, but also puts in ghostly references in the inspector that look fine but are actually null. At a minimum, I would expect the editor to reflect the actual state of the variable, even if restoring references are not possible.
     
  10. aka3eka

    aka3eka

    Joined:
    May 10, 2019
    Posts:
    32
    I have very similar problem here in Unity 2019.3.13f1.
    I have 3 levels of nested ScriptableObjects (referenced further in the text as SO) that are stored as a part of an Asset file. Nesting is implemented via List<SO> and I work with them in my Editor scripts using Array functions of SerializedObject.

    When a root SO is being deleted the following is done:
    1. Undo.RegisterCompleteObjectUndo() is called for the root (asset) object (that's SO too btw).
    2. The script goes through all nesting levels of SOs and calls AssetDatabase.RemoveObjectFromAsset() for each of them.
    3. The root SO is deleted using Undo.DestroyImmediate(). This deletes the SO from Asset file as well.

    The step 2 is required because step 3 doesn't delete nested SOs from Asset file leaving them there forever as garbage.

    When Undo operation is performed, everything looks fine except that Unity loses references to the nested SOs: they appear in the Inspector and work "fine" until the next Asset reload where they become null.

    One could suggest replacing AssetDatabase.RemoveObjectFromAsset() with Undo.DestroyImmediate() in the step 2. This doesn't really help. The problem event gets worse: after Undo is done all the references become null not even in Asset file but in memory too.

    I tried many different combinations of Undo.*() and Destroy*() calls but nothing solves the issue: references keep being lost. In Asset file all references to the nested SOs are saved as "{fileID: 0}" instead of smth. like "{fileID: 6092419105996449486}". That's happening because Undo doesn't restore deleted SO as a part of Asset file.

    So I came up with the following temporary solution until Unity guys fix the problem on their side. Here is the recipe:

    Register Undo.undoRedoPerformed callback and use AssetDatabase.AddObjectToAsset() for each of them.
    So when an Undo/Redo is performed, your callback is called and the restored SOs are added to the Asset again. This magically restores references.

    Here is the code example:

    Code (CSharp):
    1. private void OnEnable()
    2. {
    3.         Undo.undoRedoPerformed += UndoRedoCallback;
    4. }
    5.  
    6. private void OnDisable()
    7. {
    8.         Undo.undoRedoPerformed -= UndoRedoCallback;
    9. }
    10.  
    11. private void UndoRedoCallback()
    12. {
    13.         // your checks here
    14.  
    15.         if (!AssetDatabase.Contains(target)) return;
    16.  
    17.         target.items.ForEach(item => {
    18.                 if (!AssetDatabase.Contains(item))
    19.                 {
    20.                     AssetDatabase.AddObjectToAsset(item, target);
    21.                 }
    22.         });
    23. }
     
  11. jaoels

    jaoels

    Joined:
    Sep 4, 2018
    Posts:
    1
    Still an issue in 2020.3.5f1.

    I found a hack/workaround that works well (at least in my case), so I thought I might as well share since this is the only post I could find on the issue.

    Since the reference is valid in the inspector that means the serialized data must still be valid. What we can do is look up the object id in the serialized data, find the object with that id in the scene, and then manually set the value of the missing reference:

    Code (CSharp):
    1. if (scriptToFix.myBrokenReference == null) {
    2. // Create a serialized object for the component that has the broken reference
    3. SerializedObject serializedObject = new SerializedObject(scriptToFix);
    4.  
    5. // Get the serialized value of the broken reference as a serialized property
    6. SerializedProperty serializedProperty = serializedObject.FindProperty("myBrokenReference");
    7.  
    8. // Find the game object by the serialized instance id
    9. GameObject actualReference = EditorUtility.InstanceIDToObject(serializedProperty.objectReferenceInstanceIDValue) as GameObject;
    10.  
    11. // Assign the broken reference the object we just found
    12. scriptToFix.myBrokenReference = actualReference;
    13. }
    This is just the simplified pseudocode, you would need to tailor the code it to fit your case and add validations etc.

    You would call this code in some sort of validation method when something in the scene changed for example.
     
    Cobo3 likes this.
  12. Cyphre117

    Cyphre117

    Joined:
    Jul 3, 2021
    Posts:
    1
    I had a related issue that I was able to solve with info in this thread. Posting my solution here in the hope it helps someone else.

    In my case I had a list of game objects referencing each other in a loop (used for mesh generation). I wanted to handle the case that the user selects one of the game objects and presses the delete key, the loop should be maintained by joining the two neighbours. When the user presses Undo, the deleted object should be restored and re-inserted into the loop (
    List<>
    ) of references.

    The issue occurred when someone selected multiple nodes, pressed delete, then undo, redo, then undo again. (There were other ways to reproduce but this was a common one). When trying to get back to the original state, some nodes would lose their to the next node in the loop, breaking mesh generation.

    The fix ended up being to switch
    Undo.RecordObject
    for
    Undo.RegisterCompleteObjectUndo
    inside
    JoinNeighbours


    Code (CSharp):
    1. [ExecuteAlways]
    2. public class FloorNodeScript : MonoBehaviour { ...
    3.  
    4. private void OnDestroy()
    5. {
    6.   if (Application.isEditor)
    7.   {
    8.     JoinNeighbours();
    9.   }
    10. }
    11.  
    12. private FloorNodeScript JoinNeighbours()
    13. {
    14.   var prevNode = GetPreviousNode();
    15.   if (prevNode != null)
    16.   {
    17.     Undo.RegisterCompleteObjectUndo(prevNode, undoString); // <- originally Undo.RecordObject
    18.     prevNode.SetNextNode(nextNodeGameObject);
    19.     prevNode.shouldRebuildMesh = true;
    20.     PrefabUtility.RecordPrefabInstancePropertyModifications(prevNode);
    21.   }
    22.   return prevNode;
    23. }
    Hope that helps someone!
     
    Last edited: Feb 16, 2022
    HamCha87 likes this.
  13. HamCha87

    HamCha87

    Joined:
    Jan 4, 2019
    Posts:
    7
    Thank you super much for sharing! For years I'd been finding workarounds during runtime to restore MonoBebaviour references. Never knew RecordPrefabInstancePropertyModifications() was a thing.
     
    Last edited: Feb 29, 2024 at 3:09 AM