うら干物書き

ゲームを作っています。

【Unity】EditorUtility.SetDirtyのなく頃に

 何ということだ。
 バグが見つかってしまった。
 バグを出さずにプログラムを組んでいた連続期間記録が2時間31分で止まってしまった。

 バグが見つかった以上、調査せねばなるまい。
 直せるかどうかわからないけど……。


この記事にはUnity2017.2.0f3を使用しています。

事件発生、現場へ急行だ

 事件現場は、以前書いたJsonデータ作成記事だ。

www.urablog.xyz

 むむむ、クッキング番組を模してJsonデータを作成する記事のようだな。
 そんな頭の中がお花畑の状態でスクリプトを作っていたら、そりゃバグだって生まれるさ!

第一発見者にバグ内容を確認だ

 さて、そもそもどんなバグが発生したというのか。
 少なくともこの記事のテーマであるJsonデータの作成には成功していたはずだ。

 第一発見者に話を聞いたところ、問題はScriptableObjectにあるようだ。
 カスタムエディタウィンドウの編集情報を保存するための仕組みに問題があるというのか。

事件を再現してみる

 では問題が発生した状況のスクリプトを組んで再現してみよう。

TempScriptableObject.cs

using UnityEngine;

[System.Serializable]
public struct TempData
{
    [SerializeField]
    public string   strValue;
    [SerializeField]
    public int      intValue;
    [SerializeField]
    public float[]  floatValues;
}

public class TempScriptableObject : ScriptableObject
{
    [SerializeField]
    private TempData m_data = new TempData();
    public  TempData Data
    {
        get { return m_data; }
        set { m_data = value; }
    }
    
} // class TempScriptableObject

TempEditorWindow.cs

using UnityEngine;
using UnityEditor;

public class TempEditorWindow : ScriptableWizard
{
    [SerializeField]
    private TempData    m_data  = new TempData();
    [SerializeField]
    private TempScriptableObject m_savedObject  = null;

    private static readonly string SAVE_ASSET_PATH = "Assets/Editor/TempObject.asset";

    [MenuItem( "Tool/Temp Window" )]
    public static void Open()
    {
        var window      = DisplayWizard<TempEditorWindow>( "Temp Window" );

        var savedAsset  = AssetDatabase.LoadAssetAtPath<TempScriptableObject>( SAVE_ASSET_PATH );
        // ファイルが存在しないときには作成しよう。
        if( savedAsset == null )
        {
            savedAsset  = CreateInstance<TempScriptableObject>();
            AssetDatabase.CreateAsset( savedAsset, SAVE_ASSET_PATH );
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
        }

        window.m_savedObject    = savedAsset;
        window.m_data           = savedAsset.Data;
    }

    private void OnWizardUpdate()
    {
        if( m_savedObject != null )
        {
            m_savedObject.Data = m_data;
        }
    }
    
} // class TempEditorWindow

f:id:urahimono:20171018001821p:plain

 カスタムエディタウィンドウを閉じてしまうと、先ほどまで編集していた情報が全てクリアされてしまうため、情報をScriptableObject保存しておき、ウィンドウを開いた際にScriptableObjectから情報を読み込むという仕組みだ。
 見る限り、この機能は正しく動作しているように見える。
 ウィンドウを閉じても、ScriptableObjectの値は保存されている。

f:id:urahimono:20171018001835p:plain

 問題はここからだ。
 Unityそのものを閉じてみよう。
f:id:urahimono:20171018001853p:plain

 そして再度Unityを立ち上げ直し、ScriptableObjectの値を確認してみる。
f:id:urahimono:20171018001903p:plain

 ScriptableObjectの値が初期化されてしまった!

推理開始だ

 Unityを立ち上げている間の値は確かに更新されていた。
 そして、エディタウィンドウを経由せず直接ScriptableObjectの値を変えた場合はUnityを閉じても値は保存されていた。
 Unity側がScriptableObjectが更新したことを把握してなかったのだろうか……。
 更新を把握できない……。
 Dirtyフラグか!?
 Dirtyフラグが立っていなかったから、更新したことを検知できなかったということなのか。

 ということはDirtyフラグを立ててしまえばこの不具合は解決できるのではないか。
 Dirtyフラグを立てられる奴に心当たりがある。
 EditorUtility.SetDirty()だ!

docs.unity3d.com

 だが奴には……。

奴にはアリバイがある

 EditorUtility.SetDirty()はこう証言している。

Unity 5.3 より前は、SetDirty はオブジェクトにダーティとマーク付けする主要な方法でした。5.3 以降マルチシーン編集の導入に伴い、シーンのオブジェクトを変更するときにこの関数は使用されなくなりました。代わりに、オブジェクトを変更する前に Undo.RecordObject を使います。この関数はオブジェクトのシーンにダーティと印をつけ、エディターに Undo を記録します。

 俺を使うことが出来ない。
 Undo.RecordObject()こそが真犯人だ。と言っている。

 ただ、今回の事件現場はシーン内ではない。
 シーン内のオブジェクトでない以上、Undo.RecordObject()は使えない。
 彼はシロだ。

 続けてEditorUtility.SetDirty()はこうも証言し始めた。

カスタムエディターを使用してコンポーネントかアセットのシリアライズしたプロパティーを変更する場合は、SerializedObject.FindProperty、SerializedObject.Update、EditorGUILayout.PropertyField、SerializedObject.ApplyModifiedProperties を使用します。これらによって、変更されたオブジェクトに「ダーティ」と印をつけて、「元に戻す」(Undo) のステートにします。

 Editorクラスを継承した場合なら、SerializedObjectの取得は容易なのだが、今回の場合はどうだろうか。
 SerializedObjectの取得は難しそうだ。
 一応、無理やりSerializedObjectを使う形に変更して試してみよう。

TempEditorWindow.cs

private void OnWizardUpdate()
{
    if( m_savedObject != null )
    {
        m_savedObject.Data = m_data;

        var serializedScriptableObject  = new SerializedObject( m_savedObject );

        serializedScriptableObject.Update();
        serializedScriptableObject.ApplyModifiedProperties();
    }
}

f:id:urahimono:20171018001903p:plain

 ……ダメか。
 SerializedObjectの取得方法にも問題がありそうだが、現在の警察の化学力ではこれが限界だ。

 EditorUtility.SetDirty()はこうも訴える。

したがって、この関数を使うただ 1 つの状況は、他の手段を通じてシーンオブジェクトでないオブジェクトを変更し、しかも、特に変更手順に取り消し (undo) 手続きを入れたくない場合です。このようなケースはほとんどなく、どうしても必要な場合を除いては、このコマンドを使用すべきではありません。

……。

このようなケースはほとんどなく

 ほとんどか……。
 だが今回の事件がほとんどないケースとは限らない。
 試してみる価値はあるさ。

TempEditorWindow.cs

private void OnWizardUpdate()
{
    if( m_savedObject != null )
    {
        m_savedObject.Data = m_data;
        EditorUtility.SetDirty( m_savedObject );
    }
}

f:id:urahimono:20171018002016p:plain

 直った!
 事件は解決だ!

彼もまたバグに踊らされた被害者だったのだ

 しかし、事件は本当に解決されたのだろうか……。
 EditorUtility.SetDirty()は最後まで俺を使うべきではないと訴え続けていた。
 もしこれが冤罪だったとしたら、大変なことだ。

 私はこの事件を風化させないためにも、多くの人たちにこの事件のことを広めようと思う。
 そのためには、この事件について調べたことをまとめて本にして出版しよう。
 その本の題名は、

『EditorUtility.SetDirtyのなく頃に』