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

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

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

 さて、このようになっているXMLファイルから、ステージにオブジェクトを配置するわけですが、前項のCSV版と同様の機能がある、ゲームオブジェクトXMLビルダーを使用します。使用方法は、前項とあまり変わりません。GameStage::OnCreate()関数に記述があります。
void GameStage::OnCreate() {
    try {
        //ビューとライトの作成
        CreateViewLight();
        //オブジェクトのグループを作成する
        auto Group = CreateSharedObjectGroup(L"EnemyGroup");
        //ゲームオブジェクトビルダー
        GameObjecttXMLBuilder 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);
        //XMLからゲームオブジェクトの構築
        wstring XMLStr = DataDir + L"GameStage";
        XMLStr += Util::IntToWStr(m_StageNum);
        XMLStr += L".xml";
        Builder.Build(GetThis<Stage>(), XMLStr, L"GameStage/GameObject");
        //プレーヤーの作成
        CreatePlayer();
    }
    catch (...) {
        throw;
    }
}
 赤くなっているのがビルダー関連です。XML用にはGameObjecttXMLBuilderクラスを使用します。
 インスタンスを定義したら、前項のように
        Builder.Register<TilingPlate>(L"TilingPlate");
 とクラス名クラス名の文字列を対で登録します。
 登録が済んだら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のノードを受け取るコンストラクタを実際に見てみましょう。例として、TilingPlateクラスのコンストラクタを紹介します。
//構築と破棄
TilingPlate::TilingPlate(const shared_ptr<Stage>& StagePtr, IXMLDOMNodePtr pNode):
    GameObject(StagePtr)
{
    try {
        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");
        //トークン(カラム)の配列
        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',');
        Vec3 Rot;
        //回転は「XM_PIDIV2」の文字列になっている場合がある
        Rot.x = (Tokens[0] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[0].c_str());
        Rot.y = (Tokens[1] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[1].c_str());
        Rot.z = (Tokens[2] == L"XM_PIDIV2") ? XM_PIDIV2 : (float)_wtof(Tokens[2].c_str());
        //プレートの回転の引数はクオータニオンになっているので変換
        m_Qt.rotationRollPitchYawFromVector(Rot);
        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());
    }
    catch (...) {
        throw;
    }
}
 まず、渡されたノードを使って、L"Scale"L"Rot"と指定があるアトリビュートを取得します。
        auto ScaleStr = XmlDocReader::GetAttribute(pNode, L"Scale");
 というのは、XML上は、Scale="40,40,1"のようになっているアトリビュートです。この40,40,1という文字列がwstring型に変換されて取得できます。(ScaleStrはwstring型です)
 このように文字列を取得したら、後はトークンに分割して各値をメンバ変数m_Scaleなどに代入してます。トークン分割して、分割された文字列からfloat型への変換までの過程は、これまでのものと変わりません。
 このようにXMLからゲームオブジェクトを作成することがGameObjecttXMLBuilderクラスを使うことで比較的簡単に行うことができます。前項のCSV版と比べてみましょう。

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