TOP PAGE

汎用的なバイナリのシリアライズ・デシリアライズ モジュール;BinaryPack.cs

通信で伝達されるデータは、送信する場合にエンコードされてバイナリで送ります。
受信側では、その受信したバイナリデータをデコードして元の情報に復元します。
バイナリでなく文字列に変換する方法でもよいのですが、文字列にエンコードすると、 大きな情報量に変換される場合が多く、少ない情報量で送りたい場合は、 バイナリにシリアライズする方が良いと考えます。
バイナリへシリアライズでは、MessagePackというのが有名なようですが、 それが使えない環境でもシリアライズを使いたいため、 自前で簡単なシリアライズのプログラムを作ることにしました。
最終的に、1つのファイル(BinaryPack.cs)にまとめられています。

これを利用した、簡単なUnityの作品例は、こちらにあります。

シリアライズ・デシリアライズモジュールの基本的考え方と使い方


上記での1バイトのデータ識別番号に対して、バイナリーにエンコードする関数とデコードする関数を用意して使うことします。
この識別番号は、BinaryPackクラスの中で次のように用意しています。
    public const int ObjectOfInt = 1;//intのオブジェクト識別ID
    public const int ObjectOfFloat = 2;//floatのオブジェクト識別ID
    public const int ObjectOfString = 3;//stringのオブジェクト識別ID
(同じデータ型でも、例えばバイトオーダーが変われば、異なるデータ識別番号を使う)
効率を考えると、言語によって作り方が変わると思われますが、C#ではBinaryPack.csの一つのモジュールで実現しました。
このファイル(BinaryPack.cs)のソースは後述していますが、 BinaryPackクラスのstaticメソッドで実現しています。
このクラスに以下の内部クラス「Data」を作り、シリアライズはこれを介して行います。
BinaryPack.Dataは、上記イメージの一つの次の構造になっています。
    public class Data// binaryのシリアライズデータの一時記憶に使う構造体
    {
        public int id;// 下記バイナリのシリアライズ前のデータ型(上のイメージの赤の部分)
        public byte[] bytes;// バイナリ配列に変換したデータの記憶域
        //『上記配列内は、idは含まれない。
        //idを付加した配列は、BinaryPackオブジェクトのGetSerializedBuffer()で行われる。』
        public Data(int id)
        {
            this.id = id;// オブジェクトを識別する番号
            this.bytes = null;
        }
    }

BinaryPackの使い方と概要

BinaryPackは、int、float、stringをbyte[]にシリアライズと復元を行うstaticメソッドがあり、シリアライズ関数は、getBytesBy〜から始まる名前で、BinaryPack.Dataのインスタンスを生成して返す関数になっている。 このDataクラスには、複合時に使う1byteの識別用idと、引数のデータをbyte列した情報を記憶するbytesのメンバがある。
下記がstaticのシリアライズメソッドです。(byte列は、ビッグ エンディアン順)
Data getBytesByInt(int data)Dataオブジェクトを生成し、idに1をセットし、bytesにintのbyte列を設定します。
Data getBytesByFloat(float data)Dataオブジェクトを生成し、idに2をセットし、bytesにfloatのbyte列を設定します。
Data getBytesByString(string data)Dataオブジェクトを生成し、idに3をセットし、bytesにstringのbyte列を設定します。

また、復元(デシリアライズ)関数は、Dataのオブジェクトのbytesのメンバを引数にすると、第2引数の変数が0で、そのオブジェクトを復元して返す関数です。
getObjectOf〜から始まる名前で、下記のstatic復号(デシリアライズ)メソッドがあります。
object getObjectOfInt(byte[] buffer, ref int idx)bufferのidxの位置よりintに復元
object getObjectOfFloat(byte[] buffer, ref int idx)bufferのidxの位置よりfloatに復元
string getObjectOfString(byte[] buffer, ref int idx)bufferのidxの位置よりfloatに復元
以上のシリアライズと、デシリアライズを検証したコードをここに示します。
第2引数の添え字が指し示す位置から、復元するオブジェクトに対応する長さまで読み取り、読み取った長さに応じて、第2引数の参照変数idxの内容が増えます。
(int や float型は4byte情報なので4増えます。stringは可変長でその長さ分だけ増えます)
このような仕様にしている理由は、複数のオブジェクトが一つのバイト列に連続して並ぶ場合、 idxが実行により、次のオブジェクトの位置に更新されるので、連続実行のコードが短くできます。
また復元メソッドは、delegate object GetObject(byte []buffer, ref int idx);のデリゲート変数に記憶できるように同じ引数にしてあります。
そして、複数の型が混じったbyte列から復号する場合に、識別idを先頭に埋め込むことで、どの復元メソッドを使うか、分岐できる仕組みを提供しています。
そしてこの復元メソッドを、識別idを添え字にしたテーブル参照で選択して実行できるように、
public static GetObject []dencoders = new GetObject[256];// デリゲート変数の配列の配列を用意しています。
この配列に、あらかじめ識別idを引数した要素に、対応する復元メソッドを記憶して使います。
BinaryPackで、上記int,float,stringの復元用デリゲート関数がstaticコンストラクタで、記憶済みになっています。
各識別idの添え字に使う定数は、クラス先頭で定義しています。
自身でシリアライズ化したい新しいクラスを作る場合、上記で示したように、staticのシリシリアライズメソッド(getBytesBy〜)と、 GetObject デリゲートの復元用メソッド(getObjectOf〜)を作ります。
そして重複しないように注意して識別ID の番号を決めて、static GetObject []dencoders の配列の 識別IDを添え字とした要素に復元用メソッドを記憶して使います。
次のようなコードです。→  BinaryPack.dencoders[識別ID] = getObjectOf〜;
独自のシリアライズする場合のクラスの例をして、SerializeStrVector3の作成例を後述しています

複数のオブジェクトを一つのバイト列にシリアライズした後、デシリアライズして検証したコードをここに示します。

このように、複数の型のデータが交じり合ったデータ群のシリアライズと復元(デシリアライズ)に対応するために、次のインスタンスとスタティックメソッドを用意しました。
スタティック
メソッド
void Add(BinaryPack.Data bindata)シリアライズしたbindataをリストに追加
スタティック
メソッド
byte[] GetSerializedBuffer() Addで記憶したリスト要素を、一つにまとめたシリアライズのbyte列を生成して返す。
これは、識別idを先頭に埋め込んだAddの順番に並ぶbyte配列です。
スタティック
メソッド
object GetDeserializedObject(
byte[] serializedBuff, ref int idx_buff )
serializedBuffの配列のidx_buff位置から一つのオブジェクトを復元して、それを返す。
参照引数のidx_buffの内容は、読み取った次のオブジェクトを示す位置に更新される。
上記メソッド群を使うと、複数のオブジェクトを一つのバイト列にシリアライズする場合や、デシリアライズが用意になります。
このこの使用例の検証コードをここに示します。
また、シリアライズで得られたバイト列をファイル化して、それをpythonでデシリアライズして検証しているコードはここに示します。

上記のBinaryPackのオブジェクトを使ったシリアライズは、複数のオブジェクトの一つのバイト列にする場合を考慮したメソッドです。
ですが一つのオブジェクトだけをシリアライズ化する場合や復元する場合に向きません。
そこで、一つのオブジェクトだけ対象にした次のstaticメソッドを用意しています。
byte[] getBynary(Data data)識別IDを埋め込んだbyte列を得る
object getObjectOfOne(byte[] buffer, int idx=0)bufferのidxの位置より復元。(idxはIDがある位置を指定しなければならない。)
上記を使って、"abcあいう"の文字列だけをシリアライズして、復元するコードは次のようになります。
        byte[] array = BinaryPack.getBynary(BinaryPack.getBytesByString("abcあいう"));
        string s = (string)BinaryPack.getObjectOfOne(array);
        Debug.Log($"{array}, {s}");	

シリアライズ・デシリアライズモジュール(BinaryPack.cs)のソース

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Buffers.Binary;//Unity 2021.2以降で使える

public class BinaryPack
{
    public const int ObjectOfInt = 1;//intのオブジェクト識別ID
    public const int ObjectOfFloat = 2;//floatのオブジェクト識別ID
    public const int ObjectOfString = 3;//stringtのオブジェクト識別ID

    public class Data// binaryのシリアライズデータの一時記憶に使う構造体
    {
        public int id;// 下記バイナリのシリアライズ前のデータ型
        public byte[] bytes;// バイナリ配列に変換したデータの記憶域
        //『上記配列内は、idは含まれない。
        //idを付加した配列は、BinaryPackオブジェクトのGetSerializedBuffer()で行われる。』
        public Data(int id)
        {
            this.id = id;// オブジェクトを識別する番号
            this.bytes = null;
        }
    }

    public delegate object GetObject(byte []buffer, ref int idx);//デリゲート定義
    public static GetObject []dencoders = new GetObject[256];// デリゲート変数の配列

    private List<BinaryPack.Data> bindatas = new List<BinaryPack.Data>();
    private int Count = 0;// bindatasで利用されるbyte数

    static BinaryPack()    // staticコンストラクタ
    {
        BinaryPack.dencoders[ObjectOfInt] = getObjectOfInt;//intのオブジェクト識別IDの復元関数の登録
        BinaryPack.dencoders[ObjectOfFloat] = getObjectOfFloat;//floatのオブジェクト識別IDの復元関数の登録
        BinaryPack.dencoders[ObjectOfString] = getObjectOfString;//stringのオブジェクト識別IDの復元関数の登録
    }

    public static Data getBytesByInt(int data)
    {   // int data をバイナリ配列に変換
        Data binData = new Data(BinaryPack.ObjectOfInt);
        binData.bytes = BitConverter.GetBytes(data);
        // リトルエンディアンであれば、ビックエンディアンに変更
        if (BitConverter.IsLittleEndian) Array.Reverse(binData.bytes);
        return binData;
    }
    public static object getObjectOfInt(byte[] buffer, ref int idx)
    {   // 配列のidx 位置からのバイナリをintに変換
        byte[] buffer2 = new byte[sizeof(Int32)];
        Buffer.BlockCopy(buffer, idx, buffer2, 0, buffer2.Length);
        if (BitConverter.IsLittleEndian) Array.Reverse(buffer2);// リトルエンディアンであれば反転
        object obj = BitConverter.ToInt32(buffer2, 0);// intのobjectを復元
        idx += sizeof(Int32);
        return obj;
    }
    public static Data getBytesByFloat(float data)
    {   // float data をバイナリ配列に変換
        Data binData = new Data(BinaryPack.ObjectOfFloat);
        binData.bytes = BitConverter.GetBytes(data);
        // リトルエンディアンであれば、ビックエンディアンに変更
        if (BitConverter.IsLittleEndian) Array.Reverse(binData.bytes);
        return binData;
    }
    public static object getObjectOfFloat(byte[] buffer, ref int idx)
    {   // 配列のidx 位置からのバイナリをfloatに変換
        byte[] buffer2 = new byte[sizeof(float)];
        Buffer.BlockCopy(buffer, idx, buffer2, 0, buffer2.Length);
        if (BitConverter.IsLittleEndian) Array.Reverse(buffer2);// リトルエンディアンであれば反転
        object obj = BitConverter.ToSingle(buffer2, 0);// floatのobjectを復元
        idx += sizeof(float);
        return obj;
    }
    public static Data getBytesByString(string data)
    {   // string data (UTF8)をバイナリ配列(65535byte以下)に変換
        Data binData = new Data(BinaryPack.ObjectOfString);
        byte []aString = System.Text.Encoding.UTF8.GetBytes(data);
        UInt16 count = (UInt16)aString.Length;// byte列に変換した時のbyte数
        byte []aCount = BitConverter.GetBytes(count);
        // リトルエンディアンであれば、ビックエンディアンに変更
        if (BitConverter.IsLittleEndian) Array.Reverse(aCount);
        binData.bytes = new byte[2 + aString.Length];
        Buffer.BlockCopy(aCount, 0, binData.bytes, 0, aCount.Length);// byte数を先頭に埋め込む
        Buffer.BlockCopy(aString, 0, binData.bytes, aCount.Length, aString.Length);
        return binData;
    }
    public static string getObjectOfString(byte[] buffer, ref int idx)
    {   // 配列のidx 位置からのバイナリをstringに変換
        byte[] buffer2 = new byte[sizeof(UInt16)];
        Buffer.BlockCopy(buffer, idx, buffer2, 0, buffer2.Length);
        UInt16 count = BinaryPrimitives.ReadUInt16BigEndian(buffer2);//後ろに並ぶbyte数を取得
        idx += sizeof(UInt16);
        string obj = System.Text.Encoding.UTF8.GetString(buffer, idx,count);// stringのobjectを復元
        idx += count;
        return obj;
    }

    // byte列先頭にIDがあるバイト列を返す。(戻り値の2byte目以降はdata.bytesをコピーしたバイト列)
    public static byte[] getBynary(Data data)
    {
        byte[] binary = new byte[1+data.bytes.Length];
        binary[0] = (byte)data.id;
        Buffer.BlockCopy(data.bytes, 0, binary, 1, data.bytes.Length);
        return binary;
    }

    // バイト列bufferから先頭1byteにIDがあるidx位置を指定し、そこから始まる一つのオブジェクトを生成して返す。
    public static object getObjectOfOne(byte[] buffer, int idx=0)
    {
        byte id = buffer[idx++];
        // Debug.Log($"getObjectOfOne id: {id}, idx:{idx}, buffer:{buffer}");
        if(BinaryPack.dencoders[id] == null)
        {
            throw new Exception("id: {id} , BinaryPack.dencoders[{id}] の登録が未登録");
        }
        object obj = BinaryPack.dencoders[id](buffer, ref idx);
        return obj;
    }

    // 以下は複数のBinaryPack.Dataをまとめたbayte列を作るためのメソッドとその復元で使うメソッド

    public void Add(BinaryPack.Data bindata)// シリアライズしたbindataをリストに追加
    {
        this.bindatas.Add(bindata);
        this.Count += 1 + bindata.bytes.Length ; //オブジェクト識別ID用の1byteを付加したbyte配列サイズ
    }

    public byte[] GetSerializedBuffer()// リストに在るbindataからまとめたbyte配列を得る。
    {
        byte[] serializedBuff = new byte[this.Count];// 戻り値のbyte配列を用意
        int idx_buff = 0;
        foreach (BinaryPack.Data d in this.bindatas)// 戻り値のbyte配列にコピー・設定の繰り返し
        {
            serializedBuff[idx_buff++] = (byte)d.id;
            Buffer.BlockCopy(d.bytes, 0, serializedBuff, idx_buff, d.bytes.Length);
            idx_buff += d.bytes.Length;
        }
        return serializedBuff;
    }

    // serializedBuffの配列のidx_buff位置から一つのオブジェクトを復元して、それを返す。
    // 参照引数のidx_buffの内容は、読み取った次のオブジェクトを示す位置に更新される。
    public static object GetDeserializedObject(byte[] serializedBuff, ref int idx_buff)
    {
        if (idx_buff >= serializedBuff.Length) return null;
        byte id = serializedBuff[idx_buff++];
        object obj = BinaryPack.dencoders[id](serializedBuff, ref idx_buff);
        return obj;
    }
}
上記で、System.Buffers.Binary.BinaryPrimitivesを使っており、それによりUnity 2021.2以降でないと、使えなくなっています。
(BitConverterのバイトオーダーはシステム依存で、2024年5月時点のWindowsではデフォルトがリトルエンディアンで、ビックエンディアンで変換するために使っています)

検証プログラムとその使い方 その1

BinaryPackクラスのint、float、string のシリアライズと、デシリアライズの基本staticメソッドの検証コード
    void test1()
    {
        BinaryPack.Data[] bindatas = new BinaryPack.Data[3];

        bindatas[0] = BinaryPack.getBytesByInt(123);//123をbinaryにシリアライズ化

        bindatas[1] = BinaryPack.getBytesByFloat(1.23f);//1.23fをbinaryにシリアライズ化

        bindatas[2] = BinaryPack.getBytesByString("ABCあいうxyz");//"ABCあいうxyz"をbinaryにシリアライズ化

        // 上記でシリアライズした3つのバイナリデータを以下で復元して表示しています。
        int idx = 0;
        int n = (int)BinaryPack.getObjectOfInt(bindatas[0].bytes, ref idx);
        Debug.Log($"{bindatas[0].bytes.Length}byteから復元====={n}");

        idx = 0;
        float f = (float)BinaryPack.getObjectOfFloat(bindatas[1].bytes, ref idx);
        Debug.Log($"{bindatas[1].bytes.Length}byteから復元====={f}");

        idx = 0;
        string s = BinaryPack.getObjectOfString(bindatas[2].bytes, ref idx);
        Debug.Log($"{bindatas[2].bytes.Length}byteから復元====={s}");
    }

検証プログラムとその使い方 その2 (複数のオブジェクトを一つのバイト列にする)

BinaryPackクラスのint、float、string のシリアライズを行うまでは、前述と同じです。 、デシリアライズの基本staticメソッドの検証コード
    void test2()
    {
        List<BinaryPack.Data> list = new List<BinaryPack.Data>();// まとめてシリアライズするためのリスト
        int Count = 0;
        BinaryPack.Data bindata;

        list.Add(bindata = BinaryPack.getBytesByInt(123));//123をbinaryにシリアライズ化
        Count += bindata.bytes.Length + 1;

        list.Add(bindata = BinaryPack.getBytesByFloat(1.23f));//1.23fをbinaryにシリアライズ化
        Count += bindata.bytes.Length + 1;

        list.Add(bindata = BinaryPack.getBytesByString("ABCあいうxyz"));//"ABCあいうxyz"をbinaryにシリアライズ化
        Count += bindata.bytes.Length + 1;

        byte[] serializedBuff = new byte[Count];// このバイナリ配列に、まとめてシリアライズする。
        int idx_buff = 0;
        foreach (BinaryPack.Data d in list)// シリアライズの繰り返し
        {
            serializedBuff[idx_buff++] = (byte)d.id;
            Buffer.BlockCopy(d.bytes, 0, serializedBuff, idx_buff, d.bytes.Length);
            idx_buff += d.bytes.Length;
        }
        Debug.Log($"全体が{serializedBuff.Length}byteのbufferにシリアライズされた");


        for (idx_buff = 0; idx_buff < serializedBuff.Length;)// 復元を繰り返しで行う
        {   
            int iBack = idx_buff; 
            int id = serializedBuff[idx_buff++];//オブジェクト識別IDを取得
            object obj = BinaryPack.dencoders[id](serializedBuff, ref idx_buff);// 各種オブジェクトの復元
            Debug.Log($"{idx_buff - iBack}byteから復元====={obj}");
        }
    }

検証プログラムとその使い方 その3 (複数のオブジェクトを一つのバイト列にする)

前述のコードを、BinaryPackのインスタンスを使って簡潔に書き直したコードです。
    void test3()
    {
        BinaryPack binaryPack = new BinaryPack();
        binaryPack.Add(BinaryPack.getBytesByInt(123));//123をbinaryにシリアライズ化
        binaryPack.Add(BinaryPack.getBytesByFloat(1.23f));//1.23fをbinaryにシリアライズ化
        binaryPack.Add(BinaryPack.getBytesByString("ABCあいうxyz"));//"ABCあいうxyz"をbinaryにシリアライズ化
        byte[] serializedBuff = binaryPack.GetSerializedBuffer();// シリアライズ配列をまとめてて一つにする。
        Debug.Log($"全体が{serializedBuff.Length}byteのbufferにシリアライズされた");

        int idx_buff = 0;
        while (idx_buff < serializedBuff.Length)// 復元を繰り返しで行う
        {
            int iBack = idx_buff;
            object obj = BinaryPack.GetDeserializedObject(serializedBuff, ref idx_buff);// 各種オブジェクトの復元
            Debug.Log($"{idx_buff - iBack}byteから復元====={obj}");
        }
    }

検証プログラムとその使い方 その4 (C#シリアライズしたファイルをpythonでデシリアライズ)

シリアライズのコードは上記と同じで、これで一つにまとめたバイト列を"test.bin"に書き出すコードを追加しています。
    void test3()
    {
        BinaryPack binaryPack = new BinaryPack();
        binaryPack.Add(BinaryPack.getBytesByInt(123));//123をbinaryにシリアライズ化
        binaryPack.Add(BinaryPack.getBytesByFloat(1.23f));//1.23fをbinaryにシリアライズ化
        binaryPack.Add(BinaryPack.getBytesByString("ABCあいうxyz"));//"ABCあいうxyz"をbinaryにシリアライズ化
        byte[] serializedBuff = binaryPack.GetSerializedBuffer();// シリアライズ配列をまとめてて一つにする。
        Debug.Log($"全体が{serializedBuff.Length}byteのbufferにシリアライズされた");

        using (FileStream stream = File.Open("test.bin", FileMode.Create))
        {
            stream.Write(serializedBuff, 0, serializedBuff.Length);// ファイルに書き込み
        }
    }
上記で作成されたシリアライズの"test.bin"は、下記のpythonのコードでデシリアライズできます。
from struct import pack, unpack # pythonには単精度浮動小数点型が存在しないが、これを使うと変換可能

def getObjectOfInt(bin, idx):
    i_next=idx+4
    i_data = int.from_bytes(bin[idx:i_next],"big") # ビッグエンディアン
    return i_data, i_next

def getObjectOfFloat(bin, idx):
    i_next=idx+4
    f_data, = unpack(">f", bin[idx:i_next]) # > float32 のビッグエンディアン
    return f_data, i_next

def getObjectOfString(bin, idx): # binのidx 位置からのバイナリをstringに変換
    i_next=idx+2
    count, = unpack(">H", bin[idx:i_next]) # '>'で、ビッグエンディアン、 2byte unsigned short
    idx=i_next
    i_next += count
    s_data = bin[idx:i_next].decode('utf-8')
    return s_data, i_next

with open("test.bin", "br") as fr:
   bin = fr.read() # 読み取り(引数がないなら全て読み取る)

print(bin)
idx = 0
while idx < len(bin): # 復元を繰り返しで行う
    if bin[idx] == 1:
       data,idx = getObjectOfInt(bin, idx+1) # int型デシリアライズ
       print( f"data:{data} , idx:{idx}" )
    elif bin[idx] == 2:
       data,idx = getObjectOfFloat(bin, idx+1) # float32型デシリアライズ
       print( f"data:{data} , idx:{idx}" )
    elif bin[idx] == 3:
       data,idx = getObjectOfString(bin, idx+1) # 文字列(utf-8)型デシリアライズ
       print( f"data:{data} , idx:{idx}" )
    else: break

シリアライズ・デシリアライズモジュール(BinaryPack.cs)利用した 簡単なUnityの作品例

ドラックで移動できるキューブが2つだけの作品ですが、 終了時に2つのキューブの位置(Vector3)をバイナリにシリアライズしてファイルに保存します。
そして再び実行した時に、デシリアライズして終了時の位置にキューブ復元し、継続してドラック操作をできるようにした簡単な作品例です。

(この動作ムービーを紹介するXへのリンク)
この作品を次のイメージのようにパッケージにしたファイル(BinaryPack.unitypackage)は、
  →このリンクからダウンロードできます。
(Unity 2021.2以降でないと使えませんのご注意ください)

このパッケージの、BinaryPack_TestSceneフォルダ内のBinaryPack_TestSceneを開いて、実行の確認ができます。
このフォルダ内のソース内のファイルは次のようになっています。(ソースファイル名の部分にその内容へのリンクがあります。)
MainTestBinaryPack.csメインのコードです。起動時のStartで、"Serialize.bin"のファイルがあれば、それをデシリアライズして、 得られたVector3でキューブを終了時の位置に設定します。このファイルは、OnApplicationQuitで記憶終了時に保存(キューブの名前と位置をデシリアライズ)します。
SerializeStrVector3.cs 文字列と、Vector3をシリアライズ・デシリアライズするSerializeStrVector3クラスを定義しています。
これは、BinaryPackで使うためのクラス定義例です。(新しいシリアライズを行う場合に作ります。)
BinaryPack_Test.csこの作品とは、関係ないのですが、前述したBinaryPackクラスのテストコードが記述されています。
Scriptフォルダ内に以下のファがあります。
BinaryPack.csこのページで紹介しているシリアライズ・デシリアライズモジュールです。
(このファイルをコピーして利用すると、単純なシリアライスが実現できます。)

MainTestBinaryPack.cs

メインのコードです。起動時のStartで、"Serialize.bin"のファイルがあれば、それをデシリアライズして、 得られたVector3でキューブを終了時の位置に設定します。
このファイルは、OnApplicationQuitで記憶終了時に保存(キューブの名前と位置をデシリアライズ)します。
なお、キューブはスクリプトで2つ生成し、"Cube1"と"Cube2"の名前を付けています。この名前と位置がシリアライズ対象で、 後述するSerializeStrVector3に設定して、BinaryPackでバイト列にシリアライズして、ファイルに保存しています。
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.EventSystems;

public class MainTestBinaryPack : MonoBehaviour
{
    static bool initialized = SerializeStrVector3.Initialize();
    BinaryPack binaryPack;
    const string filePath = "Serialize.bin";
    

    GameObject Cube1, Cube2;
    GameObject moveTarget = null;// ドラック対象の記憶用(ドラック中以外はnull)
    Vector3 prevMousePosition;// ドラック移動前のマウスワールド座標

    void Start()
    {
        SerializeStrVector3.Initialize();// この作品では、上記staticメンバの初期化に使っているので必要ないが、ここで設定する手法もありです。

        // 動的に2つのキューブを生成
        Cube1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
        Cube1.name = "Cube1";
        Cube1.transform.position = new Vector3(-5, 3, 0);

        Cube2 = GameObject.CreatePrimitive(PrimitiveType.Cube);
        Cube2.name = "Cube2";
        Cube2.transform.position = new Vector3(5, -3, 0);

        // filePathのファイルが存在すれば、そのファイル内容で、位置を復元する。
        FileInfo fileInfo = new FileInfo(filePath);
        if ( true && fileInfo.Exists)
        {
            byte[] buff = new byte[fileInfo.Length];
            using (FileStream stream = fileInfo.OpenRead())// ファイル内容のバイナリをbuffに記憶
            {
                int n = 0;
                do { n += stream.Read(buff, n, buff.Length); 
                } while (n < buff.Length);
            }
            binaryPack = new BinaryPack();// テスト対象のシリアライズオブジェクト用意
            int idx = 0;
            SerializeStrVector3 ssv3;// 各種オブジェクトの復元用
            while(idx < buff.Length)// 位置(Vector3)を復元して設定する繰り返し
            {
                ssv3 = (SerializeStrVector3)BinaryPack.GetDeserializedObject(buff, ref idx);
                GameObject obj = GameObject.Find(ssv3.str);//復元対象のオブジェクトを取得
                obj.transform.position = ssv3.v3;
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            Vector3 mouseScreenPosition = Input.mousePosition;
            mouseScreenPosition.z = -Camera.main.transform.position.z;// Z軸修正
            Vector3 mouseWorldPosition = Camera.main.ScreenToWorldPoint(mouseScreenPosition);
            Ray ray = Camera.main.ScreenPointToRay(mouseScreenPosition);
            RaycastHit hit;
            if ( Physics.Raycast(ray, out hit) == false && moveTarget == null) return;// Rayがどれにも当たらない
            if(moveTarget == null)
            {
                if (hit.collider.gameObject.name == "Cube1") moveTarget = Cube1;//ドラック対象を指定
                if (hit.collider.gameObject.name == "Cube2") moveTarget = Cube2;
                prevMousePosition = mouseWorldPosition;
            }
            else
            {
                Vector3 move = mouseWorldPosition - prevMousePosition;//マウスドラック量
                prevMousePosition = mouseWorldPosition;
                moveTarget.transform.position += move;// ドラックで移動
                Debug.Log($"{mouseScreenPosition}---> {mouseWorldPosition}");
            }
        }
        else
        {
            moveTarget = null;
        }
    }

    public void OnApplicationQuit()// プログラム終了時に、SerializeStrVector3オブジェクトをファイル化
    {
        binaryPack = new BinaryPack();
        SerializeStrVector3 ssv3 = new SerializeStrVector3();
        ssv3.str = "Cube1";
        ssv3.v3 = this.Cube1.transform.position;
        binaryPack.Add(SerializeStrVector3.getBytesByMyData(ssv3));
        ssv3.str = "Cube2";
        ssv3.v3 = this.Cube2.transform.position;
        binaryPack.Add(SerializeStrVector3.getBytesByMyData(ssv3));
        byte[] buff = binaryPack.GetSerializedBuffer();// 上記2つをシリアライズしたバイト列を取得
        using (FileStream stream = File.Open(filePath, FileMode.Create))
        {
            stream.Write(buff, 0, buff.Length);// ファイルに書き込み
        }
    }
}

SerializeStrVector3.cs

文字列と、Vewctor3(float型が3つ)をシリアライスするクラスの例です。
前述の通りで、BinaryPack.Dataを生成するstaticのシリシリアライズメソッド(getBytesByMyData)と、 GetObject デリゲートの復元用メソッド(getObjectOfMyData)を作っています。
そして重複しないように注意して識別ID の番号の4を決めて、static GetObject []dencoders の配列の 識別IDを添え字とした要素に復元用メソッドを記憶するInitializeメソッドを作っています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class SerializeStrVector3
{
    const int ObjectOfMyData = 4;// シリアライズで使うオブジェクト識別番号

    public string str;// Serialize対象データ
    public Vector3 v3;// Serialize対象データ

    static SerializeStrVector3(){// static コンストラクタ
    }

    public static bool Initialize()
    {
        BinaryPack.dencoders[ObjectOfMyData] = getObjectOfMyData;//MyDataのオブジェクト識別IDの復元関数の登録
        return true;
    }


    public static BinaryPack.Data getBytesByMyData(SerializeStrVector3 pram)// シリアライズメソッド
    {  
        BinaryPack.Data str = BinaryPack.getBytesByString(pram.str);// BinaryPackのシリアライズメソッドを利用
        BinaryPack.Data v3x = BinaryPack.getBytesByFloat(pram.v3.x);
        BinaryPack.Data v3y = BinaryPack.getBytesByFloat(pram.v3.y);
        BinaryPack.Data v3z = BinaryPack.getBytesByFloat(pram.v3.z);

        BinaryPack.Data binData = new BinaryPack.Data(ObjectOfMyData);
        binData.bytes = new byte[str.bytes.Length + 4 * 3];

        int idx = 0;
        Buffer.BlockCopy(str.bytes, 0, binData.bytes, idx, str.bytes.Length);
        idx += str.bytes.Length;
        Buffer.BlockCopy(v3x.bytes, 0, binData.bytes, idx, v3x.bytes.Length);
        idx += v3x.bytes.Length;
        Buffer.BlockCopy(v3y.bytes, 0, binData.bytes, idx, v3y.bytes.Length);
        idx += v3y.bytes.Length;
        Buffer.BlockCopy(v3z.bytes, 0, binData.bytes, idx, v3z.bytes.Length);
        idx += v3z.bytes.Length;
        return binData;
    }
    public static SerializeStrVector3 getObjectOfMyData(byte[] buffer, ref int idx)// デシリアライズメソッド
    {   // 配列のidx 位置からのバイナリをSerializeStrVector3に変換
        SerializeStrVector3 obj = new SerializeStrVector3();
        obj.str = BinaryPack.getObjectOfString(buffer, ref idx);
        obj.v3.x = (float)BinaryPack.getObjectOfFloat(buffer, ref idx);
        obj.v3.y = (float)BinaryPack.getObjectOfFloat(buffer, ref idx);
        obj.v3.z = (float)BinaryPack.getObjectOfFloat(buffer, ref idx);
        return obj;
    }
}