エフアンダーバー

個人開発の記録

Unityのエディタ拡張で設定を保存

Unityのエディタ拡張にてプロジェクト間で共有する設定の保存がしたくなり、いろいろ試したのでその記録。

はじめに

Unityのエディタ拡張を書いているとプロジェクト間で設定を共有したくなることがあります。

こんなときによく紹介されるのがEditorPrefsクラスです。 EditorPrefsは"Editor Preferences"の略で「エディタ設定」の意味です。 ゲーム製作時に使うPlayerPrefsクラスは有名かと思いますが、これのエディタ版です (たまにPlayerPrefsクラスをゲームデータのセーブに使う人がいますが、 これも"Player Preferences"の略で「プレイヤー設定」の意味なので、設定以外には使わないのが賢明です)。 使い方も概ねPlayerPrefsクラスと同じです。

ただし、EditorPrefsクラスにはPlayerPrefsクラスと決定的に違う点があります。 それは保存する値がプロジェクト固有でないということです(共有するためのクラスなので当たり前ですが)。 これが何を意味するかというと、自分以外が保存した設定の変更が可能なのです。 つまり、誤った操作を行うとUnityや他のエディタ拡張の動作を不安定にしかねません。

ひとつ、例としてDeleteAllメソッドを挙げると、 PlayerPrefsではPlayerPrefs.DeleteAll()で、ゲーム内のすべての設定を削除できました。 一方、EditorPrefs.DeleteAll()を実行すると、Unity本体を含むすべてのUnityエディタ関連の設定が削除されます。 もはや、なぜこんなAPIが公開されているのかわからないレベル。

そんなこんなでEditorPrefsクラスをどう使っていくのがいいだろうか、というのが今回の趣旨です。

そもそもEditorPrefsクラス使わなければいいんじゃないか、という意見もありそうですが、今回はその辺は扱わない方向で。 標準の機能で苦労せずに書くという方針でいきます。

また、この記事で紹介するのはプロジェクト間で設定を共有する方法です。 プロジェクト固有でいいのであれば、 EditorUserSettingsクラスによる保存やScriptableObjectクラスによるアセット化を考えるのがいいと思います。

準備

同じコードを何度も書くのもアレなので、共通の基底クラスを定義します。

using UnityEditor;
using UnityEngine;

public abstract class CustomEditorWindow : EditorWindow
{
   #region Fields

    [SerializeField]
    protected int m_IntValue;

    [SerializeField]
    protected float m_FloatValue;

    [SerializeField]
    protected string m_StringValue;

   #endregion

   #region Messages

    protected void OnGUI()
    {
        this.m_IntValue = EditorGUILayout.IntField("Integer", m_IntValue);
        this.m_FloatValue = EditorGUILayout.Slider("Float", m_FloatValue, 0.0f, 1.0f);
        this.m_StringValue = EditorGUILayout.TextField("String", m_StringValue);
    }

   #endregion
}

f:id:fspace:20160825194516p:plain

内容としてはシリアライズされる変数の定義とその編集用GUIを作成しているだけです。

ここで定義した変数をどう保存するかを考えていきます。

シンプルな方法

まずはシンプルというか、素直な方法。

ひとつひとつ変数をEditorPrefsに保存していきます。

using UnityEditor;

public class ForEachField : CustomEditorWindow
{
   #region Constants

    private const string Prefix = "UniqueID_";

    private const string IntegerKey = Prefix + "Integer";
    private const string FloatKey = Prefix + "Float";
    private const string StringKey = Prefix + "String";

   #endregion

   #region Messages

    private void OnEnable()
    {
        m_IntValue = EditorPrefs.GetInt(IntegerKey);
        m_FloatValue = EditorPrefs.GetFloat(FloatKey);
        m_StringValue = EditorPrefs.GetString(StringKey);
    }

    private void OnDisable()
    {
        EditorPrefs.SetInt(IntegerKey, m_IntValue);
        EditorPrefs.SetFloat(FloatKey, m_FloatValue);
        EditorPrefs.SetString(StringKey, m_StringValue);
    }

   #endregion

   #region Methods

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

   #endregion
}

注意すべき点はEditorPrefsのキーに必ずユニーク(他と絶対に被らない)な文字列を付加することです (つまり、コード中の"UniqueID_"の部分を自分固有の文字列に置き換える)。 もしも他のエディタ拡張開発者と値が被ってしまうと、お互いに設定を書き換え合って邪魔をすることになります。

利点と欠点

この方法の利点は細かな制御ができることです。 例えば、どこかの値が変更された場合にその部分の値だけを保存したり、 設定の一部分だけを削除したりといったことができます。

一方、この方法の欠点はキーをひとつひとつ管理しなければならないことと、一括削除ができないことです。 キーの管理は現状のコードをみる限りでは問題ないように見えるかもしれませんが、 コードを書き換える中で設定項目が変わったりすると古くなったキーの削除が面倒になります。 一括削除は最初に説明した通りDeleteAllが使えないためできません。

すべてのキーの列挙ができれば、もう少しマシな状況だったのですが、 EditorPrefsにはそのような機能はないようなので、これ以上はどうしようもありません。

JSON化して保存する方法

キーが多くて面倒ならば、ひとつのキーにすべての情報を保存してしまおうという方法。

JSONというのは特にWeb関係でよく使われる汎用のデータ保存形式です(XMLとかYAMLみたいなやつ)。 Unity 5.3 でオブジェクトとJSONを相互変換できる機能が追加されたため、オブジェクトをJSONとして保存するコードを書くのは容易です。 JSONはテキスト形式なので、EditorPrefsSetStringGetStringで保存・復元できます。

using UnityEditor;

public class UnityObjectAsJson : CustomEditorWindow
{
   #region Constants

    private const string Key = "UniqueID";

   #endregion

   #region Messages

    private void OnEnable()
    {
        if (EditorPrefs.HasKey(Key))
        {
            string json = EditorPrefs.GetString(Key);

            EditorJsonUtility.FromJsonOverwrite(json, this);
        }
    }

    private void OnDisable()
    {
        string json = EditorJsonUtility.ToJson(this);

        EditorPrefs.SetString(Key, json);
    }

   #endregion

   #region Methods

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

   #endregion
}

UnityEngine.Objectを継承したクラスをJSONに変換する場合には基本的にEditorJsonUtility.ToJsonを使います。 このコードでは保存対象をEditorWindowを継承したこのクラス自身にしているため、 EditorJsonUtility.ToJsonに自分自身を渡します。

string json = EditorJsonUtility.ToJson(this);

EditorPrefs.SetString(Key, json);

JSONはテキスト形式なので結果がstringで返ってきます。 それをそのままEditorPrefs.SetStringで保存します。

if (EditorPrefs.HasKey(Key))
{
    string json = EditorPrefs.GetString(Key);

    EditorJsonUtility.FromJsonOverwrite(json, this);
}

値の復元時にはまず値が保存されているかどうかを確認します。 これは値が保存されていなかった場合に空の文字列をJSONとして渡してしまわないようにするためです。

あとはEditorPrefsからJSONを取得して、EditorJsonUtility.FromJsonOverwriteを呼び出すことで、 そこに保存されている値で自分自身の値を書き換えるように指示します。

EditorPrefsのキーには先ほどと同じくユニークな文字列を指定する必要があります。

利点と欠点

この方法の利点はなんといっても楽なことです。 一度書いてしまえば、あとはクラス内の変数を書き換えるだけで勝手に値が保存・復元されます *1。 また、設定の一括削除はEditorPrefs.DeleteKeyでキーを削除することで行えます。

一方、欠点は細かな制御ができないことと意図しない情報が保存されることです。

この方法ではコードからわかる通り、すべての保存・復元・削除を一括でしかできません。 そのため、一部分の変更でも保存する場合にはすべての値を改めて保存することになります。 削除も同様で、一部分の削除ができないため、削除したい部分に初期値を代入していくしかありません。 部分的な復元に至っては代替手段もありません。

意図しない情報というのが何かを示すには、実際に保存される情報を見せたほうが早いと思うので貼ります。 次の内容が先ほどのコードで保存されるJSONです。

{
  "MonoBehaviour": {
    "m_Enabled": 1,
    "m_EditorHideFlags": 0,
    "m_Name": "",
    "m_EditorClassIdentifier": "",
    "m_AutoRepaintOnSceneChange": false,
    "m_MinSize": {
      "x": 100.0,
      "y": 100.0
    },
    "m_MaxSize": {
      "x": 4000.0,
      "y": 4000.0
    },
    "m_TitleContent": {
      "m_Text": "UnityObjectAsJson",
      "m_Image": "@{instanceID=0}",
      "m_Tooltip": ""
    },
    "m_DepthBufferBits": 0,
    "m_AntiAlias": 0,
    "m_Pos": {
      "serializedVersion": "2",
      "x": 291.0,
      "y": 260.0,
      "width": 400.0,
      "height": 300.0
    },
    "m_IntValue": 1234567890,
    "m_FloatValue": 0.125,
    "m_StringValue": "テキストだよ^_^"
  }
}

実際に保存したいのは下のほうの三つだけなのですが、 EditorWindow関連の値がいろいろと保存されてしまっています。

JSON化して保存する方法 その2

最後はEditorWindow自体をJSONにするのではなく、 設定保存専用のクラスをつくって、それをJSONとして保存する方法。

using System;
using UnityEditor;
using UnityEngine;

public class SerializableAsJson : CustomEditorWindow
{
   #region Settings

    [Serializable]
    private class Settings
    {
        public int m_IntValue;

        public float m_FloatValue;

        public string m_StringValue;
    }

   #endregion

   #region Constants

    private const string Key = "UniqueID";

   #endregion

   #region Messages

    private void OnEnable()
    {
        if (EditorPrefs.HasKey(Key))
        {
            string json = EditorPrefs.GetString(Key);

            Settings settings = JsonUtility.FromJson<Settings>(json);

            this.m_IntValue = settings.m_IntValue;
            this.m_FloatValue = settings.m_FloatValue;
            this.m_StringValue = settings.m_StringValue;
        }
    }

    private void OnDisable()
    {
        Settings settings = new Settings();
        settings.m_IntValue = this.m_IntValue;
        settings.m_FloatValue = this.m_FloatValue;
        settings.m_StringValue = this.m_StringValue;

        string json = JsonUtility.ToJson(settings);

        EditorPrefs.SetString(Key, json);
    }

   #endregion

   #region Methods

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

   #endregion
}

概ね前述の方法と同じですが、JSONとの相互変換にはEditorJsonUtilityではなくJsonUtilityを使っています。 これはEditorJsonUtilityUnityEngine.Objectを継承したクラスに対してしか使えないためです。

注意点としては、設定保存用のクラスには必ず[Serializable]をつけてシリアライズ可能にしておく必要があります。 あるいは代わりにScriptableObjectを継承してもかまいません。

基底クラスの都合でわざわざ設定内容をフィールドにコピーしていますが、 設定用のクラス自体をそのままフィールドとして持てば、 先ほどの方法と同程度に簡単に書けます。

EditorPrefsのキーには前述の方法同様ユニークな値を使う必要があります。

利点と欠点

この方法の利点と欠点は前述の二つの方法を足して2で割った感じです。

専用のクラスを書かなければならないため少しだけ手間ですが、キーの管理や一括削除は楽です。 部分的な保存・復元・削除はやはり苦手ですが、部分的な復元はできたりと少しだけ緩和されています。

保存されるJSONは次の通り、すっきりとしたものになります。

{
    "m_IntValue":  1234567890,
    "m_FloatValue":  0.125,
    "m_StringValue":  "テキストだよ^_^"
}

おわりに

JSON云々は思いついたときにはすごくいい方法のように思えたのですが、 記事書きながらまとめてみると果たしてどの方法がいいのやらという感じです。 実際にいろいろ書いてみないとわかりませんね。 特にUnityのJSON変換関係は少しクセがあるみたいなので。

それにしても、またしょうもない内容で長々と書いてしまった・・・。
もっと簡潔に「こうすればこれができるよ」くらいで書いた方が需要あるんだろうけど、 性分なのか思いついたことを全部書きたくなってしまうんですよね。 結果的に初心者向けに書いていたはずがとっつきにくい文章に・・・。

まあきっとどこかに需要はあるでしょう、多分。



執筆時のUnityのバージョン:5.4.0f3

*1:ただし、保存したい変数はシリアライズされる状態である必要があります(publicで宣言するか[SerializeField]をつける)