通信で伝達されるデータは、送信する場合にエンコードされてバイナリで送ります。
受信側では、その受信したバイナリデータをデコードして元の情報に復元します。
バイナリでなく文字列に変換する方法でもよいのですが、文字列にエンコードすると、
大きな情報量に変換される場合が多く、少ない情報量で送りたい場合は、
バイナリにシリアライズする方が良いと考えます。
バイナリへシリアライズでは、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; } }