ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

【Unityエディタ拡張】Undo.RecordObjectの記憶範囲について

エディタ拡張をした際に、Undoに対応する

Undoが効かない書き方

通常のUnityコンポーネント編集のように、ctrl + zのUndo操作、Redo操作に対応するためには、コンポーネントのクラスを直接操作せず、SerializedObjectやSerializedPropertyを介して操作する必要があります。

簡単なコンポーネントを作って例を挙げますと。

using UnityEngine;

public class RecordObjectTest : MonoBehaviour
{
    public int Number;
}

このエディタ拡張として以下のコードを用意します。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(RecordObjectTest))]
public class RecordObjectTestEditor : Editor
{
    private RecordObjectTest _component;
    
    private void OnEnable()
    {
        _component = target as RecordObjectTest;
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        base.OnInspectorGUI();
        if (GUILayout.Button("1に設定")) { _component.Number = 1; }
        serializedObject.ApplyModifiedProperties();
    }
}

インスペクタは以下のようになります。

ボタンを押すとNumberはちゃんと1に設定されますが、Ctrl + Zを押しても0には戻りません。

Undoが効く書き方

この場合、Numberに値を設定する前にRecordObjectを書いておくと、Undoで0に戻せます。

if (GUILayout.Button("1に設定"))
{
    Undo.RecordObject(_component, "1に設定");
    _component.Number = 1;
}

なお、Undo可能な操作の履歴はUndoHistoryというウィンドウで確認できます。

UndoRecordObjectについて

公式リファレンス

docs.unity3d.com

具体的な説明はここですね。

Internally this creates a temporary copy of the object's state. At the end of the frame Unity diffs the state and detects what has changed. The changed properties are recorded on the undo stack. If nothing has changed (Binary exact comparison is used for all properties), no undo operations are stored on the stack.

雑に訳すと。

内部的には、オブジェクトの一時的なコピーを生成します。フレームの最後に状態の差を見て変化があったかチェックします。変更されたプロパティはUndoスタックに追加されます。何の変更も無かった場合(バイナリの厳密な比較が行われています)、Undo操作は追加されません。

要するにエディタのフレーム単位でUndoをひとまとめにするという感じです。(内部の挙動までちゃんと書いてくれてて正直驚きました)

using (var check = new EditorGUI.ChangeCheckScope())
{
    var newNum = EditorGUILayout.IntField("数値", _component.Number);
    if (check.changed)
    {
        Undo.RecordObject(_component, "数値変更");
        _component.Number = newNum;
    }
}

本来IntFieldで書いてしまうとUndo対象にならないんですが、この書き方だとRecordObjectしてから反映してるのでUndoできます。 こんな感じでチェックで囲って、一旦仮変数で受けつつ、変更があったら反映させるというやり方も無しではないのかなと。 これも何か嫌ですが、SerializedPropertyみたいに文字列でアクセスするよりはマシかなぁと自分は思いますね。ただネストが深いので「Undo可能なField」みたいな拡張メソッドを作った方がいい気はしますが。

推奨されるやり方とはとても思えないので(書きやすくないし、Undoの粒度が1フレーム内の変更というのも雑)、使いやすいときだけ。