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 }
内容としてはシリアライズされる変数の定義とその編集用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はテキスト形式なので、EditorPrefs
のSetString
とGetString
で保存・復元できます。
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
を使っています。
これはEditorJsonUtility
はUnityEngine.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]をつける)