第3章 データの保存 - エディター拡張入門

第3章 データの保存

エディター拡張で機能を実装している場合、値を保存して次回以降も使いまわしたい時があります。その値はエディター拡張の設定に関するパラメーターであったり、ゲームに関係するパラメーターであったりさまざまです。Unity にはデータを保存する手段が大きく分けて 3 パターンあります。本章では、それらを紹介し目的に合った手段を選べるように解説していきます。

3.1 EditorPrefs

PC 内で共有できるデータの保存方法です。プロジェクトに縛られず、Unity エディターをまたいで値を共有するのに適しています。

影響範囲

保存した値はメジャーバージョンごとの Unity エディターに影響します。

Unity 4.x で保存した値は、Unity 4.x でのみ扱えます。Unity 5.x もまた Unity 5.x でのみ扱えます。

Unity4.x のみ影響。Unity5.x のみ影響というように分かれる

図3.1: Unity4.x のみ影響。Unity5.x のみ影響というように分かれる

何を保存するか

EditorPrefs で保存すべきものはウィンドウの位置・サイズUnity エディターの環境設定(Preferences にあるような設定)の値です。独自アセットでも環境に関する設定であれば EditorPrefs を使用するようにしてください。注意として EditorPrefs 経由で保存される値はすべて平文で保存されます。決して、パスワードなど重要な情報は保存しないようにしてください。

EditorPrefs が保存されている場所

表3.1:

プラットフォーム 場所
Windows(Unity4.x) HKEY_CURRENT_USER\Software\Unity Technologies\UnityEditor 4.x
Windows(Unity5.x) HKEY_CURRENT_USER\Software\Unity Technologies\UnityEditor 5.x
Mac OS X(Unity4.x) ~/Library/Preferences/com.unity3d.UnityEditor4.x.plist
Mac OS X(Unity5.x) ~/Library/Preferences/com.unity3d.UnityEditor5.x.plist

EditorPrefs はメジャーバージョンごとに分けて保存されます。特に Windows はレジストリに値を保存します。EditorPrefs 経由「のみ」であれば問題はないのですが、直接レジストリを触ることもできてしまうので、その過程で誤った設定をしてしまい、最悪 Windows が起動しなくなるということもありえます。十分気をつけてください。

com.unity3d.UnityEditor5.x.plist を Xcode で開いた状態

図3.2: com.unity3d.UnityEditor5.x.plist を Xcode で開いた状態

使い方

OnEnable など 1度しか呼ばれないメソッド内のタイミングで値を取得します。値の変更タイミングで EditorPrefs に保存するようにしましょう。

using UnityEngine;
using UnityEditor;

public class ExampleWindow : EditorWindow
{
    int intervalTime = 60;
    const string AUTO_SAVE_INTERVAL_TIME = "AutoSave interval time (sec)";


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

    void OnEnable ()
    {
        intervalTime = EditorPrefs.GetInt (AUTO_SAVE_INTERVAL_TIME, 60);
    }

    void OnGUI ()
    {
        EditorGUI.BeginChangeCheck ();

        //シーン自動保存間隔(秒)
        intervalTime = EditorGUILayout.IntSlider ("間隔(秒)", intervalTime, 1, 3600);

        if (EditorGUI.EndChangeCheck ())
            EditorPrefs.SetInt (AUTO_SAVE_INTERVAL_TIME, intervalTime);
    }
}

また、ウィンドウのサイズを保存する場合は、それほど値の重要性も高くないので OnDisable で値の保存をするのが適しています。決して OnGUI で毎回保存しないようにしてください。OnGUI の様な、多く呼び出されるメソッドで書き込み作業をやると高負荷となってしまいます。

using UnityEngine;
using UnityEditor;

public class ExampleWindow : EditorWindow
{
    const string SIZE_WIDTH_KEY = "ExampleWindow size width";
    const string SIZE_HEIGHT_KEY = "ExampleWindow size height";

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

    void OnEnable ()
    {
        var width = EditorPrefs.GetFloat (SIZE_WIDTH_KEY, 600);
        var height = EditorPrefs.GetFloat (SIZE_HEIGHT_KEY, 400);
        position = new Rect (position.x, position.y, width, height);
    }

    void OnDisable ()
    {
        EditorPrefs.SetFloat (SIZE_WIDTH_KEY, position.width);
        EditorPrefs.SetFloat (SIZE_HEIGHT_KEY, position.height);
    }
}

3.2 EditorUserSettings.Set/GetConfigValue

プロジェクト内で共有できるデータの保存方法です。ここで保存された値は暗号化されています。個人情報であるパスワードなどを保存するのに適しています。

影響範囲 と 保存場所

この API で保存したデータは、プロジェクト内のみ影響します。データの保存先が Library/EditorUserSettings.asset であるため、Library フォルダーを他人と共有しない限りは情報が他人と共有されることはありません。*2

何を保存するか

さまざまなツールを使っていると、ログインのためのメールアドレスやパスワードが必要になってきたりします。Oauth のアクセストークンなどもその1つです。

EditorUserSettings.asset はバイナリ形式で保存されており簡単には中身を見れないようになっています。とはいっても Unity が提供している binary2text を使用することでバイナリをテキスト形式に変換し、見ることができてしまうので注意してください。

使い方

試しにデータを保存してみます。

using UnityEditor;

public class NewBehaviourScript
{
    [InitializeOnLoadMethod]
    static void SaveConfig ()
    {
        EditorUserSettings.SetConfigValue ("Data 1", "text");
    }
}

保存できたかを確かめてみましょう。EditorUserSettings.asset はバイナリ形式なのでテキスト形式にします。

cd /Applications/Unity/Unity.app/Contents/Tools
./binary2text /path/to/unityproject/Library/EditorUserSettings.asset

値が、暗号化されて保存されていることがわかりました。

External References


ID: 1 (ClassID: 162) EditorUserSettings
    m_ObjectHideFlags 0 (unsigned int)
    m_ConfigValues  (map)
        size 2 (int)
        data  (pair)
            first "Data 1" (string)
            second "17544c12" (string)
        data  (pair)
            first "vcSharedLogLevel" (string)
            second "0a5f5209" (string)

    m_VCAutomaticAdd 1 (bool)
    m_VCDebugCom 0 (bool)
    m_VCDebugCmd 0 (bool)
    m_VCDebugOut 0 (bool)
    m_SemanticMergeMode 2 (int)
    m_VCShowFailedCheckout 1 (bool)

3.3 ScriptableObject

プロジェクト内で共有できるデータの保存方法。さまざまな応用が効く保存方法です。チーム内で共有したい設定や、大量のデータを保存したい場合にこの方法を選択します。

影響範囲

ScriptableObject が Unity プロジェクト内でデータを保存するための主役となる形式です。Unity プロジェクト内にアセットとして保存することでいつでもデータを保存でき、いつでもスクリプトからデータを読み込むことができます。

インスペクターで値の編集を行うことができる

図3.3: インスペクターで値の編集を行うことができる

using UnityEngine;
using UnityEditor;

[CreateAssetMenu]
public class NewBehaviourScript : ScriptableObject
{
    [Range(0,10)]
    public int number = 3;

    public bool toggle = false;

    public string[] texts = new string[5];
}

何を保存するか

エディター拡張で作成したアセットのデータや設定ファイル、ビルド後にゲームデータとして使うデータベースとしての役割を持たせることができます。

ScriptableObject を保存する場所

Project の Assets フォルダー以下であればどこにでも保存できます。エディター拡張専用の ScriptableObject であれば Editor フォルダー以下に作成するのが好ましいです。

使い方

情報量が多いので詳しくは 第4章「ScriptableObject」 で紹介します。

3.4 JSON

テキスト形式の軽量なデータ記述言語の 1 つです。一般的に Web やサーバーからデータを取得するときのデータ形式として扱われていますが、その限りではなく幅広い分野で利用されています。

Unity 5.3 より、正式に JsonUtility クラスが追加され、JSONに正式対応が行われました。

ただ、みなさんが普段使用している JSON ライブラリよりは高速ですが高性能ではなく、使用が限られています。

オブジェクトを JSON に変換する条件は Unity のシリアライザでの条件と同じで、次の通りです。

 1: クラスに [Serializable] 属性があること。
 2: フィールドに [SerializeField] 属性があること、または public 変数であること。
 3: 他、詳細は第5章「SerializedObject について」を参照してください。

Unityのシリアライザを使用するということは、シリアライザが対応できないものはJSONにできないということです。

 1: Dictionary はシリアライズできない
 2: object[]、List<object> のような object 配列はシリアライズできない
 3: 配列オブジェクトをそのまま渡してもシリアライズできない(JsonUtility.ToJson(List<T>) ができない)

使い方

使い方は単純で JsonUtility.ToJsonJsonUtility.FromJson を呼び出すことでオブジェクトと JSON の変換が可能です。

[Serializable]
public class Example
{
    [SerializeField]
    string name = "hoge";

    [SerializeField]
    int number = 10;
}

/*
次のJSONデータが出力される
{
    "name": "hoge",
    "number": 10
}
*/
Debug.Log(JsonUtility.ToJson(new Example(), true));

JsonUtility と EditorJsonUtility

JsonUtility では UnityEngine.Object は JSON に変換できません(変換できないとされているが ScriptableObject を含む一部のオブジェクトは可能)。

UnityEngine.Object を JSON に変換するにはエディター専用の EditorJsonUtility を使用します。ただし、 EditorJsonUtility は、配列には対応しておらず少し工夫が必要です。

EditorJsonUtility の配列対応は、少し無理がありますが最終的なJSON形式を文字列の連結で作成します。

/*
次のような JSON を取得できる
{"key名":[{"name":"hoge"},{"name":"hoge"}]}
*/
public static string ToJson(string key, Object[] objs)
{
    var json = objs.Select(obj => EditorJsonUtility.ToJson(obj)).ToArray();
    var values = string.Join(",", json);
    return string.Format("{\"{0}\":{1}]}", key, values);;
}

配列の扱い

多くの Json ライブラリでは配列のシリアライズもできるようになっています。ですが、同じ流れで Unity の JsonUtility を使用してもシリアライズは行われません。

var list = new List<Example>{
  new Example(),
  new Example()
};

/*
{} が返る。
取得したいのは次のような配列
[{"name":"hoge","number":10},{"name":"hoge","number":10}]
*/
JsonUtility.ToJson(list)

「どうしても配列のみをシリアライズしたい。」ということであれば工夫が必要になります。

シリアライズ可能なクラスのフィールド変数であればシリアライズできる

シリアライズできるものはクラスのフィールド変数なので、まずはその状態を作り出します。

次のコードは、Listをシリアライズするために汎用性を考えて作成した SerializableList クラスです。

/*
使い方は List とほぼ同じ(AddRange がないので自作する)
*/
[Serializable]
public class SerializableList<T> : Collection<T>, ISerializationCallbackReceiver
{
    [SerializeField]
    List<T> items;

    public void OnBeforeSerialize()
    {
        items = (List<T>)Items;
    }

    public void OnAfterDeserialize()
    {
        Clear();
        foreach (var item in items)
            Add(item);
    }
}

これを JsonUtility でシリアライズすると、次の結果が得られます。

var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};

/*
次のようなJSONが出力される。
{"items":[{"name":"hoge","number":10},{"name":"hoge","number":10}]}
*/
Debug.Log(JsonUtility.ToJson(serializedList));

ここで押さえるべきなのは ISerializationCallbackReceiver の存在です。JsonUtility で JSON に変換するときに、ISerializationCallbackReceiver の OnBeforeSerializeOnAfterDeserialize が呼び出されます。これを利用して、ToJson 呼び出すときのみオブジェクトをシリアライズ可能なフィールドへと代入することで目的が達成できます。

シリアライズできたのはいいのですが、最終的には JSON 形式ではなく配列のみの表示が好ましいです("items" のキーがいらないということ)。

そこで SerializableList クラス内に ToJson メソッドを作成して文字列のカスタマイズが行えるようにします。

public string ToJson()
{
    var result = "[]"
    var json = JsonUtility.ToJson(this);
    var regex = new Regex("^{\"items\":(?<array>.*)}$");
    var match = regex.Match(json);
    if (match.Success)
        result = match.Groups["array"].Value;

    return result;
}

この ToJson メソッドを使用すると、次の結果が得られます。

var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};

/*
次のような文字列が出力される。
[{"name":"hoge","number":10},{"name":"hoge","number":10}]
*/
Debug.Log(serializedList.ToJson());

ただこのやり方だとデシリアライズができなくなるので FromJson も自作します。

public static SerializableList<T> FromJson(string arrayString)
{
    var json = "{\"items\":" + arrayString + "}";
    return JsonUtility.FromJson<SerializableList<T>>(json);
}

これでデシリアライズもできるようになりました。

var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};

var json = serializedList.ToJson();
var serializableList = SerializableList<Example>.FromJson(json);
// Example オブジェクトが 2 つ取得できている
Debug.Log(serializableList.Count == 2);

SerializableList.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using UnityEngine;

[Serializable]
public class SerializableList<T> : Collection<T>, ISerializationCallbackReceiver
{
    [SerializeField]
    List<T> items;

    public void OnBeforeSerialize()
    {
        items = (List<T>)Items;
    }

    public void OnAfterDeserialize()
    {
        Clear();
        foreach (var item in items)
            Add(item);
    }

    public string ToJson(bool prettyPrint = false)
    {
        var result = "[]";
        var json = JsonUtility.ToJson(this, prettyPrint);
        var pattern = prettyPrint ? "^\\{\n\\s+\"items\":\\s(?<array>.*)\n\\s+\\]\n}$" : "^{\"items\":(?<array>.*)}$";
        var regex = new Regex(pattern, RegexOptions.Singleline);
        var match = regex.Match(json);
        if (match.Success)
        {
            result = match.Groups["array"].Value;
            if (prettyPrint)
                result += "\n]";
        }
        return result;
    }

    public static SerializableList<T> FromJson(string arrayString)
    {
        var json = "{\"items\":" + arrayString + "}";

        return JsonUtility.FromJson<SerializableList<T>>(json);
    }
}

Dictionary の扱い

JsonUtility で Dictionary の扱いは非常に難しいことになります。まず、一般的な JSON ライブラリのようなシリアライズは Unity ではできないため、ほぼすべての機能を自作することになります。こうなってしまっては JsonUtility を使用する意味もなくなってしまうので MiniJSON*1 を使用するか Unity に対応している他の JSON ライブラリを使用したほうが無難です。

[*2] 他人との共有・バージョン管理では Library フォルダーは共有してはいけません。.meta ファイルを共有するようにしてください。

第2章 標準で使えるエディター拡張機能 第4章 ScriptableObject