バイナリデータのデータ構造
09/11 2020
アジェンダ
はじめに
ゲームやアプリケーションでは、往々にして独自のバイナリデータを作成することがあります。
なぜならテキストデータよりも基本的にファイルサイズが小さくなり、処理が高速だからです。
本記事では、バイナリデータのデータ構造に関するプラクティスを紹介します。
バイナリデータのデータ構造
バイナリデータは以下のようなデータ構造にすると良いです。
/// <summary>
/// binファイルヘッダ
/// </summary>
public struct BinFileHeaderStruct
{
public uint signature; //!<シグネチャ [4]
public byte byteOrder; //!<バイトオーダー [5]
public byte bitOrder; //!<ビットオーダー [6]
public byte majorVersion; //!<メジャーバージョン [7]
public byte minorVersion; //!<マイナーバージョン [8]
public uint fileHeaderStructSize; //!<ファイルヘッダ構造体サイズ [byte] [12]
public uint keyDataStructSize; //!<キーデータ構造体サイズ [byte] [16]
public uint keyDataNum; //!<キーデータ数 [20]
};
/// <summary>
/// binファイルキーデータ
/// </summary>
public struct BinFileKeyDataStruct
{
public int key; //!<キー [4]
};
シグネチャ
ファイルの識別子です。
「独自のバイナリデータは先頭の4バイトにシグネチャを入れる」というフォーマットで運用することで、不正なファイルの読み込みを防止できます。
識別子はユニークなものであれば何でも良いですが、筆者はだいたいそのデータの表す4文字に設定しています。
例えば、アクションデータであれば"ACTN"、エフェクトデータであれば"EFCT"などです。
また、コメントにバイト数を書きましょう。
デバッグがしやすくなります。
バイトオーダー、ビットオーダー
異なるプラットフォームでリリースする可能性がある場合は、バイトオーダーとビットオーダーの情報を保持しましょう。
設定する数値は0, 1をリトルエンディアン、ビッグエンディアンに対応させても良いですが、筆者はわかりやすさのため0x4c(L)、0x42(B)を使用しています。
ただし、近年ではほとんどのケースでバイトオーダーがリトルエンディアン、ビットオーダーがビッグエンディアンなので、必要ないと判断した場合は省略しても良いでしょう。
メジャーバージョン、マイナーバージョン
互換性を保つためにバージョンの情報を保持しましょう。
筆者はよくメジャーバージョン、マイナーバージョンの2つにしていますが、粒度はお好みで。
ファイルヘッダ構造体サイズ、キーデータ構造体サイズ、キーデータ数
ファイルヘッダ構造体サイズ、データ部の構造体サイズを保持します。
データが複数ある場合は、データ数を保持します。
この三つがあれば、先頭アドレスがわかっていれば、どのデータに対してもオフセットの計算のみでアクセスできます。
また、データに保持することで、互換性を保った実装がシンプルになります。
ただし、C#だとbyte型のポインタを持っておくのは扱いづらいため、恩恵は少なくなります。
バイナリデータの読み込み
C#では、バイナリデータの読み込みには、以下のような拡張メソッドを用意すると良いです。
/// <summary>
/// BinaryReader拡張メソッド
/// </summary>
public static unsafe class BinaryReaderExtensions
{
/// <summary>
/// 構造体の読み込み
/// </summary>
/// <typeparam name="TStruct">構造体</typeparam>
/// <param name="binaryReader">BinaryReader</param>
/// <returns>構造体</returns>
public static TStruct ReadStruct<TStruct>(this BinaryReader binaryReader) where TStruct : struct
{
return binaryReader.ReadStruct<TStruct>(Marshal.SizeOf(typeof(TStruct)));
}
/// <summary>
/// 構造体の読み込み
/// </summary>
/// <typeparam name="TStruct">構造体</typeparam>
/// <param name="binaryReader">BinaryReader</param>
/// <param name="size">サイズ</param>
/// <returns>構造体</returns>
public static TStruct ReadStruct<TStruct>(this BinaryReader binaryReader, int size) where TStruct : struct
{
var buffer = binaryReader.ReadBytes(size);
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
return (TStruct)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(TStruct));
}
finally
{
handle.Free();
}
}
}
以下のようなコードで読み込みます。
var fileHeader = binaryReader.ReadStruct<BinFileHeaderStruct>();
binaryReader.BaseStream.Position = fileHeader.fileHeaderStructSize;
var dataArray = new BinFileKeyDataStruct[fileHeader.keyDataNum];
for (int idx = 0; idx < fileHeader.keyDataNum; idx++)
{
dataArray[idx] = binaryReader.ReadStruct<BinFileKeyDataStruct>((int)fileHeader.keyDataStructSize);
}
まとめ
-
ヘッダのメンバ
- シグネチャ
- バイトオーダー、ビットオーダー
- メジャーバージョン、マイナーバージョン
- ファイルヘッダ構造体サイズ、データ部構造体サイズ、データ数 (データが複数ある場合)
- バイナリデータ自身の情報でどのデータに対してもオフセットの計算のみでアクセスできるようにする
- コメントにバイト数を書く