Help us understand the problem. What is going on with this article?

JsonUtilityを使ってDictionaryを含むクラスをシリアライズする (unity)

前提

  • unity 2018.4.2f1

やりたいこと

  • クラスを丸ごとセーブロードできるように、任意のクラスのシリアライズ/デシリアライズ(stringとの相互変換)を低コストで実現したい。

これまでやっていたこと

オレオレJsonシリアライズの自前実装 (蛇足なので隠蔽)

オレオレJsonシリアライズ

  • オレオレJsonは、対になる括弧とセパレータをユニークにすることで、解析を簡単にしました。
    • {1>"a":"あ"<1>"b":{2>"A":"ああ"<2>"B":"いい"<2}<1>"c":"う"<1}みたいな感じです。
  • 必要なクラス全てで以下を実装しました。
    • ToString ()をオーバーライドして、シリアライズしたいメンバーをToString ()して連結する感じで、オレオレJsonを吐かせます。(下記コードを参照)
    • string jsonを受け取るコンストラクタで、メンバーの名前で要素を取り出してはnewする感じで、オレオレJsonからインスタンスを再現します。
  • また、ジェネリックなユーティリティ関数群を使って記述を簡素化し、Dictionaryを含む様々な型にも対応させます。
  • 使い勝手は悪くないのですが、stringを取り回すせいで、大規模に使うとGC Allocが大量発生してよろしくないです。

使用時のイメージ

public Preference () {
    this.version = CurrentVersion;
    this.CurrentSlot = 0;
    this.Continue = false;
    this.seVolume = 0.5f;
    this.smVolume = 0f;
}
public Preference (string json) : this () {
    if (!string.IsNullOrEmpty (json)) {
        if (json == "load") {
            json = PlayerPrefs.GetString (Prefkey);
        }
        this.version = json.JsToValue ("version", string.Empty);
        this.CurrentSlot = json.JsToValue (int.Parse, "CurrentSlot", 0);
        this.Continue = json.JsToValue (bool.Parse, "Continue", false);
        this.seVolume = json.JsToValue (float.Parse, "seVolume", 0.5f);
        this.smVolume = json.JsToValue (float.Parse, "smVolume", 0f);
    }
}

public override string ToString () {
    return (new [] {
        this.version.ValueToJs ("version"),
        this.CurrentSlot.ValueToJs ("CurrentSlot"),
        this.Continue.ValueToJs ("Continue"),
        this.seVolume.ValueToJs ("seVolume"),
        this.smVolume.ValueToJs ("smVolume"),
    }).Brackets ();
}

やってみたこと

  • ISerializationCallbackReceiverを使った簡易な方法を採用しました。
  • シリアライズできない型とシリアライズ可能な型を相互に変換するだけで、シリアライズ自体はJsonUtilityにさせています。

コード

using System;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;

public class JsonUtilityDictionaryTest : MonoBehaviour {

    void Start () {
        var x = new ItemDict (new string [] { "a", "b", "c", "d", "e", "f" }); // 生成
        var y = JsonUtility.ToJson (x, true); // シリアライズ
        var z = JsonUtility.FromJson<ItemDict> (y); // デシリアライズ

        Debug.Log (x);
        Debug.Log (y);
        Debug.Log (z);
    }

}


[Serializable]
public class ItemDict : ISerializationCallbackReceiver {
    public int this [Item i] { get { return Items [i]; } }
    [NonSerialized] public Dictionary<Item, int> Items;
    [SerializeField] private List<Item> _keys;
    [SerializeField] private List<int> _values;

    public ItemDict (ICollection<string> names) {
        Items = new Dictionary<Item, int> { };
        foreach (var name in names) {
            Items.Add (new Item (name), Random.Range (0, 100));
        }
    }

    public void OnBeforeSerialize () {
        _keys = new List<Item> { };
        _values = new List<int> { };
        foreach (KeyValuePair<Item, int> keyvalue in Items) {
            _keys.Add (keyvalue.Key);
            _values.Add (keyvalue.Value);
        }
    }

    public void OnAfterDeserialize () {
        Items = new Dictionary<Item, int> { };
        for (var i = 0; i < _keys.Count; i++) {
            Items.Add (_keys [i], _values [i]);
        }
    }

    public override string ToString () {
        var items = new List<KeyValuePair<Item, int>> { };
        foreach (var item in Items) {
            items.Add (item);
        }
        return $"\"Items\":[ {string.Join (",", items.ConvertAll (item => $"{item.Key}:{item.Value}"))}]";
    }
}

[Serializable]
public class Item : ISerializationCallbackReceiver {

    [SerializeField] public string Name;
    [NonSerialized] public Guid Id;
    [SerializeField] private string _id;

    public Item (string name) {
        Id = Guid.NewGuid ();
        this.Name = name ?? Id.ToString ();
    }

    public void OnBeforeSerialize () {
        _id = Id.ToString ();
    }

    public void OnAfterDeserialize () {
        Id = Guid.Empty;
        Guid.TryParse (_id, out Id);
    }

    public override string ToString () {
        return $"{{\"name\":\"{Name}\", \"id\":\"{Id}\"}}";
    }

}

元のインスタンス

"Items":[ {"name":"a", "id":"83cc1660-fb42-4816-8852-61c2918b445f"}:40,{"name":"b", "id":"f667c4be-191b-4fcd-ba66-ccb4a5c0abde"}:72,{"name":"c", "id":"d48e1201-6c22-45e3-9394-7a7e205e67dc"}:39,{"name":"d", "id":"e31e2516-58e9-43e3-afed-b0292810dd40"}:31,{"name":"e", "id":"84f4ca56-9ccc-4a51-bf10-8ba7921d2bcc"}:82,{"name":"f", "id":"5019a795-6f7f-49d9-b3e2-3e89f936a2e3"}:13]

シリアライズ結果

{
    "_keys": [
        {
            "Name": "a",
            "_id": "83cc1660-fb42-4816-8852-61c2918b445f"
        },
        {
            "Name": "b",
            "_id": "f667c4be-191b-4fcd-ba66-ccb4a5c0abde"
        },
        {
            "Name": "c",
            "_id": "d48e1201-6c22-45e3-9394-7a7e205e67dc"
        },
        {
            "Name": "d",
            "_id": "e31e2516-58e9-43e3-afed-b0292810dd40"
        },
        {
            "Name": "e",
            "_id": "84f4ca56-9ccc-4a51-bf10-8ba7921d2bcc"
        },
        {
            "Name": "f",
            "_id": "5019a795-6f7f-49d9-b3e2-3e89f936a2e3"
        }
    ],
    "_values": [
        40,
        72,
        39,
        31,
        82,
        13
    ]
}

復元されたインスタンス

"Items":[ {"name":"a", "id":"83cc1660-fb42-4816-8852-61c2918b445f"}:40,{"name":"b", "id":"f667c4be-191b-4fcd-ba66-ccb4a5c0abde"}:72,{"name":"c", "id":"d48e1201-6c22-45e3-9394-7a7e205e67dc"}:39,{"name":"d", "id":"e31e2516-58e9-43e3-afed-b0292810dd40"}:31,{"name":"e", "id":"84f4ca56-9ccc-4a51-bf10-8ba7921d2bcc"}:82,{"name":"f", "id":"5019a795-6f7f-49d9-b3e2-3e89f936a2e3"}:13]

分かったこと

  • 実行の効率は良いのだと思いますが、記述が一般化できず、クラス毎に個別の処理を書かなければならないので、かなり面倒です。
  • List<T>は型次第で、扱えたり扱えなかったりするようです。
    • [SerializeField] private List<KeyValuePair<Item, int>> _keyvalues;とかはダメでした。
  • Dictionaryはダメっぽいです。
  • 執筆時点で、QiitaのCodeブロックのシンタックスハイライトは、文字列挿入(string interpolation)のエスケープに対応できていないようです。({{}}が認識されない)

参考

ありがとうございました。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away