Siv3D Advent Calendar 2015 - Qiitaの23日目の記事です。
でもこんなソースコードは嫌だ。
見づらいし調整のたびにビルドしなければならない。
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ファイル
1行目を識別子用の行、1列目をコメント用の列として確保している。
また、2列目は識別子を設定せず、列単位でのコメントとして活用する。
#実装
実装例は以下のとおり。
CSVファイルの値を変数に読み込み、長方形を描画している。
# 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さんの投稿です。