セーブとロード

メリークリスマス、Siv3D Advent Calendar 2013, 25 日目の記事です。
今日は BinaryWriter と BinaryReader を使って、簡単なゲームデータのセーブとロードに挑戦しましょう。

BinaryWriter はデータをテキストではなくバイト列としてファイルに書き込みます。
ファイルに得点をセーブするプログラムで TextWriter との違いを見てみましょう。

# include <Siv3D.hpp>

void Main()
{
	const int score = 1234567;

	TextWriter textWriter(L"save.txt");

	textWriter.write(score);


	BinaryWriter binaryWriter(L"save.bin");
	
	binaryWriter.write(score);
}

save.txt は 1234567 という文字列が保存された 10 バイトのテキストファイルになります。一方 save.bin は int 型の 4 バイトのデータがそのまま保存され、テキストエディタで開くと変な文字列として解釈されます。

次に、今保存した得点をロードするプログラムで TextReader と BinaryReader の違いを見てみましょう。

# include <Siv3D.hpp>

void Main()
{
	TextReader textReader(L"save.txt");	
	
	String text;
	
	textReader.readLine(text);

	const int scoreA = Integer(text);


	BinaryReader binaryReader(L"save.bin");
	
	int scoreB = 0;

	binaryReader.read(&scoreB, sizeof(scoreB));


	Println(L"A: ", scoreA);

	Println(L"B: ", scoreB);

	WaitKey();
}

テキストファイルの場合、読み込んだ文字列を目的のデータ型に変換する必要があります。バイナリデータの場合はデータを変数に直接ロードできます。

セーブデータを人間が読める形式にする必要がないならば、速度やサイズで優れたバイナリ形式でセーブデータを扱いましょう。

構造体の読み書き

構造体を使うと、一度に多くのデータを読み書きできます。

# include <Siv3D.hpp>

struct Result
{
	int stage1Score;
	int stage2Score;
	double time;
};

void Main()
{
	Result result;
	result.stage1Score = 500;
	result.stage2Score = 400;
	result.time = 43.2;

	{
		BinaryWriter writer(L"result.bin");

		writer.write(result);
	}
	//	ここで writer のスコープが外れてファイルがクローズされる
	//	writer.close() でもよい

	// クローズされたファイルでないと正常に開けない
	BinaryReader reader(L"result.bin");

	Result loadData;
	reader.read(&loadData, sizeof(loadData));

	Println(L"stage1Score: ", loadData.stage1Score);
	Println(L"stage2Score: ", loadData.stage2Score);
	Println(L"time: ", loadData.time);

	WaitKey();
}

開いたファイルは、読み書きが終了したらクローズしましょう。クローズしていないファイルは、別の BinaryReader や BinaryWriter で正常に開くことができません。

非 POD クラス型の読み書き

Point, Vec2, Color などは、ポインタとデータのサイズを指定して write できます。

# include <Siv3D.hpp>

void Main()
{
	const Point pos(10, 50);

	{
		BinaryWriter writer(L"pos.bin");

		writer.write(&pos, sizeof(pos));
	}

	BinaryReader reader(L"pos.bin");

	Point loadData(0, 0);
	reader.read(&loadData, sizeof(loadData));

	Println(L"pos: ", loadData);

	WaitKey();
}

String の読み書き

std::vector や String のように、メンバ変数にポインタを持つクラスを、単純に write / read してはいけません。write されるのはポインタだけで、実際のバッファのデータは write されないという問題が生じるためです。

したがって次のように工夫する必要があります。String を文字列の長さと、実際のデータに分けてセーブ・ロードするサンプルです。

# include <Siv3D.hpp>

struct Monster
{
	String name;
	int number;
};

void Main()
{
	Monster pikachu;
	pikachu.name = L"ピカチュウ";
	pikachu.number = 25;

	{
		BinaryWriter writer(L"pikachu.bin");

		writer.write(pikachu.name.length);

		writer.write(pikachu.name.data(), pikachu.name.length * sizeof(wchar_t));

		writer.write(pikachu.number);
	}

	BinaryReader reader(L"pikachu.bin");

	size_t length = 0;

	reader.read(&length, sizeof(length));

	if (length > 6)
	{	// 異常な値(誤ったファイルを読み込んで巨大な値になるのを防ぐ)
		return;
	}

	Monster loadData;
	loadData.name.resize(length);

	reader.read(&loadData.name[0], loadData.name.length * sizeof(wchar_t));

	reader.read(&loadData.number, sizeof(loadData.number));

	Println(L"Name: ", loadData.name);
	Println(L"Number: ", loadData.number);

	WaitKey();
}

保存するデータの内容によっては単純なバイナリではなく INI 形式や CSV 形式、場合によっては画像を使うこともあるでしょう。より高度なアプリケーションでは、ファイルの破損チェックや、データの改造を防ぐ暗号化も必要になります。

将来的にはそういったファイル保存のユーティリティや、String, std::vector などのデータの保存を簡単にする機能を Siv3D に追加する予定です。