LoginSignup
1

MemoryPackでUniRxのReactive〇〇をシリアライズする

Last updated at Posted at 2023-02-01

はじめに

MemoryPack で UniRx の Reactive〇〇 のような ジェネリック型 をシリアライズする方法を備忘録として残しておきます。
UniRxの ReactiveProperty, ReactiveCollection, ReactiveDictionary を例に出しますが、基本的にはどのジェネリック型にも対応できると思います。他のライブラリのクラスなど、自分では変更できないようなクラス向けです

環境は以下の通りです。

  • Unity2021.3.17
  • MemoryPack1.9.12

本記事の内容の実装とテストはこちら https://github.com/euglenach/MemoryPackSerializeRxTest

MemoryPackとは

MemoryPackのGithubneuecc先生のブログを見ることをオススメします。

ジェネリック型の外部クラスをシリアライズする

ReactiveProperty

MemoryPackのReadmeの serialize-external-types にしたがってシリアライズ対応していきます。

ラッパーとフォーマッターのコード全文
ReactivePropertyFormatter.cs
#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

フォーマッターのコード全文
ReactiveCollectionFormatter.cs
#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

フォーマッターのコード全文
ReactiveDictionaryFormatter.cs
#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で死ぬかも

RegisterGenericTypeIL2CPP環境だと、コンパイル時に必要な型を認識できない可能性があるようです。
なので愚直に使う型を全て列挙する必要があります。

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で連絡いただけると幸いです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1