Last active
June 17, 2024 12:51
-
-
Save lazlo-bonin/a85586dd37fdf7cf4971d93fa5d2f6f7 to your computer and use it in GitHub Desktop.
Fixing Unity's broken Undo.RecordObject
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEditor; | |
using UnityEngine; | |
using UnityObject = UnityEngine.Object; | |
namespace Ludiq | |
{ | |
public static class UndoUtility | |
{ | |
private static void RecordObject(UnityObject uo, string name) | |
{ | |
if (uo == null) | |
{ | |
return; | |
} | |
// We can't use Undo.RecordObject, because Unity will attempt | |
// to store only a diff of SerializedProperties on the undo stack | |
// (likely in a way similar to how prefab modifications are stored). | |
// This is memory efficient, however it may completely mess up | |
// the order of lists/arrays that are meant to be treated holistically. | |
// Instead, we have to use Undo.RegisterCompleteObjectUndo. | |
// For example: | |
// 1. Call RecordObject before deleting an item "x" from a serialized list | |
// 2. Unity realizes this item was at index "i", and stores "remove x at i" on the undo stack | |
// 3. Do any other operation that changes the overall order of your list | |
// 4. Undo, and Unity will pull the opposite of 2, meaning "insert x at i". | |
// However, the "i" index is no longer guaranteed to be that destined to "x". | |
// If the list has to strictly be treated holistically (as a whole), we cannot | |
// trust a list of differential operations to reconstitute it properly. | |
// In our case, SerializationData.objectReferences is one such list. | |
// It is always treated holistically by our FullSerializer object converter, | |
// and therefore must always come as a whole, not a set of diffs. | |
// Undo.RecordObject(uo, name); // BAD | |
Undo.RegisterCompleteObjectUndo(uo, name); // GOOD | |
// The documentation seems to imply this is automatically done | |
// by calling Undo.RecordObject, however upon testing that doesn't | |
// appear to be true. TODO: report bug to unity. | |
// https://docs.unity3d.com/ScriptReference/EditorUtility.SetDirty.html | |
if (!uo.IsSceneBound()) | |
{ | |
EditorUtility.SetDirty(uo); | |
} | |
// From the Unity documentation: | |
/* Call this method after making modifications to an instance of a Prefab | |
* to record those changes in the instance. If this method is not called, | |
* changes made to the instance are lost. Note that if you are not using | |
* SerializedProperty/SerializedObject, changes to the object are not recorded | |
* in the undo system whether or not this method is called. | |
*/ | |
if (uo.IsPrefabInstance()) | |
{ | |
// One more catch: because we use RegisterCompleteObjectUndo | |
// instead of RecordObject, the object isn't added to the internal list | |
// of objects that need to be actually recorded when the undo | |
// record is flushed. | |
// The problem is that RecordPrefabInstancePropertyModifications must | |
// be called *after* making modifications for it to work. This seems to | |
// be done automatically from undo record flushing, but since our object | |
// isn't registered for flushing, it will never get called. | |
// The hacky workaround here is to wait one editor frame to actually | |
// apply the modifications at a later time. Another solution would have | |
// been to have manual BeginUndoCheck and EndUndoCheck methods, | |
// but it would be more complicated on usage and well, this seems to work. | |
EditorApplication.delayCall += () => | |
{ | |
PrefabUtility.RecordPrefabInstancePropertyModifications(uo); | |
}; | |
} | |
} | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEditor; | |
using UnityEngine; | |
using UnityObject = UnityEngine.Object; | |
namespace Ludiq | |
{ | |
public static class UnityObjectUtility | |
{ | |
public static UnityObject GetPrefabDefinition(this UnityObject uo) | |
{ | |
Ensure.That(nameof(uo)).IsNotNull(uo); | |
return PrefabUtility.GetPrefabParent(uo); | |
} | |
public static bool IsPrefabInstance(this UnityObject uo) | |
{ | |
Ensure.That(nameof(uo)).IsNotNull(uo); | |
return GetPrefabDefinition(uo) != null; | |
} | |
public static bool IsPrefabDefinition(this UnityObject uo) | |
{ | |
Ensure.That(nameof(uo)).IsNotNull(uo); | |
return GetPrefabDefinition(uo) == null && PrefabUtility.GetPrefabObject(uo) != null; | |
} | |
public static bool IsConnectedPrefabInstance(this UnityObject go) | |
{ | |
Ensure.That(nameof(go)).IsNotNull(go); | |
return IsPrefabInstance(go) && PrefabUtility.GetPrefabObject(go) != null; | |
} | |
public static bool IsDisconnectedPrefabInstance(this UnityObject go) | |
{ | |
Ensure.That(nameof(go)).IsNotNull(go); | |
return IsPrefabInstance(go) && PrefabUtility.GetPrefabObject(go) == null; | |
} | |
public static bool IsSceneBound(this UnityObject uo) | |
{ | |
Ensure.That(nameof(uo)).IsNotNull(uo); | |
return | |
(uo is GameObject && !IsPrefabDefinition((UnityObject)uo)) || | |
(uo is Component && !IsPrefabDefinition(((Component)uo).gameObject)); | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I just wanted to thank you for this. :)