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

public const int ObjectOfInt = 1;//intのオブジェクト識別ID
public const int ObjectOfFloat = 2;//floatのオブジェクト識別ID
public const int ObjectOfString = 3;//stringのオブジェクト識別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;
}
}
| 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列を設定します。 |
| 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に復元 |
| スタティック メソッド | 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の内容は、読み取った次のオブジェクトを示す位置に更新される。 |
| byte[] getBynary(Data data) | 識別IDを埋め込んだbyte列を得る |
| object getObjectOfOne(byte[] buffer, int idx=0) | bufferのidxの位置より復元。(idxはIDがある位置を指定しなければならない。) |
byte[] array = BinaryPack.getBynary(BinaryPack.getBytesByString("abcあいう"));
string s = (string)BinaryPack.getObjectOfOne(array);
Debug.Log($"{array}, {s}");
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以降でないと、使えなくなっています。
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}");
}
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}");
}
}
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}");
}
}
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


| MainTestBinaryPack.cs | メインのコードです。起動時のStartで、"Serialize.bin"のファイルがあれば、それをデシリアライズして、 得られたVector3でキューブを終了時の位置に設定します。このファイルは、OnApplicationQuitで記憶終了時に保存(キューブの名前と位置をデシリアライズ)します。 |
| SerializeStrVector3.cs | 文字列と、Vector3をシリアライズ・デシリアライズするSerializeStrVector3クラスを定義しています。 これは、BinaryPackで使うためのクラス定義例です。(新しいシリアライズを行う場合に作ります。) |
| BinaryPack_Test.cs | この作品とは、関係ないのですが、前述したBinaryPackクラスのテストコードが記述されています。 |
| BinaryPack.cs | このページで紹介しているシリアライズ・デシリアライズモジュールです。 (このファイルをコピーして利用すると、単純なシリアライスが実現できます。) |
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);// ファイルに書き込み
}
}
}
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;
}
}