16.データの読み込み

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


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

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

XMLによるゲームオブジェクトビルダー

 前項ではCSVによるゲームオブジェクトビルダーを紹介しました。今項ではそのXML版を紹介します。FullSample604を実行してみてください。実行画面は前項と同じになります。
 まず、XMLですがmediaディレクトリGameStage1.xmlというXMLファイルがあります。その中身は以下です。
<?xml version="1.0" encoding="utf-8" ?>
<GameStage>
    <GameObject Type="TilingFixedBox" Scale="50,1,50" Rot="0,0,0" Pos="0,-0.5,0" UPic="1.0" VPic="1.0" Tex="SKY_TX" />
    <GameObject Type="TilingFixedBox" Scale="40,1,1" Rot="0,0,0" Pos="0,0.5,19.5" UPic="1.0" VPic="1.0" Tex="WALL_TX" />
    <GameObject Type="TilingFixedBox" Scale="40,1,1" Rot="0,0,0" Pos="0,0.5,-19.5" UPic="1.0" VPic="1.0" Tex="WALL_TX" />
    <GameObject Type="TilingFixedBox" Scale="40,1,1" Rot="0,XM_PIDIV2,0" Pos="19.5,0.5,0" UPic="1.0" VPic="1.0" Tex="WALL_TX" />
    <GameObject Type="TilingFixedBox" Scale="40,1,1" Rot="0,XM_PIDIV2,0" Pos="-19.5,0.5,0" UPic="1.0" VPic="1.0" Tex="WALL_TX" />
    <GameObject Type="SeekObject" Pos="-10,0.25,0"  />
    <GameObject Type="SeekObject" Pos="10,0.25,0"  />
    <GameObject Type="SeekObject" Pos="0,0.25,-10"  />
    <GameObject Type="SeekObject" Pos="0,0.25,10"  />
    <GameObject Type="MoveBox" Pos="-15,0.5,0"  />
    <GameObject Type="MoveBox" Pos="15,0.5,0"  />
    <GameObject Type="MoveBox" Pos="0,0.5,-15"  />
    <GameObject Type="MoveBox" Pos="0,0.5,15"  />
</GameStage>
 XMLを見てみると<GameObject>タグが複数作成されています。これが1つのゲームオブジェクトを表すノードになります。
 そして、<GameObject>タグにあるType="TilingFixedBox"という記述がクラス名を表します。タグの囲みの中にあるhoge="huga"のような記述をアトリビュートといいます。

 さて、このようになっているXMLファイルから、ステージにオブジェクトを配置するわけですが、前項のCSV版と同様の機能がある、ゲームオブジェクトXMLビルダーを使用します。使用方法は、前項とあまり変わりません。GameStage::OnCreate()関数に記述があります。
void GameStage::OnCreate() {
    try {
        //ビューとライトの作成
        CreateViewLight();
        //オブジェクトのグループを作成する
        auto Group = CreateSharedObjectGroup(L"SeekGroup");
        //ゲームオブジェクトビルダー
        GameObjecttXMLBuilder Builder;
        //ゲームオブジェクトの登録
        Builder.Register<TilingFixedBox>(L"TilingFixedBox");
        Builder.Register<SeekObject>(L"SeekObject");
        Builder.Register<MoveBox>(L"MoveBox");
        wstring DataDir;
        App::GetApp()->GetDataDirectory(DataDir);
        //XMLからゲームオブジェクトの構築
        wstring XMLStr = DataDir + L"GameStage";
        auto shScene = App::GetApp()->GetScene<Scene>();
        XMLStr += Util::IntToWStr(shScene->GetStageNum());
        XMLStr += L".xml";
        Builder.Build(GetThis<Stage>(), XMLStr, L"GameStage/GameObject");
        //プレーヤーの作成
        CreatePlayer();
    }
    catch (...) {
        throw;
    }
}
 赤くなっているのがビルダー関連です。XML用にはGameObjecttXMLBuilderクラスを使用します。
 インスタンスを定義したら、前項のように
        Builder.Register<TilingFixedBox>(L"TilingFixedBox");
 とクラス名クラス名の文字列を対で登録します。
 登録が済んだらXMLファイル名を作成し、ゲームオブジェクトへのパスと一緒にBuild()関数に渡します。
        Builder.Build(GetThis<Stage>(), XMLStr, L"GameStage/GameObject");
 こんな感じです。ゲームオブジェクトへのパスXMLのパスです。XMLはノードが入れ子状態になっているので、ディレクトリのパスのように考えられます。L"GameStage/GameObject"というのはGameStage配下のGameObjectという意味です。
 XMLパスディレクトリのパスと違うところは同じノード名を複数作れるところにあります。つまりL"GameStage/GameObject"に該当するノードは複数あることになります。
 GameObjecttXMLBuilderクラスは指定されたパスを抽出して該当するノードをすべて読み取ります。そして、そのTypeアトリビュートに該当するクラスを構築して、コンストラクタにそのノードを渡します。
 ですから、CSVのビルダー1行のCSVを渡すのに対して、XMLのビルダー1つのノードを渡します。XMLのノードは子ノードを作成することもできますから、当然、CSVより複雑なデータを渡すことも可能です。しかし、このサンプルでは前項との比較が目的ですので、XMLの構造も単純になってます。

XMLのノードを受け取るコンストラクタ

 ではXMLのノードを受け取るコンストラクタを実際に見てみましょう。例として、TilingFixedBoxクラスのコンストラクタを紹介します。
TilingFixedBox::TilingFixedBox(const shared_ptr<Stage>& StagePtr, IXMLDOMNodePtr pNode) :
    GameObject(StagePtr)
{
    auto ScaleStr = XmlDocReader::GetAttribute(pNode, L"Scale");
    auto RotStr = XmlDocReader::GetAttribute(pNode, L"Rot");
    auto PosStr = XmlDocReader::GetAttribute(pNode, L"Pos");
    auto UPicStr = XmlDocReader::GetAttribute(pNode, L"UPic");
    auto VPicStr = XmlDocReader::GetAttribute(pNode, L"VPic");
    auto TexStr = XmlDocReader::GetAttribute(pNode, L"Tex");
    //トークン(カラム)の配列
    vector<wstring> Tokens;
    //トークン(カラム)単位で文字列を抽出(L','をデリミタとして区分け)
    Tokens.clear();
    Util::WStrToTokenVector(Tokens, ScaleStr, L',');
    //各トークン(カラム)をスケール、回転、位置に読み込む
    m_Scale = Vec3(
        (float)_wtof(Tokens[0].c_str()),
        (float)_wtof(Tokens[1].c_str()),
        (float)_wtof(Tokens[2].c_str())
    );
    Tokens.clear();
    Util::WStrToTokenVector(Tokens, RotStr, L',');
    //回転は「XM_PIDIV2」の文字列になっている場合がある
    m_Rotation.x = (Tokens[0] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[0].c_str());
    m_Rotation.y = (Tokens[1] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[1].c_str());
    m_Rotation.z = (Tokens[2] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[2].c_str());
    Tokens.clear();
    Util::WStrToTokenVector(Tokens, PosStr, L',');
    m_Position = Vec3(
        (float)_wtof(Tokens[0].c_str()),
        (float)_wtof(Tokens[1].c_str()),
        (float)_wtof(Tokens[2].c_str())
    );
    m_UPic = (float)_wtof(UPicStr.c_str());
    m_VPic = (float)_wtof(VPicStr.c_str());
    m_Texname = TexStr;
}
 まず、渡されたノードを使って、L"Scale"L"Rot"と指定があるアトリビュートを取得します。
        auto ScaleStr = XmlDocReader::GetAttribute(pNode, L"Scale");
 というのは、XML上は、Scale="50,1,50"のようになっているアトリビュートです。この50,1,50という文字列がwstring型に変換されて取得できます。(ScaleStrはwstring型です)
 このように文字列を取得したら、後はトークンに分割して各値をメンバ変数m_Scaleなどに代入してます。トークン分割して、分割された文字列からfloat型への変換までの過程は、これまでのものと変わりません。
 このようにXMLからゲームオブジェクトを作成することがGameObjecttXMLBuilderクラスを使うことで比較的簡単に行うことができます。前項のCSV版と比べてみましょう。

 さて、では、データの読み込みをどのように設計したらいいのでしょうか?
 これはゲームによるとしか言いようがありません。例えばステージ上にセルをまたぐオブジェクトがないのであればFullSample601のようなCSVのセルマップやFullSample602XMLによるセルマップ読み込みがいいでしょう。しかし、オブジェクトが体力や知力や攻撃力などの複雑なパラメータを持っているのであれば、CSVだと、1行がとんでもなく長いものになってしまいます。そういう場合は構造的に扱えるXML形式がいいと思います。
 また、ゲーム中にパラメータの保存が行われる場合も、XML形式で保存する(この場合はXmlDocクラスを使って保存する)のが安全でしょう。CSVも保存可能ですが、単純なCSVであれば保存もいいですが、複雑なデータをCSVで保存するのは不具合のもとになります。CSVはどうしても1列ずれただけで大変なことになるデータ形式ですから。