はじめに
MemoryPack で UniRx の Reactive〇〇 のような ジェネリック型 をシリアライズする方法を備忘録として残しておきます。
UniRxの ReactiveProperty
, ReactiveCollection
, ReactiveDictionary
を例に出しますが、基本的にはどのジェネリック型にも対応できると思います。他のライブラリのクラスなど、自分では変更できないようなクラス向けです
環境は以下の通りです。
- Unity2021.3.17
- MemoryPack1.9.12
本記事の内容の実装とテストはこちら https://github.com/euglenach/MemoryPackSerializeRxTest
Zennに他にも色々なクラスをまとめてます(https://zenn.dev/euglenach/scraps/324467a4a4cfdd)
MemoryPackとは
MemoryPackのGithubやneuecc先生のブログを見ることをオススメします。
ジェネリック型の外部クラスをシリアライズする
ReactiveProperty
MemoryPackのReadmeの serialize-external-types にしたがってシリアライズ対応していきます。
ラッパーとフォーマッターのコード全文
#nullable enable
using MemoryPack;
using UniRx;
[MemoryPackable]
public readonly partial struct SerializableReactiveProperty<T>
{
[MemoryPackIgnore] public readonly ReactiveProperty<T> ReactiveProperty;
[MemoryPackInclude] private T Value => ReactiveProperty.Value;
[MemoryPackConstructor]
private SerializableReactiveProperty(T value)
{
ReactiveProperty = new(value);
}
public SerializableReactiveProperty(ReactiveProperty<T> reactiveProperty)
{
this.ReactiveProperty = reactiveProperty;
}
}
public class ReactivePropertyFormatter<T> : MemoryPackFormatter<ReactiveProperty<T>>
{
public override void Serialize(ref MemoryPackWriter writer, ref ReactiveProperty<T>? value)
{
if (value == null)
{
writer.WriteNullObjectHeader();
return;
}
writer.WritePackable(new SerializableReactiveProperty<T>(value));
}
public override void Deserialize(ref MemoryPackReader reader, ref ReactiveProperty<T>? value)
{
if (reader.PeekIsNull())
{
value = null;
reader.Advance(1);
return;
}
var wrapped = reader.ReadPackable<SerializableReactiveProperty<T>>();
value = wrapped.ReactiveProperty;
}
}
内部にReactivePropertyを持ち、ReactiveProperty.Value
だけシリアライズ対象にする MemoryPackableな構造体 を定義しました。カスタムフォーマッターではそれをもとにシリアライズさせます。
ReactiveCollection
フォーマッターのコード全文
#nullable enable
using MemoryPack;
using UniRx;
public class ReactiveCollectionFormatter<TElement> : MemoryPackFormatter<ReactiveCollection<TElement>>
{
public override void Serialize(ref MemoryPackWriter writer, ref ReactiveCollection<TElement>? value)
{
if (value == null)
{
writer.WriteNullCollectionHeader();
return;
}
var formatter = writer.GetFormatter<TElement?>();
writer.WriteCollectionHeader(value.Count);
foreach (var item in value)
{
var v = item;
formatter.Serialize(ref writer, ref v);
}
}
public override void Deserialize(ref MemoryPackReader reader, ref ReactiveCollection<TElement>? value)
{
if (!reader.TryReadCollectionHeader(out var length))
{
value = default;
return;
}
var formatter = reader.GetFormatter<TElement?>();
var collection = new ReactiveCollection<TElement>();
for (var i = 0; i < length; i++)
{
TElement? v = default;
formatter.Deserialize(ref reader, ref v);
collection.Add(v);
}
value = collection;
}
}
ReactiveCollection は GenericCollectionFormatter の実装を丸パクリしています。
ReactiveDictionary
フォーマッターのコード全文
#nullable enable
using MemoryPack.Formatters;
using UniRx;
public class ReactiveDictionaryFormatter<TKey, TValue> : GenericDictionaryFormatterBase<ReactiveDictionary<TKey, TValue>,TKey, TValue>
where TKey : notnull
{
protected override ReactiveDictionary<TKey, TValue> CreateDictionary()
{
return new();
}
}
ReadtiveDictionary
のような IDictionary<TKey, TValue>
は GenericDictionaryFormatterBase
クラス を継承したカスタムフォーマッターを作ると実装がとても楽です。
他にも ISet<T>
向けの GenericSetFormatterBase
があります。 (なぜ GenericCollectionFormatterBase は無いのだろうか...)
MemoryPackFormatterProviderに登録する
これらジェネリック型は MemoryPackFormatterProvider.RegisterGenericType()
で登録します。
MemoryPackFormatterProvider.RegisterGenericType(typeof(ReactiveProperty<>), typeof(ReactivePropertyFormatter<>));
MemoryPackFormatterProvider.RegisterGenericType(typeof(ReactiveCollection<>), typeof(ReactiveCollectionFormatter<>));
MemoryPackFormatterProvider.RegisterGenericType(typeof(ReactiveDictionary<,>), typeof(ReactiveDictionaryFormatter<,>));
第1引数にシリアライズ対象の型 、第2引数にフォーマッターの型 を渡します。
IL2CPPで死ぬかも
RegisterGenericType
はIL2CPP環境だと、コンパイル時に必要な型を認識できない可能性があるようです。
なので愚直に使う型を全て列挙する必要があります。
MemoryPackFormatterProvider.Register(new ReactivePropertyFormatter<string>());
MemoryPackFormatterProvider.Register(new ReactivePropertyFormatter<int>());
MemoryPackFormatterProvider.Register(new ReactivePropertyFormatter<float>());
MemoryPackFormatterProvider.RegisterCollection<ReactiveCollection<int>, int>();
MemoryPackFormatterProvider.RegisterCollection<ReactiveCollection<float>, float>();
MemoryPackFormatterProvider.RegisterCollection<ReactiveCollection<string>, string>();
// etc...
おわりに
ここ1週間くらい MemoryPack をちょこちょこと触っていました。
MemoryPack は割と新しいライブラリで情報があまりありません。その中で手探りで見つけた方法なので MemoryPackの作法として正しくない 可能性があります。間違い等ありましたらコメントやTwitterで連絡いただけると幸いです。