16.データの読み込み

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


 このサンプルはFullSample603というディレクトリに含まれます。
 BaseCrossDx11.slnというソリューションを開くとDx11版が起動します。
 このサンプルは現在Dx12版はありません。

 実行結果は1601と変わりませんので、そちらを確認ください。

リフレクションについて

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

 GameObjecttCSVBuilderクラスは、CSVを読み込んで、その各行の先頭のカラムにある文字列から、その文字列に対応するクラスを構築します。その時、そのCSVを1行だけコンストラクタに渡します。
 ですから、各ゲームオブジェクトは、その1行の文字列を受け取るコンストラクタを記述する必要があります。以下はTilingFixedBoxのコンストラクタです。Character.cppにあります。
TilingFixedBox::TilingFixedBox(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())
        );
        //回転はXM_PIDIV2の文字列になっている場合がある
        m_Rotation.x = (Tokens[4] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[4].c_str());
        m_Rotation.y = (Tokens[5] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[5].c_str());
        m_Rotation.z = (Tokens[6] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[6].c_str());
        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());
        m_Texname = Tokens[12];
    }
    catch (...) {
        throw;
    }
}
 赤くなっているconst wstring& LineGameObjecttCSVBuilderから渡される1行分のCSVです。ですから、コンストラクタではその文字列をトークンに分割して、必要なトークンをメンバ変数(m_Scaleやm_Position)に代入してます。
 この方式の利点は、クラスごとの引数を個別に変えられることです。渡される引数はconst wstring& Lineですが、その内容は自由に設定できます。例えばTilingFixedBoxクラススケーリング、回転、位置、テクスチャなどの情報が入ってますが、SeekObjectクラスなどは位置(つまりPosition)しか入ってません。
 つまり行ごとにCSVの内容を変えて記述することが可能です。TilingPlateクラスに渡されるCSV行は、左から2番目のカラム(1番目はクラス名が入る)はScaleですが、Enemy1クラスに渡されるのはPositionです。
 このように、各ゲームオブジェクトクラスのコンストラクタでCSV読み込みが完結するので、クラス単位で独立したコーディングが可能になります。