【Unity】エディタ拡張で「Manager」的なものに使えるScriptableSingleton

Unityのエディタ拡張で「Manager」的なクラスを作るときに使えるScriptableSingletonという機能についてまとめます。

Unity2020.1.0

エディタ拡張で「Manager」的なクラス?

いまUnityのエディタ拡張で、値を保存しておくManager的なクラスを作ることを考えます。
そしてこのManagerが保持する値を二つのエディタウィンドウで描画するものとします。

f:id:halya_11:20201019232312p:plain
Managerとその値を描画するウィンドウ

さてエディタウィンドウが保持する値はアセンブリのリロード(再生したりスクリプトコンパイルしたり)したら消えてしまいます。
そのためエディタで使う値はEditorWindowにシリアライズしておいたりしますが、上記のように複数のEditorWindowで使用する値の場合はそうもいきません。

そこで登場するのがScriptableSingletonです。
これを使うとアセンブリのリロードが走っても値を保持しておけるシングルトンを作ることができます。

再生したりスクリプトコンパイルしたりしても値を保持しておけるインスタンスを作る

さてScriptableSingletonはScriptableSingletonクラスを継承することで作成できます。

using System;
using UnityEditor;

public class ExampleScriptableSingleton : ScriptableSingleton<ExampleScriptableSingleton>
{
    private int _currentNumber;
    
    /// <summary>
    /// 今の値
    /// </summary>
    public int CurrentNumber
    {
        get => _currentNumber;
        set
        {
            var oldValue = _currentNumber;
            _currentNumber = value;
            if (oldValue != value)
            {
                CurrentNumberChanged?.Invoke(oldValue, value);
            }
        }
    }

    /// <summary>
    /// <see cref="CurrentNumber"/>が変わった時に呼ばれるイベント
    /// </summary>
    public event Action<int, int> CurrentNumberChanged;
}

これには以下のようにExampleScriptableSingleton.instanceとすることでアクセスできます。

using UnityEngine;
using UnityEditor;

public class ExampleWindow : EditorWindow 
{

    [MenuItem("Window/ExampleWindow")]
    private static void Open()
    {
        GetWindow<ExampleWindow>("ExampleWindow");
    }

    private void OnGUI()
    {
        if (GUILayout.Button("Increment"))
        {
            ExampleScriptableSingleton.instance.CurrentNumber++;
        }
        if (GUILayout.Button("Decrement"))
        {
            ExampleScriptableSingleton.instance.CurrentNumber--;
        }
    }

    private void OnEnable()
    {
        ExampleScriptableSingleton.instance.CurrentNumberChanged += OnCurrentNumberChanged;
    }

    private void OnDisable()
    {
        ExampleScriptableSingleton.instance.CurrentNumberChanged -= OnCurrentNumberChanged;
    }

    private void OnCurrentNumberChanged(int oldValue, int newValue)
    {
        Debug.Log($"Current value was changed : {oldValue} to {newValue}");
    }
}

このウィンドウでは以下のように値をインクリメント/デクリメントできます。
またその際にログが出力されるようになっています。
これを動かすと再生したりスクリプトコンパイルしても値が保持されていることを確認できます。

f:id:halya_11:20201019233103p:plain
サンプルウィンドウ

Unityを再起動しても消えないようにする

さて上記の例のようにしてScriptableSingletonを使うと、アセンブリリロードが走っても消えないインスタンスを作成できました。
ただこれはあくまでメモリ上に保持されているというだけなので、Unityを再起動したら消えてしまいます。

もし値を永続化したい場合には、以下の対応を行います。
※ 以下で紹介しているFilePathアトリビュートはUnity2020.1からpublicなAPIになったようです

  1. ScriptableSingletonを継承したクラスにFilePathアトリビュートを付ける
  2. 永続化したい値をシリアライズ対象にする
  3. 永続化のタイミングでSave()を呼ぶ

これに対応したコードは以下の通りです。

using System;
using UnityEditor;
using UnityEngine;

// アトリビュートを付ける
[FilePath("ExampleScriptableSingleton/ExampleScriptableSingleton.dat", FilePathAttribute.Location.ProjectFolder)]
public class ExampleScriptableSingleton : ScriptableSingleton<ExampleScriptableSingleton>
{
    // シリアライズ対象にする
    [SerializeField]
    private int _currentNumber;
    
    public int CurrentNumber
    {
        get => _currentNumber;
        set
        {
            var oldValue = _currentNumber;
            _currentNumber = value;
            if (oldValue != value)
            {
                CurrentNumberChanged?.Invoke(oldValue, value);
                // 永続化する
                Save(true);
            }
        }
    }

    public event Action<int, int> CurrentNumberChanged;
}

これで、Unityを再起動してもCurrentNumberが保存されるようになりました。

永続化ファイルの置き場

なお、永続化するためのファイルの置き場はFilePathアトリビュートの第二引数で指定できます。

まず上記のようにFilePathAttribute.Location.ProjectFolderを指定するとUnityプロジェクト直下の指定したパスに保存されます。
これはプロジェクト固有の値となるので、他のUnityプロジェクトに影響を与えるべきでない値に使います。

またFilePathAttribute.Location.PreferencesFolderを指定するとPreferenceとして保存され、
複数のUnityプロジェクトをまたいだ設定値として保存することができます。
ちなみに僕の手元のWindows環境では以下のようなパスに保存されていました。

C:\Users\UserName\AppData\Roaming\Unity\Editor-5.x\Preferences\ExampleScriptableSingleton\ExampleScriptableSingleton.dat

参考

docs.unity3d.com