603.オブジェクトビルダーを使ったデータの読み込み(CSV版)

リフレクションについて

 一般的なプログラミング言語の機能にリフレクションというのがあります。
 例えばインスタンス構築
    class TestClass{
    public:
        TestClass(){}
        //...
    };

    int main(){
        //以下のようなコードは書けません
        auto Ptr = new GetType("TestClass");
        //...
        return 0;
    }
 のように記述できたら便利だとは思いませんか?
 どういうときに利用できるかというと、文字列を使ってクラスの構築ができることです。こういう機能をリフレクションといいます。
 文字列でクラスのインスタンスを構築できれば、例えばCSVやXMLにクラス名を記述しておくことで、そのクラスを構築することが可能になります。
 しかし残念ながらネイティブなC++にはこういう機能はありません(C++/CLIではできます)。ですので、ネイティブC++の場合はリフレクションを自作しなければいけません。
 BaseCrossには擬似的なリフレクションを使ったゲームオブジェクトビルダーが実装されてます。この機能を使うと、CSVやXMLにクラスの識別名を記述することで、そのクラスのコンストラクタに、構築に必要なパラメータを渡すことが可能になります。
 FullSample603FullSample604ゲームオブジェクトビルダーを利用したサンプルです。
 今項で紹介するFullSample603リフレクションを使って、CSVからゲームオブジェクトを構築するサンプルとなります。
 ビルドして実行するとFullSample602と同じステージになります。しかしGameStage.cppを見ていただくと、構築用のコードがほとんど記述されてないのがわかると思います。構築用のデータは、ビューやプレイヤーなど一部のクラスを除いてCSVファイルから読み込んでいます。

CSVの準備

 mediaディレクトリの中にはGameStage1.csvがあります。以下がその内容です。
TilingPlate,40,40,1,XM_PIDIV2,0,0,0,0,0,1,1
TilingFixedBox,40,1,1,0,0,0,0,0.5,19.5,1,1
TilingFixedBox,40,1,1,0,0,0,0,0.5,-19.5,1,1
TilingFixedBox,40,1,1,0,XM_PIDIV2,0,19.5,0.5,0,1,1
TilingFixedBox,40,1,1,0,XM_PIDIV2,0,-19.5,0.5,0,1,1
Enemy1,-10,0.25,0,,,,,,,,
Enemy1,10,0.25,0,,,,,,,,
Enemy1,0,0.25,-10,,,,,,,,
Enemy1,0,0.25,10,,,,,,,,
Enemy2,-15,0.25,0,,,,,,,,
Enemy2,15,0.25,0,,,,,,,,
Enemy2,0,0.25,-15,,,,,,,,
Enemy2,0,0.25,15,,,,,,,,
Enemy3,10,0.25,10,,,,,,,,
 それぞれ0列目(一番左)にクラス名が記述されているのがわかります。オブジェクトビルダーは、このクラス名に対応するクラスを作成し、そのコンストラクタに該当の行を渡します。
 では、実装を見てみましょう。以下はGameStage::OnCreate()関数です。
void GameStage::OnCreate() {
    try {
        //ビューとライトの作成
        CreateViewLight();
        //オブジェクトのグループを作成する
        auto Group = CreateSharedObjectGroup(L"EnemyGroup");
        //ゲームオブジェクトビルダー
        GameObjecttCSVBuilder Builder;
        //ゲームオブジェクトの登録
        Builder.Register<TilingPlate>(L"TilingPlate");
        Builder.Register<TilingFixedBox>(L"TilingFixedBox");
        Builder.Register<Enemy1>(L"Enemy1");
        Builder.Register<Enemy2>(L"Enemy2");
        Builder.Register<Enemy3>(L"Enemy3");
        wstring DataDir;
        App::GetApp()->GetDataDirectory(DataDir);
        //CSVからゲームオブジェクトの構築
        wstring CSVStr = DataDir + L"GameStage";
        CSVStr += Util::IntToWStr(m_StageNum);
        CSVStr += L".csv";
        Builder.Build(GetThis<Stage>(), CSVStr);
        //プレーヤーの作成
        CreatePlayer();
    }
    catch (...) {
        throw;
    }
}
 オブジェクトビルダーにかかわる操作は赤くなっているところです。
 まず、
        GameObjecttCSVBuilder Builder;
 とGameObjecttCSVBuilderのインスタンスを定義します。
 これはCSV読み込み用のビルダーです。次項にはXML読み込み用のビルダーを紹介します。
 ビルダーの操作としては、まず、クラス名クラス名の文字列の対を登録します。
        Builder.Register<TilingPlate>(L"TilingPlate");
 のような感じです。これはTilingPlate型L"TilingPlate"文字列に対応させています。文字列はCSVに記述される文字列です。通常これは型名と同じ名前にします。
 冒頭に述べたリフレクションを使っているのが、GameObjecttCSVBuilderとその関連クラスです。興味がある人は読んでみましょう。
 クラス名の登録が終わったら、CSVファイル名を指定して
        Builder.Build(GetThis<Stage>(), CSVStr);
 と呼び出します。すると、各ゲームオブジェクトがCSVの内容に従って構築されます。
 ここでCSVファイル名を作成するのにm_StageNumを使っています。この変数はGameStageクラスに、シーンから渡される変数で、複数のゲームステージを作る場合の参考になると思います。
 さてFullSample601FullSample602にあったCSVから個別データの取得はどこに行ってしまったのでしょうか?
 実は、各ゲームオブジェクトクラスのコンストラクタに記述します。

 GameObjecttCSVBuilderクラスは、CSVを読み込んで、その各行の先頭のカラムにある文字列から、その文字列に対応するクラスを構築します。その時、そのCSVを1行だけコンストラクタに渡します。
 ですから、各ゲームオブジェクトは、その1行の文字列を受け取るコンストラクタを記述する必要があります。以下はTilingPlateのコンストラクタです。
//構築と破棄
TilingPlate::TilingPlate(const shared_ptr<Stage>& StagePtr, const wstring& Line) :
    GameObject(StagePtr)
{
    try {
        //トークン(カラム)の配列
        vector<wstring> Tokens;
        Util::WStrToTokenVector(Tokens, Line, L',');
        //各トークン(カラム)をスケール、回転、位置に読み込む
        m_Scale = Vec3(
            (float)_wtof(Tokens[1].c_str()),
            (float)_wtof(Tokens[2].c_str()),
            (float)_wtof(Tokens[3].c_str())
        );
        Vec3 Rot;
        //回転は「XM_PIDIV2」の文字列になっている場合がある
        Rot.x = (Tokens[4] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[4].c_str());
        Rot.y = (Tokens[5] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[5].c_str());
        Rot.z = (Tokens[6] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[6].c_str());
        //プレートの回転の引数はクオータニオンになっているので変換
        m_Qt.rotationRollPitchYawFromVector(Rot);
        m_Position = Vec3(
            (float)_wtof(Tokens[7].c_str()),
            (float)_wtof(Tokens[8].c_str()),
            (float)_wtof(Tokens[9].c_str())
        );
        m_UPic = (float)_wtof(Tokens[10].c_str());
        m_VPic = (float)_wtof(Tokens[11].c_str());
    }
    catch (...) {
        throw;
    }
}
 赤くなっているconst wstring& LineGameObjecttCSVBuilderから渡される1行分のCSVです。ですから、コンストラクタではその文字列をトークンに分割して、必要なトークンをメンバ変数(m_Scaleやm_Position)に代入してます。
 この方式の利点は、クラスごとの引数を個別に変えられることです。渡される引数はconst wstring& Lineですが、その内容は自由に設定できます。例えばTilingPlateクラススケーリング、回転、位置などの情報が入ってますが、Enemy1クラスなどは位置(つまりPosition)しか入ってません。
 つまり行ごとにCSVの内容を変えて記述することが可能です。TilingPlateクラスに渡されるCSV行は、左から2番目のカラム(1番目はクラス名が入る)はScaleですが、Enemy1クラスに渡されるのはPositionです。
 このように、各ゲームオブジェクトクラスのコンストラクタでCSV読み込みが完結するので、クラス単位で独立したコーディングが可能になります。