早い・簡単・便利な Config ファイルクラスを作る (C++/OpenSiv3D)

**プログラムを再ビルドせずにパラメータを変更** できれば、ゲームやアプリを高速に開発できます。本記事では、**TOML** 形式のテキストファイルに `int32` や `double`, `String`, `Vec2`, `ColorF`, `Rect`, `Array<Point>`, `Array<String>` といった型の値を記述し、プログラムの実行中に値を書き換えたときに、実行中のアプリケーションに反映されるような Config ファイルを扱う便利クラスを紹介します。
最短のケースで、テキストファイルに 1 行、プログラムに 2 行追加するだけで、実行中に更新可能な変数・配列を作れるようになります。

## ライブラリ
◆ [Config.hpp](https://gist.githubusercontent.com/Reputeless/5ab831881cf2326f29eb91872398570c/raw/e7d9c688134570791126cd509a4d611a6cea7c3a/ConfigFile.hpp) をダウンロードしてプロジェクトに追加します。

## 使い方
設定記述ファイルをプロジェクトフォルダ (`App/` フォルダ内) に配置します。
ここでは `config.toml` というファイルを作ります。
TOML の仕様については [TOML仕様の和訳](https://qiita.com/minoritea/items/c0de47b8beb813c655d4) が参考になります。

```config.toml
[Setting]
a = 123
b = "text"
c = "(0.1, 0.2, 0.3)"
d = [10, 20, 30, 40, 50]
```

記述されたデータ全てを管理する `ConfigData` クラスを作ります。
`struct Setting setting` のように、セクションごとに内部で構造体を定義すると扱いやすくなります。
`void ConfigData::reload(const TOMLReader& toml)` では、それぞれの内部の構造体に対して `S3DCFG_RELOAD_SECTION` をします。
それぞれの構造体は `void reload(const TOMLReader& toml)` メンバを持ち、最初にセクション名で `S3DCFG_LOAD_SECTION` したあと、すべての値に `S3DCFG_LOAD_VALUE` または `S3DCFG_LOAD_ARRAY` します。
これで、サンプルプログラムのように、いつでも値を更新できるようになります。

```Main.cpp
# include <Siv3D.hpp> // OpenSiv3D v0.3.1
# include "ConfigFile.hpp"

struct ConfigData
{
	void reload(const TOMLReader& toml)
	{
		// 新しい構造体を作ったらここに追加
		S3DCFG_RELOAD_SECTION(setting);
	}

	// [] セクションごとに構造体を作ると便利
	struct Setting
	{
		int32 a;
		String b;
		ColorF c;
		Array<int32> d;

		// それぞれの構造体に reload 関数を定義する
		void reload(const TOMLReader& toml)
		{
			// 先頭に S3DCFG_LOAD_SECTION
			S3DCFG_LOAD_SECTION(U"Setting");
			// LOAD_VALUE で値をロード
			S3DCFG_LOAD_VALUE(a);
			S3DCFG_LOAD_VALUE(b);
			S3DCFG_LOAD_VALUE(c);
			// 配列の場合は LOAD_ARRAY
			S3DCFG_LOAD_ARRAY(d);
		}
	} setting;
};

using Config = ConfigFile<ConfigData>;

void Update(const Config& config)
{
	Graphics::SetBackground(config.setting.c);
	Print << config.setting.a;
	Print << config.setting.b;
	Print << config.setting.c;
	Print << config.setting.d;
}

void Main()
{
	// 設定記述ファイルのファイルパス
	const FilePath configPath = U"config.toml";
	Config config;

	try
	{
		// Config ファイルを読み込み
		config.reload(configPath);
	}
	catch (const ConfigError& err)
	{
		// Config の読み込みエラーがあると ConfigError 例外
		err.show();
		return;
	}

	Update(config);

	while (System::Update())
	{
		// Config ファイルが更新されたら
		if (config.hasChanged())
		{
			try
			{
				ClearPrint();

				// Config ファイルを再読み込み
				config.reload(configPath);
			}
			catch (const ConfigError& err)
			{
				err.print();
			}

			Update(config);
		}
	}
}

```

## サンプルプログラム
少し規模が大きいサンプルです。
記述ファイルを変更すると、ウィンドウタイトルやサイズ、背景色、パラメータが変わります。

```config.toml
[Window]
title = "My Game"
size = "(1280, 720)"

[UI]
background = "(0.8, 0.9, 1.0)"

[Parameter]
a = 123
b = 3.1415
c = "text"
d = "(0.1, 0.2, 0.3)"
e = [10, 20, 30, 40, 50]
f = ["AAA", "BBB", "CCC"]
```

```Main.cpp
# include <Siv3D.hpp> // OpenSiv3D v0.3.1
# include "ConfigFile.hpp"

struct ConfigData
{
	void reload(const TOMLReader& toml)
	{
		S3DCFG_RELOAD_SECTION(window);
		S3DCFG_RELOAD_SECTION(ui);
		S3DCFG_RELOAD_SECTION(parameters);
	}

	struct Window
	{
		String title;
		Size size = s3d::Window::DefaultClientSize;

		void reload(const TOMLReader& toml)
		{
			S3DCFG_LOAD_SECTION(U"Window");
			S3DCFG_LOAD_VALUE(title);
			S3DCFG_LOAD_VALUE(size);
		}
	} window;

	struct UI
	{
		ColorF background = Palette::DefaultBackground;

		void reload(const TOMLReader& toml)
		{
			S3DCFG_LOAD_SECTION(U"UI");
			S3DCFG_LOAD_VALUE(background);
		}
	} ui;

	struct Parameter
	{
		int32 a;
		double b;
		String c;
		Vec3 d;
		Array<int32> e;
		Array<String> f;

		void reload(const TOMLReader& toml)
		{
			S3DCFG_LOAD_SECTION(U"Parameter");
			S3DCFG_LOAD_VALUE(a);
			S3DCFG_LOAD_VALUE(b);
			S3DCFG_LOAD_VALUE(c);
			S3DCFG_LOAD_VALUE(d);
			S3DCFG_LOAD_ARRAY(e);
			S3DCFG_LOAD_ARRAY(f);
		}
	} parameters;
};

using Config = ConfigFile<ConfigData>;

void Update(const Config& config)
{
	Window::SetTitle(config.window.title);
	Window::Resize(config.window.size);
	Graphics::SetBackground(config.ui.background);
	Print << config.parameters.a;
	Print << config.parameters.b;
	Print << config.parameters.c;
	Print << config.parameters.d;
	Print << config.parameters.e;
	Print << config.parameters.f;
}

void Main()
{
	const FilePath configPath = U"config.toml";
	Config config;

	try
	{
		config.reload(configPath);
	}
	catch (const ConfigError& err)
	{
		err.show();
		return;
	}

	Update(config);

	while (System::Update())
	{
		if (config.hasChanged())
		{
			try
			{
				ClearPrint();
				config.reload(configPath);
			}
			catch (const ConfigError& err)
			{
				err.print();
			}

			Update(config);
		}
	}
}
```

## 現時点での制約
「配列の配列」や「テーブルの配列」などを 1 行でロードできるマクロは未実装なので、各構造体の reload 内で、`TOMLReader` の API を使って自前でロードを実装する必要があります。

## まとめ
C++/OpenSiv3D プログラミングに Config クラスを導入して、高速な開発を実現しましょう。