Skip to content

Instantly share code, notes, and snippets.

@lazlo-bonin
Last active January 29, 2024 11:24
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save lazlo-bonin/a85586dd37fdf7cf4971d93fa5d2f6f7 to your computer and use it in GitHub Desktop.
Save lazlo-bonin/a85586dd37fdf7cf4971d93fa5d2f6f7 to your computer and use it in GitHub Desktop.
Fixing Unity's broken Undo.RecordObject
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);
};
}
}
}
}
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));
}
}
}
@Rhuantavan
Copy link

I just wanted to thank you for this. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment