CSVでお手軽パラメータ管理

[Siv3D Advent Calendar 2015 - Qiita](http://qiita.com/advent-calendar/2015/siv3d)の23日目の記事です。

突然だが、とりとめなく四角形を沢山表示したい。
![risou.png](https://qiita-image-store.s3.amazonaws.com/0/106059/33a17246-9784-0277-6f11-6a22abed94d4.png)

でもこんなソースコードは嫌だ。
見づらいし調整のたびにビルドしなければならない。

```cpp:test.cpp
Rect(10,10,20,200).draw({255,128,128,255});
Rect(30,30,40,220).draw({128,255,128,255});
Rect(70,50,60,240).draw({128,128,255,255});
Rect(130,70,80,260).draw({128,128,128,255});
Rect(210,90,100,280).draw({255,0,0,255});
Rect(310,110,120,300).draw({0,255,0,255});
Rect(430,130,140,320).draw({0,0,255,255});
Rect(570,150,160,340).draw({255,255,255,255});
```
そうだ。Siv3DではCSV形式で記述されたファイルを超お手軽に読み込めるではないか。
ということで、CSVを使ってパラメータ管理を効率化してみる。

# 概要

今回はゲームでの運用を想定し、
CSVファイルに記入した値を以下のように解釈する。

・縦軸：パラメータの種類。攻撃力とかHPとか。
・横軸：パラメータのバリエーション。雑魚敵とかボスとか。

つまりこうだ。
![mob.png](https://qiita-image-store.s3.amazonaws.com/0/106059/5b947db4-e8df-f32c-34f6-7708188c0dee.png)
※実装例では敵のパラメータではなく、四角形のパラメータを管理する。

今回の紹介でミソとなる点は以下のとおり。

**識別子によるパラメータ種類の管理**
CSVファイルの列（縦軸）でパラメータの種類（HP、攻撃力）を管理する。
各列にはそれぞれ識別子を設定し、読み込みを自動化することで、パラメータ種類の増減に対応しやすくする。

**IDによるパラメータバリエーションの管理**
行（横軸）をひとまとまりのパラメータバリエーション（雑魚A、ラスボス）とし、IDで管理する。
IDをキーとすることで、他のパラメータと協調しやすくなる。

**パラメータファイルのコメントアウト対応**
CSVファイル内で、パラメータを記載したままコメントアウトできるようにする。
調整の際に重宝する。


#CSVファイル

CSVではこんなふうにパラメータを記述する。
![data.png](https://qiita-image-store.s3.amazonaws.com/0/106059/b62f5a88-6838-06f7-35c4-0cba7e55a58b.png)

1行目を識別子用の行、1列目をコメント用の列として確保している。
また、2列目は識別子を設定せず、列単位でのコメントとして活用する。



#実装
実装例は以下のとおり。
CSVファイルの値を変数に読み込み、長方形を描画している。


```cpp:Main.cpp

# include <Siv3D.hpp>
# include <unordered_map>

//enumを使ってパラメータをID管理する
enum class EnumRectId{
	None			= 0,
	LightRedBox		= 100,
	LightGreenBox	= 200,
	LightBlueBox	= 300,
	RedBox			= 400,
	GreenBox		= 500,
	BlueBox			= 600,
	WhiteBox		= 700,
};

//CSVのパラメータを直接使うより読みやすい変数に移したほうがよい
struct StructRectParam{
	EnumRectId RectId;
	Point Size;
	Point Pos;
	Color Color;
};

void Main()
{
	//CSV読み込み
	CSVReader Csv(L"data.csv");

	//パラメータの各列の識別子を定義
	String RectId = L"RectId";
	String PosX = L"PosX", PosY = L"PosY", SizeX = L"SizeX", SizeY = L"SizeY";
	String ColorR = L"ColorRed", ColorG = L"ColorGreen", ColorB = L"ColorBlue", ColorA = L"ColorAlpha";

	//mapで識別子と対応する列を関連付け
	std::unordered_map<String, int> ItemMap;
	for (int i = 0; i < Csv.columns(0); i++){
		ItemMap[Csv.get<String>(0, i)] = i; //コメント列もマッピングされるが許容
	}	

	//パラメータ本体をArrayではなくmapで管理する
	//それはそれとして一覧用にArrayを準備
	std::unordered_map<EnumRectId, StructRectParam> ParamMap;
	Array<EnumRectId> ParamIdList;

	//パラメータ読み込み処理
	for (int i = 0; i < Csv.rows; i++){
		//一列目が空でないならその行は飛ばす（コメントアウト処理）
		if (Csv.get<String>(i, 0) != L""){ continue; }

		//tmpParamにパラメータを読み込んで、ParamMapに追加
		//識別子mapで識別子を読み込み対象列に変換
		StructRectParam tmpParam;
		//enumは直接読めないので一度intで読んでからenumにキャスト
		tmpParam.RectId = (EnumRectId)Csv.get<int>(i, ItemMap[RectId]);
		tmpParam.Pos =	Point(Csv.get<int>(i, ItemMap[PosX]), Csv.get<int>(i, ItemMap[PosY]));
		tmpParam.Size = Point(Csv.get<int>(i, ItemMap[SizeX]), Csv.get<int>(i, ItemMap[SizeY]));
		tmpParam.Color = Color(
			Csv.get<int>(i, ItemMap[ColorR]), Csv.get<int>(i, ItemMap[ColorG]), Csv.get<int>(i, ItemMap[ColorB]), Csv.get<int>(i, ItemMap[ColorA]));
		
		ParamMap[tmpParam.RectId] = (tmpParam);	//mapにRectIdをキーとして格納
		ParamIdList.push_back(tmpParam.RectId);	//ArrayにRectIdを追加
	}

	while (System::Update())
	{
		//パラメータの数だけ長方形を描画
		for (auto prmId : ParamIdList){
			//こういう時にArrayが役に立つ
			Rect(ParamMap[prmId].Pos, ParamMap[prmId].Size).draw(ParamMap[prmId].Color);
		}

		//パラメータとは別の位置にID指定で長方形を描画
		//普段使うならむしろこちらがメインになる
		Rect(Point(50, 400), ParamMap[EnumRectId::GreenBox].Size).draw(ParamMap[EnumRectId::GreenBox].Color);
	}
}
```

実行するとこんな感じ。
![yattaze.png](https://qiita-image-store.s3.amazonaws.com/0/106059/898848f2-7581-5af2-4846-a10cececf5a8.png)

#補足
**わざわざIDで呼び出すの回りくどくない？**
コメントに記載したとおり、IDをパラメータ群の識別子にしておくと何かと取り回しがいい。
例えば雑魚Aのパラメータが1種類だけならばいいが、実は他にも描画パラメータがあったり、あとから仲間になった時のパラメータが追加になる、などすると収集がつかなくなる。
ここで各項目間で共通のIDがあれば便利だ。雑魚Aに関わることなら全て雑魚AのIDを投げておけばいい。

**なぜIDをenumで管理しているの？intかStringでもよくね？**
値の直打ちはTypoひとつで問題に繋がる。
IDは似たような字面になることが多いので、うっかり似たような別のIDにしてしまったら原因を突き止めるのも一苦労だ。
VC++2013ならオートコレクト機能があるのでTypoは少なくなるし、長いIDを覚えておく必要がない。
enum classを使用することでIDの属性を明確にすることができる。EnumGameParamIdならゲーム用パラメータIDだし、EnumDrawParamIdなら描画パラメータIDだ。
ソースコードの外でもenumで管理できると楽なのだがこればかりは仕方ない（変換クラスを作るのも本末転倒のような気がする）。
ちなみに、頭にEnumとつけているのはオートコレクトで呼び出しやすいからだ。同じ理由で構造体にもStructをつけている。

**ExcelでCSVを開いたままだと読み込めない**
ExcelでCSVファイルを開いたまま実行した場合、パラメータが読み込まれないようである。
筆者は[別のツール](http://qiita.com/drafts/41996822664477fdf666)を使って編集している。
これなら開きっぱなしで実行しても問題なく読み込まれる。

# まとめ
・CSVは、沢山のパラメータを管理する場合に役立つ
・識別子でパラメータを、IDでバリエーションを管理できると扱いやすい
・enumは直接扱えない。intで読み込んでキャストしよう。
・CSVを開いたままでは読み込まれないことがある。そんな時は編集ツールを変えてみよう。


明日は@hat_a2cさんの投稿です。