使用 Unity Undo.RecordObject 时如何避免破坏列表?

How can we avoid corrupting lists when using Unity Undo.RecordObject?

事实证明,Undo.RecordObject 并不是适用于所有撤消情况的灵丹妙药。借助 RecordObject,可以准确还原对象的一些更改,但有时某些更改会在撤消时导致对象损坏。

这里尝试补救这种情况:Fixing Unity's broken Undo.RecordObject

来自该解决方案:

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.

使用 Undo.RegisterCompleteObjectUndo 似乎是一种极端的措施,因此了解有时是否有更微妙的解决方案来解决这个问题会很有趣,例如以更适合 RecordObject 的方式使用列表,但是为了做到这一点,我们需要准确地理解 RecordObject 在做什么以及它是如何出错的。上面给出的描述并没有很好地解释它,因为原则上将一系列的插入和删除倒转应该可以完美地重建列表的先前状态,而整体对待列表是什么意思?

那么 RecordObject 到底是怎么出错的,为了防止 RecordObject 在撤消时造成损坏,允许进行什么样的列表操作?是否有某些列表操作可以保证安全?

这是讨论 RecordObject 另一个问题的论坛帖子:What does Undo.RecordObject record, exactly?

没有明确的结论,除了据说,“Unity 直接拒绝维护 Undo.RecordObject。我们通过 SerializedObject/SerializedProperty 处理序列化数据的所有工作,因为替代方案是源源不断的 S*** 不起作用。

这是一个 link 简单的 EditorWindow 和 ScriptableObject,允许您通过单击并拖动 5x5 网格上的单元格来添加和删除列表中的数字。

Undo.RecordObject test EditorWindow

遗憾的是,它无法自动重现问题,因为它需要用户交互。这个想法是创建一个 ListObj 资产,打开一个“RecordObject 测试”编辑器 window,select ListObj,然后 fiddle 在网格中添加和删除单元格并撤消这些更改,直到撤消会产生损坏的结果。损坏不会每次都发生,因为我还没有确定导致它的原因,但它发生的频率足以成为一个问题。我认为用鼠标单击一下 add/remove 多个单元格可能很重要,因为这会导致多个 RecordObject 事件折叠在一起,但我不确定。

使用 SerializedProperty 而不是使用 RecordObject 修改列表时,似乎会发生同样的损坏,因此这个问题并不是 RecordObject 特有的,而是表示撤消对列表的编辑时更普遍的困难。幸运的是,RegisterCompleteObjectUndo 似乎确实解决了这个问题,但不清楚为什么必须这样做。

这是一张图片,显示了在示例 EditorWindow 中撤消操作可能导致的损坏列表示例。请特别注意列表中间的多个零。通过设计,EditorWindow 无法将任何数字的多个副本放入列表中,但以某种方式撤消会生成此列表。

原因

这是因为撤消会将更改组合在一起。 当 Unity 尝试组合数组操作时,似乎只保留最旧的修改。而且似乎 Unity 折叠了所有更改数组大小的操作因为它将它们视为相同类型的操作。

Undo operations are automatically combined together based on events.

目前可以确定MouseDown事件可以自动拆分这些undo操作,但是MouseDrag不会。

重现

看个简单的例子,Unity记录的是这些:

  1. 通过单击添加 3 个元素 1、2、3。

    • size 0->1
    • size 1->2
    • size 2->3
  2. 通过从 3 拖动到 1 来删除它们。

    • size 3->2, array[2] 3->3
    • size 2->1, array[1] 2->2
    • size 1->0, array[0] 1->1
  3. 执行撤消操作。

    这里由于第2步的操作是通过拖拽的方式进行的,合并在一起,Unity只保留最旧的尺寸修改size 3->2, array[2] 3->3,动作就做size 2->3, array[2] 3->3,最后的结果你会得到的是[0, 0, 3].

  4. 顺便提一下,如果您在步骤 2 中删除了从 1 到 3 的元素,撤消将恢复为所需的结果。

    • size 3->2, array[0] 1->2, array[1] 2->3, array[2] 3->3
    • size 2->1, array[0] 2->3, array[1] 3->3
    • size 1->0, array[0] 3->3

解决方案

这里有一些方法可以避免这个问题。

  1. 不记录 MouseDrag 事件中的对象。

  2. 手动增加撤消组索引。

     private void RemoveCell(ListObj obj, int index)
     {
         Undo.IncrementCurrentGroup();
         Undo.RecordObject(obj, "remove cell");
         obj.content.Remove(index);
     }
    
  3. 使用 RegisterCompleteObjectUndo。