'How can we avoid corrupting lists when using Unity Undo.RecordObject?
It turns out that Undo.RecordObject is not a magic bullet solution for all undo situations. There are some changes to an object that can be accurately reverted thanks to RecordObject, but sometimes some changes lead to a corrupted object upon undo.
Here is an attempt to remedy the situation: Fixing Unity's broken Undo.RecordObject
From that solution:
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:
- Call RecordObject before deleting an item "x" from a serialized list
- Unity realizes this item was at index "i", and stores "remove x at i" on the undo stack
- Do any other operation that changes the overall order of your list
- 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.
Using Undo.RegisterCompleteObjectUndo seems like an extreme measure, so it would be interesting to know if there may sometimes be more subtle solutions to this problem, such as using lists in a way that works better with RecordObject, but in order to do this we'd need to understand exactly what RecordObject is doing and how it is going wrong. The description given above does not properly explain it, because in principle reversing a series of inserts and removals ought to perfectly reconstruct the previous state of the list, and what does it mean by treating a list holistically?
So how exactly is RecordObject going wrong, and what sort of list operations are allowable in order to prevent RecordObject from causing corruption upon undo? Are there certain list operations that are guaranteed to be safe?
Here is a forum thread discussing another issue with RecordObject: What does Undo.RecordObject record, exactly?
It came to no clear conclusion, except that supposedly, "Unity straight up refuses to maintain Undo.RecordObject at all. We do all of work with serialized data through SerializedObject/SerializedProperty, since the alternative is an endless stream of S*** not working."
Here is a link to a simple EditorWindow and ScriptableObject that allow you to add and remove numbers from a list by clicking and dragging across cells on a 5x5 grid.
Undo.RecordObject test EditorWindow
Unfortunately it cannot automatically reproduce the problem, because it requires user interaction. The idea is to create a ListObj asset, open a "RecordObject test" editor window, select the ListObj, and then fiddle around adding and removing cells from the grid and undoing those changes until the undo produces a corrupted result. The corruption does not happen every time because I haven't yet pinned down what causes it, but it happens often enough to be a problem. I think it may be important to add/remove multiple cells with a single stroke of the mouse, since this causes multiple RecordObject events to be collapsed together, but I am not sure.
It seems that the same corruption occurs when using SerializedProperty to modify the list instead of using RecordObject, so this issue is not particular to RecordObject but represents a more general difficulty with undoing edits to lists. Fortunately, RegisterCompleteObjectUndo does seem to fix the problem, but it is not clear why this should be necessary.
Here is an image that shows an example of the corrupted list that can result from undoing in the example EditorWindow. Notice particularly the multiple zeros in the middle of the list. By design the EditorWindow is prevented from putting multiple copies of any number into the list, yet undo somehow produces this list.
Solution 1:[1]
Reason
This is because Undo will combine changes together. And it seems like that only the oldest modification is reserved when Unity tries to combine array operations. And it seems like that Unity collapses all operations that change the size of an array because it treats them as the same type of operation.
Undo operations are automatically combined together based on events.
At present, it can be determined that the MouseDown event can automatically split these undo operations, but MouseDrag will not.
Reproduce
Let's see a simple example, these are what Unity is recorded:
Add 3 elements 1, 2, 3 by click.
size 0->1size 1->2size 2->3
Remove them by dragging from 3 to 1.
size 3->2, array[2] 3->3size 2->1, array[1] 2->2size 1->0, array[0] 1->1
Perform an undo action.
Here since the operations in step 2 are performed by dragging, they are combined, and Unity only reserves the oldest size modification
size 3->2, array[2] 3->3, the action will just dosize 2->3, array[2] 3->3, the final result you will get is[0, 0, 3].By the way in step 2 if you remove elements from 1 to 3, undo will revert to the desired result.
size 3->2, array[0] 1->2, array[1] 2->3, array[2] 3->3size 2->1, array[0] 2->3, array[1] 3->3size 1->0, array[0] 3->3
Solution
Here's some approaches to avoid the problem.
Don't record objects in MouseDrag event.
Manually increase undo group index.
private void RemoveCell(ListObj obj, int index) { Undo.IncrementCurrentGroup(); Undo.RecordObject(obj, "remove cell"); obj.content.Remove(index); }Use RegisterCompleteObjectUndo.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |

