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

  • 2
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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

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

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

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
※実装例では敵のパラメータではなく、四角形のパラメータを管理する。

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

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

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

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

CSVファイル

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

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

実行するとこんな感じ。
yattaze.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ファイルを開いたまま実行した場合、パラメータが読み込まれないようである。
筆者は別のツールを使って編集している。
これなら開きっぱなしで実行しても問題なく読み込まれる。

まとめ

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

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

この投稿は Siv3D Advent Calendar 201523日目の記事です。