Logo wizaman's blog (legacy)

Unityでシリアライズ可能なコレクション

September 4, 2016
15 min read
Table of Contents

引き続きUnityの記事を書き溜めていたので放出。

Unityでシリアライズ可能なコレクションは、Listだけです。但し、当然ですが、型引数もシリアライズ可能型を指定してやる前提です。

C#のListはいわゆる配列リストってやつで、C++でいうvector、JavaでいうArrayListに相当します。C#にもArrayListは存在しますが、古い機能のため非ジェネリックです。

ともかく、Listは内部実装として配列を持つため、List<T>ならば、T型配列としてUnityがシリアライズしてくれるわけですね。Listだけシリアライズ可能なのはまあ納得です。

とはいえ、Listだけで事足りるかというと、LinkedList(連結リスト、線形リスト)やDictionary(ハッシュマップ)あたりは欲しい感じです。

ScriptableObjectやJsonUtilityでシリアライズ・デシリアライズするとき、ISerializationCallbackReceiverを実装していると、シリアライズ前、デシリアライズ後に決まったメソッドが呼ばれます。これを利用して**コレクションをListに変換してシリアライズできます。**公式ドキュメントでもこれを利用してDictionaryをシリアライズ・デシリアライズするサンプルが掲載されています。

※OnBeforeSerializeとOnAfterDeserializeを実装しますが、何故かOnBeforeSerializeに解説が集中しているのでそっちのリンクを掲載。

この場合、インスペクタを介したリアルタイムな編集はできませんが、永続化が目的ならばこれで十分でしょう。LinkedListやDictionaryをインスペクタで触れるようにするとむしろ怖い。

そんなこんなで、Unityで使用可能なC#のコレクションをISerializationCallbackReceiver実装クラスで一通りラップしてみたので、以下に公開します。

実装

ラップされたコレクションとほぼ同じインターフェースと機能を提供するように配慮しています。

SerializableCollection.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
[Serializable]
public abstract class AbstractSerializableCollection<T, Collection> : ISerializationCallbackReceiver, ICollection, IEnumerable<T>
    where Collection : class, ICollection, IEnumerable<T>, new()
{
    public Collection collection { get { return _collection; } set { _collection = value; } }
 
    public int Count { get { return _collection.Count; } }
    public bool IsSynchronized { get { return _collection.IsSynchronized; } }
    public object SyncRoot { get { return _collection.SyncRoot; } }
 
    public void CopyTo(Array array, int index) { _collection.CopyTo(array, index); }
 
    public IEnumerator<T> GetEnumerator() { return _collection.GetEnumerator(); }
    IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
 
    public AbstractSerializableCollection()
    {
        _collection = new Collection();
    }
    public AbstractSerializableCollection(Collection collection)
    {
        _collection = (collection != null) ? collection : new Collection();
    }
 
    public virtual void OnBeforeSerialize()
    {
        _ = new List<T>(Count);
        _.AddRange(_collection);
    }
 
    public virtual void OnAfterDeserialize()
    {
        onDeserialized(_);
        _ = null;
    }
 
    public void clearSerializableValues()
    {
        _ = null;
    }
 
    protected abstract void onDeserialized(List<T> values);
 
    private Collection _collection = null;
 
    [SerializeField]
    private List<T> _ = null;
}
 
[Serializable]
public class SerializableLinkedList<T> : AbstractSerializableCollection<T, LinkedList<T>>
{
    public LinkedListNode<T> First { get { return collection.First; } }
    public LinkedListNode<T> Last { get { return collection.Last; } }
 
    public SerializableLinkedList() { }
    public SerializableLinkedList(IEnumerable<T> values) : base(new LinkedList<T>(values)) { }
 
    public bool Contains(T value) { return collection.Contains(value); }
 
    public LinkedListNode<T> AddBefore(LinkedListNode<T> node, T value) { return collection.AddBefore(node, value); }
    public void AddBefore(LinkedListNode<T> node, LinkedListNode<T> newNode) { collection.AddBefore(node, newNode); }
    public LinkedListNode<T> AddAfter(LinkedListNode<T> node, T value) { return collection.AddAfter(node, value); }
    public void AddAfter(LinkedListNode<T> node, LinkedListNode<T> newNode) { collection.AddAfter(node, newNode); }
 
    public LinkedListNode<T> AddFirst(T value) { return collection.AddFirst(value); }
    public void AddFirst(LinkedListNode<T> node) { collection.AddFirst(node); }
    public LinkedListNode<T> AddLast(T value) { return collection.AddLast(value); }
    public void AddLast(LinkedListNode<T> node) { collection.AddLast(node); }
 
    public void Clear() { collection.Clear(); }
 
    public LinkedListNode<T> Find(T value) { return collection.Find(value); }
    public LinkedListNode<T> FindLast(T value) { return collection.FindLast(value); }
 
    public bool Remove(T value) { return collection.Remove(value); }
    public void Remove(LinkedListNode<T> node) { collection.Remove(node); }
    public void RemoveFirst() { collection.RemoveFirst(); }
    public void RemoveLast() { collection.RemoveLast(); }
 
    protected override void onDeserialized(List<T> values)
    {
        Clear();
        foreach(var value in values) {
            AddLast(value);
        }
    }
}
 
[Serializable]
public class SerializableQueue<T> : AbstractSerializableCollection<T, Queue<T>>
{
    public bool Contains(T item) { return collection.Contains(item); }
 
    public SerializableQueue() { }
    public SerializableQueue(int capacity) : base(new Queue<T>(capacity)) { }
    public SerializableQueue(IEnumerable<T> values) : base(new Queue<T>(values)) { }
 
    public T Peek() { return collection.Peek(); }
    public void Enqueue(T value) { collection.Enqueue(value); }
    public T Dequeue() { return collection.Dequeue(); }
    public void Clear() { collection.Clear(); }
 
    public T[] ToArray() { return collection.ToArray(); }
    public void TrimExcess() { collection.TrimExcess(); }
 
    protected override void onDeserialized(List<T> values)
    {
        Clear();
        collection = new Queue<T>(values.Count);
        foreach(var value in values) {
            collection.Enqueue(value);
        }
    }
}
 
[Serializable]
public class SerializableStack<T> : AbstractSerializableCollection<T, Stack<T>>
{
    public bool Contains(T value) { return collection.Contains(value); }
 
    public SerializableStack() { }
    public SerializableStack(int capacity) : base(new Stack<T>(capacity)) { }
    public SerializableStack(IEnumerable<T> values) : base(new Stack<T>(values)) { }
 
    public T Peek() { return collection.Peek(); }
    public T Pop() { return collection.Pop(); }
    public void Push(T value) { collection.Push(value); }
    public void Clear() { collection.Clear(); }
 
    public T[] ToArray() { return collection.ToArray(); }
    public void TrimExcess() { collection.TrimExcess(); }
 
    protected override void onDeserialized(List<T> values)
    {
        Clear();
        collection = new Stack<T>(values.Count);
 
        // スタックなので逆順に詰める必要がある
        for(int i = values.Count - 1; i >= 0; --i) {
            Push(values[i]);
        }
    }
}
 
[Serializable]
public class SerializableGenericCollection<T, Collection> : ISerializationCallbackReceiver, ICollection<T>
    where Collection : class, ICollection<T>, new()
{
    public Collection collection { get { return _collection; } set { _collection = value; } }
 
    public int Count { get { return _collection.Count; } }
    public bool IsReadOnly { get { return _collection.IsReadOnly; } }
 
    public SerializableGenericCollection()
    {
        _collection = new Collection();
    }
    public SerializableGenericCollection(Collection collection)
    {
        _collection = (collection != null) ? collection : new Collection();
    }
 
    public bool Contains(T item) { return _collection.Contains(item); }
 
    public void Add(T item) { _collection.Add(item); }
    public bool Remove(T item) { return _collection.Remove(item); }
    public void Clear() { _collection.Clear(); }
 
    public void CopyTo(T[] array, int arrayIndex) { _collection.CopyTo(array, arrayIndex); }
 
    public IEnumerator<T> GetEnumerator() { return _collection.GetEnumerator(); }
    IEnumerator IEnumerable.GetEnumerator() { return _collection.GetEnumerator(); }
 
    public virtual void OnBeforeSerialize()
    {
        _ = new List<T>(Count);
        _.AddRange(_collection);
    }
 
    public virtual void OnAfterDeserialize()
    {
        onDeserialized(_);
        _ = null;
    }
 
    public void clearSerializableValues()
    {
        _ = null;
    }
 
    protected virtual void onDeserialized(List<T> values)
    {
        Clear();
        foreach(var value in _) {
            Add(value);
        }
    }
 
    private Collection _collection = null;
 
    [SerializeField]
    private List<T> _ = null;
}
 
[Serializable]
public class SerializableHashSet<T> : SerializableGenericCollection<T, HashSet<T>>
{
    public SerializableHashSet() { }
    public SerializableHashSet(IEqualityComparer<T> comparer) : base(new HashSet<T>(comparer)) { }
    public SerializableHashSet(IEnumerable<T> values) : base(new HashSet<T>(values)) { }
    public SerializableHashSet(IEnumerable<T> values, IEqualityComparer<T> comparer) : base(new HashSet<T>(values, comparer)) { }
 
    public IEqualityComparer<T> Comparer { get { return collection.Comparer; } }
 
    public int RemoveWhere(Predicate<T> predicate) { return collection.RemoveWhere(predicate); }
 
    public void UnionWith(IEnumerable<T> other) { collection.UnionWith(other); }
    public void ExceptWith(IEnumerable<T> other) { collection.ExceptWith(other); }
    public void IntersectWith(IEnumerable<T> other) { collection.IntersectWith(other); }
    public void SymmetricExceptWith(IEnumerable<T> other) { collection.SymmetricExceptWith(other); }
 
    public bool IsProperSubsetOf(IEnumerable<T> other) { return collection.IsProperSubsetOf(other); }
    public bool IsProperSupersetOf(IEnumerable<T> other) { return collection.IsProperSupersetOf(other); }
    public bool IsSubsetOf(IEnumerable<T> other) { return collection.IsSubsetOf(other); }
    public bool IsSupersetOf(IEnumerable<T> other) { return collection.IsSupersetOf(other); }
    public bool Overlaps(IEnumerable<T> other) { return collection.Overlaps(other); }
    public bool SetEquals(IEnumerable<T> other) { return collection.SetEquals(other); }
 
    public void TrimExcess() { collection.TrimExcess(); }
 
    public void CopyTo(T[] array) { collection.CopyTo(array); }
    public void CopyTo(T[] array, int index, int count) { collection.CopyTo(array, index, count); }
}
 
[Serializable]
public class SerializableGenericDictionayEntry<TKey, TValue>
{
    public TKey k = default(TKey);
    public TValue v = default(TValue);
}
 
[Serializable]
public class SerializableGenericDictionay<TKey, TValue, Entry, Collection> : ISerializationCallbackReceiver, IDictionary<TKey, TValue>
    where Entry : SerializableGenericDictionayEntry<TKey, TValue>, new()
    where Collection : class, IDictionary<TKey, TValue>, new()
{
    public Collection collection { get { return _collection; } set { _collection = value; } }
 
    public int Count { get { return _collection.Count; } }
    public bool IsReadOnly { get { return _collection.IsReadOnly; } }
 
    public ICollection<TKey> Keys { get { return _collection.Keys; } }
    public ICollection<TValue> Values { get { return _collection.Values; } }
 
    public TValue this[TKey key] { get { return _collection[key]; } set { _collection[key] = value; } }
 
    public SerializableGenericDictionay()
    {
        _collection = new Collection();
    }
    public SerializableGenericDictionay(Collection collection)
    {
        _collection = (collection != null) ? collection : new Collection();
    }
 
    public virtual void OnBeforeSerialize()
    {
        _ = new List<Entry>(Count);
        foreach(var pair in _collection) {
            var entry = new Entry();
            entry.k = pair.Key;
            entry.v = pair.Value;
            _.Add(entry);
        }
    }
 
    public virtual void OnAfterDeserialize()
    {
        onDeserialized(_);
        _ = null;
    }
 
    public void clearSerializableValues()
    {
        _ = null;
    }
 
    protected virtual void onDeserialized(List<Entry> entries)
    {
        Clear();
        foreach(var entry in entries) {
            Add(entry.k, entry.v);
        }
    }
 
    public bool TryGetValue(TKey key, out TValue value) { return _collection.TryGetValue(key, out value); }
 
    public bool Contains(KeyValuePair<TKey, TValue> item) { return _collection.Contains(item); }
    public bool ContainsKey(TKey key) { return _collection.ContainsKey(key); }
 
    public void Add(TKey key, TValue value) { _collection.Add(key, value); }
    public void Add(KeyValuePair<TKey, TValue> item) { _collection.Add(item); }
    public bool Remove(TKey key) { return _collection.Remove(key); }
    public bool Remove(KeyValuePair<TKey, TValue> item) { return _collection.Remove(item); }
    public void Clear() { _collection.Clear(); }
 
    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) { _collection.CopyTo(array, arrayIndex); }
 
    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() { throw new NotImplementedException(); }
    IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); }
 
    private Collection _collection = new Collection();
 
    [SerializeField]
    private List<Entry> _ = null;
}
 
[Serializable]
public class SerializableDictionay<TKey, TValue, Entry> : SerializableGenericDictionay<TKey, TValue, Entry, Dictionary<TKey, TValue>>
    where Entry : SerializableGenericDictionayEntry<TKey, TValue>, new()
{
    public SerializableDictionay() { }
    public SerializableDictionay(int capacity) : base(new Dictionary<TKey, TValue>(capacity)) { }
    public SerializableDictionay(IDictionary<TKey, TValue> dictionary) : base(new Dictionary<TKey, TValue>(dictionary)) { }
    public SerializableDictionay(IEqualityComparer<TKey> comparer) : base(new Dictionary<TKey, TValue>(comparer)) { }
    public SerializableDictionay(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) : base(new Dictionary<TKey, TValue>(dictionary, comparer)) { }
    public SerializableDictionay(int capacity, IEqualityComparer<TKey> comparer) : base(new Dictionary<TKey, TValue>(capacity, comparer)) { }
 
    public IEqualityComparer<TKey> Comparer { get { return collection.Comparer; } }
 
    public bool ContainsValue(TValue value) { return collection.ContainsValue(value); }
 
    protected override void onDeserialized(List<Entry> entries)
    {
        Clear();
        collection = new Dictionary<TKey, TValue>(entries.Count);
        foreach(var entry in entries) {
            Add(entry.k, entry.v);
        }
    }
}
 
[Serializable]
public class SerializableSortedDictionay<TKey, TValue, Entry> : SerializableGenericDictionay<TKey, TValue, Entry, SortedDictionary<TKey, TValue>>
    where Entry : SerializableGenericDictionayEntry<TKey, TValue>, new()
{
    public SerializableSortedDictionay() { }
    public SerializableSortedDictionay(IDictionary<TKey, TValue> dictionary) : base(new SortedDictionary<TKey, TValue>(dictionary)) { }
    public SerializableSortedDictionay(IComparer<TKey> comparer) : base(new SortedDictionary<TKey, TValue>(comparer)) { }
    public SerializableSortedDictionay(IDictionary<TKey, TValue> dictionary, IComparer<TKey> comparer) : base(new SortedDictionary<TKey, TValue>(dictionary, comparer)) { }
 
    public IComparer<TKey> Comparer { get { return collection.Comparer; } }
 
    public bool ContainsValue(TValue value) { return collection.ContainsValue(value); }
}

サンプル

ジェネリッククラスのままではシリアライズできないため、下記のように継承して使います。やや面倒ですが、辞書型はキーと値のペアとなる非ジェネリッククラスも指定してやる必要があります。

CollectionTest.cs

using System;
using UnityEngine;
 
public class CollectionTest : MonoBehaviour
{
    [Serializable]
    private class IntLinkedList : SerializableLinkedList<int> { }
 
    [Serializable]
    private class IntQueue : SerializableQueue<int> { }
 
    [Serializable]
    private class IntStack : SerializableStack<int> { }
 
    [Serializable]
    private class IntHashSet : SerializableHashSet<int> { }
 
    [Serializable]
    private class IntStringEntry : SerializableGenericDictionayEntry<int, string> { }
 
    [Serializable]
    private class IntStringDictionary : SerializableDictionay<int, string, IntStringEntry> { }
 
    [Serializable]
    private class IntStringSortedDictionary : SerializableSortedDictionay<int, string, IntStringEntry> { }
 
    private void Start()
    {
        var intLinkedList = new IntLinkedList();
        var intQueue = new IntQueue();
        var intStack = new IntStack();
        var intHashSet = new IntHashSet();
        var intStringDictionary = new IntStringDictionary();
        var intStringSortedDictionary = new IntStringSortedDictionary();
 
        for(int i = 0; i < 10; ++i) {
            var value = i / 2;
            intLinkedList.AddLast(value);
            intQueue.Enqueue(value);
            intStack.Push(value);
            intHashSet.Add(value);
            intStringDictionary[i] = i.ToString();
            intStringSortedDictionary[i] = i.ToString();
        }
 
        testSerialize(intLinkedList);
        testSerialize(intQueue);
        testSerialize(intStack);
        testSerialize(intHashSet);
        testSerialize(intStringDictionary);
        testSerialize(intStringSortedDictionary);
    }
 
    private void testSerialize<T>(T collection)
    {
        var json = JsonUtility.ToJson(collection);
        Debug.Log(typeof(T).Name + ": " + json);
 
        var obj = JsonUtility.FromJson<T>(json);
        json = JsonUtility.ToJson(obj);
        Debug.Log(typeof(T).Name + ": " + json);
    }
}

ログ出力は下記のようになりました。

IntLinkedList: {"_":[0,0,1,1,2,2,3,3,4,4]}
IntLinkedList: {"_":[0,0,1,1,2,2,3,3,4,4]}
IntQueue: {"_":[0,0,1,1,2,2,3,3,4,4]}
IntQueue: {"_":[0,0,1,1,2,2,3,3,4,4]}
IntStack: {"_":[4,4,3,3,2,2,1,1,0,0]}
IntStack: {"_":[4,4,3,3,2,2,1,1,0,0]}
IntHashSet: {"_":[0,1,2,3,4]}
IntHashSet: {"_":[0,1,2,3,4]}
IntStringDictionary: {"_":[{"k":0,"v":"0"},{"k":1,"v":"1"},{"k":2,"v":"2"},{"k":3,"v":"3"},{"k":4,"v":"4"},{"k":5,"v":"5"},{"k":6,"v":"6"},{"k":7,"v":"7"},{"k":8,"v":"8"},{"k":9,"v":"9"}]}
IntStringDictionary: {"_":[{"k":0,"v":"0"},{"k":1,"v":"1"},{"k":2,"v":"2"},{"k":3,"v":"3"},{"k":4,"v":"4"},{"k":5,"v":"5"},{"k":6,"v":"6"},{"k":7,"v":"7"},{"k":8,"v":"8"},{"k":9,"v":"9"}]}
IntStringSortedDictionary: {"_":[{"k":0,"v":"0"},{"k":1,"v":"1"},{"k":2,"v":"2"},{"k":3,"v":"3"},{"k":4,"v":"4"},{"k":5,"v":"5"},{"k":6,"v":"6"},{"k":7,"v":"7"},{"k":8,"v":"8"},{"k":9,"v":"9"}]}
IntStringSortedDictionary: {"_":[{"k":0,"v":"0"},{"k":1,"v":"1"},{"k":2,"v":"2"},{"k":3,"v":"3"},{"k":4,"v":"4"},{"k":5,"v":"5"},{"k":6,"v":"6"},{"k":7,"v":"7"},{"k":8,"v":"8"},{"k":9,"v":"9"}]}

解説

元のコードが長いので多少の解説はしておきます。

まずは登場するクラスの継承関係を整理しましょう。下記のようになっています。

  • AbstractSerializableCollection<T, Collection>ICollection, IEnumerable<T>を実装)
    • SerializableLinkedList<T>
    • SerializableQueue<T>
    • SerializableStack<T>
  • SerializableGenericCollection<T, Collection>ICollection<T>を実装)
    • SerializableHashSet<T>
  • SerializableGenericDictionay<TKey, TValue, Entry, Collection>IDictionary<TKey, TValue>を実装)
    • SerializableDictionay<TKey, TValue, Entry>
    • SerializableSortedDictionay<TKey, TValue, Entry>

LinkedList、Queue、Stackのラッパークラスは、ICollectionを実装したAbstractSerializableCollectionから派生しています。AbstractSerializableCollectionはあまり多くの機能を提供しません。各コレクションに要素を追加・削除するメソッドがそもそもバラバラなので、最低限の実装に留めて、実際の追加・削除メソッドはラッパークラスのほうで定義してやる感じです。LinkedListであれば、AddFirst、AddLast、RemoveFirst、RemoveLastがありますし、Queueであれば、Enqueue、Dequeueがありますね。そんな感じで、元のコレクションとインターフェースを合わせるよう配慮しています。

しかし、HashSetはICollectionを実装していません。そのため、ICollection<T>を実装したSerializableGenericCollectionを別途定義しました。こちらにはAdd、Removeといった一般的なメソッドが既に実装済みです。

あとは辞書型ですが、KeyValuePair<TKey, TValue>はジェネリッククラスのためUnityがシリアライズしません。Unity公式のシリアライズサンプルでは、キーと値をそれぞれ別のリストにシリアライズしていますが、キーと値はセットで保存されていた方がデータの中身を見たときにわかりやすいし、ここのデータサイズをケチっても仕方ないんじゃないかと思いました。なので、ラッパークラスとしての単純さを損ないますが、KeyValuePair<TKey, TValue>に相当するようなSerializableGenericDictionayEntry<TKey, TValue>を定義してやって、これを継承した非ジェネリッククラスを型パラメータとして渡すこととしました。

また、コレクションによっては、コンストラクタの引数に初期容量を指定するものがあります。そういうコレクションについては、デシリアライズ時に容量指定して初期化することで、容量不足による再割当てをできるだけ回避するよう配慮してます。Dictionaryの容量については、下記の記事が参考になります。

注意

シリアライズ時にOnBeforeSerialize()が呼ばれるので、シリアライズ用データをここで構築していますが、シリアライズ後に呼ばれるコールバックが存在しません。つまり、シリアライズ用に用意した一時的なデータリストが、シリアライズ後も保持されてしまいます。メモリ的に無駄です。

仕方ないので、clearSerializableValues()というメソッドを用意しています。メモリ消費が気になる時は、これを適宜呼んでやって一時データを破棄してください。

まとめ

セーブデータとかに使えると思います。シリアライズ・デシリアライズの度に全要素をなめるので、あんまり膨大なコレクションには向きませんが、そういうケースではそもそもこの選択が間違いでしょう。膨大なデータにはデータベースの採用を検討すればいいんじゃないですかね。