Edited at
Siv3DDay 23

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

More than 3 years have passed since last update.

Siv3D Advent Calendar 2015 - Qiitaの23日目の記事です。

突然だが、とりとめなく四角形を沢山表示したい。

でもこんなソースコードは嫌だ。

見づらいし調整のたびにビルドしなければならない。


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とか。

・横軸:パラメータのバリエーション。雑魚敵とかボスとか。

つまりこうだ。



※実装例では敵のパラメータではなく、四角形のパラメータを管理する。

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

識別子によるパラメータ種類の管理

CSVファイルの列(縦軸)でパラメータの種類(HP、攻撃力)を管理する。

各列にはそれぞれ識別子を設定し、読み込みを自動化することで、パラメータ種類の増減に対応しやすくする。

IDによるパラメータバリエーションの管理

行(横軸)をひとまとまりのパラメータバリエーション(雑魚A、ラスボス)とし、IDで管理する。

IDをキーとすることで、他のパラメータと協調しやすくなる。

パラメータファイルのコメントアウト対応

CSVファイル内で、パラメータを記載したままコメントアウトできるようにする。

調整の際に重宝する。


CSVファイル

CSVではこんなふうにパラメータを記述する。

1行目を識別子用の行、1列目をコメント用の列として確保している。

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


実装

実装例は以下のとおり。

CSVファイルの値を変数に読み込み、長方形を描画している。


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);
}
}


実行するとこんな感じ。


補足

わざわざ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ファイルを開いたまま実行した場合、パラメータが読み込まれないようである。

筆者は別のツールを使って編集している。

これなら開きっぱなしで実行しても問題なく読み込まれる。


まとめ

・CSVは、沢山のパラメータを管理する場合に役立つ

・識別子でパラメータを、IDでバリエーションを管理できると扱いやすい

・enumは直接扱えない。intで読み込んでキャストしよう。

・CSVを開いたままでは読み込まれないことがある。そんな時は編集ツールを変えてみよう。

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