16.データの読み込み

1602.XMLからのデータの読み込み


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

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

XMLとは何か

 XML形式とは、インターネットで広く使用されているテキスト文書の仕様であり、DOM(ドキュメントオブジェクトモデル)によって読み書きされます。
 HTML形式とも似ていますが、HTML形式が、主にデザインやコントロールや画像などのオブジェクトを記述する形式の仕様であるのに対して、XML形式データそのものを記述します。
 HTML形式XML形式の違いを述べることで、XML形式の説明を始めます。
 まず、HTML形式です。以下は内容の抜粋ですが
<p>
テスト文書<br />
<input type="text" name="name" value="" /><br />
<a href="hogehoge.html">ほげ</a><br />
<img src="hoge.png" />
</p>
 ここでは、pタグというブロックで囲まれて、中に、テスト文書というテキストがあります。ほかに、改行をあらわすbrタグ、入力フィールドをあらわすinputタグ、リンクをあらわすaタグ、そして画像をあらわすimgタグがあります。
 これらのタグ(<と>で囲まれたブロック)は、上記の例を見ると明らかのように、用途がはっきりしています。つまり、ブラウザはこれらのタグの指定に合わせて、リンクを作成したり、入力フィールドを作成したりします。
 意味のないタグやブラウザが理解できないタグを記述すると、ブラウザは無視します。

 それに対して、XML形式の例は以下のようになります。HTML形式形式同様、抜粋ですが
<data>
    <item id="1">テスト1</item>
    <item id="2">テスト2</item>
    <item id="3">テスト3</item>
    <item id="4">テスト4</item>
</data>
 タグアトリビュート(id="1"のような記述)、そしてタグ内のテキストで構成されていれば、タグ名は何でもよい(ただし日本語はおすすめしない)のがXML形式です。
 そして必ず矛盾のないツリー構造になってないといけません。(開始タグには、それに対応する終了タグが必要)
 そういう意味では、HTMLはXMLの一部と考えられますが、実はそうでもありません。なぜならHTMLは場合によっては終了タグがなくてもよい記述ができます。
 例外的な記述として
<data>
    <item id="1" />
</data>
 という記述が認められています(HTMLのinputタグのような記述)。内部テキストを持たないタグに対して、このような記述が可能です。これは以下と同じ意味になります。
<data>
    <item id="1"></item>
</data>

XMLヘッダ

 上記のXML説明で抜粋ですがと断ったのは、ここにXMLヘッダというのが入って、完全なXML形式となります。以下はヘッダも含めた記述です。
<?xml version="1.0" encoding="utf-8" ?>
<data>
    <item id="1">テスト1</item>
    <item id="2">テスト2</item>
    <item id="3">テスト3</item>
    <item id="4">テスト4</item>
</data>
 これで、BaseCrossから読み込めるようになります。encoding="utf-8"とあるように、通常XMLファイルは、UTF-8で記述します。Shift-JISで記述する場合は、
<?xml version="1.0" encoding="Shift-JIS" ?>
<data>
    <item id="1">テスト1</item>
    <item id="2">テスト2</item>
    <item id="3">テスト3</item>
    <item id="4">テスト4</item>
</data>
 のように記述します。どちらのエンコードで記述しても、読みこんだあとはwstringで扱えるようになります。
 このほかに、ヘッダには
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
 のように、スタンドアローンかどうかを加えることもありますが、これは、外部参照をしてるかしてないかの宣言です。standalone="yes"となっていて外部参照していると読み込みエラーになります。
 外部参照は、仕様的には使えないこともないですが、この説明の範疇を超えますので、省略します。

XmlDocReaderクラス

 今項のサンプルの実行イメージは前項のCSV読み込みのものとと同じです。使用する敵クラスなども同じクラスを使用しています。
 今項で使用するXMLは以下のようになっています。mediaディレクトリGameStage.xmlとして保存されています。
<?xml version="1.0" encoding="utf-8" ?>
<GameStage>
    <GameObjects>
        <TilingFixedBox>
            <Scale>50,1,50</Scale>
            <Rot>0,0,0</Rot>
            <Pos>0,-0.5,0</Pos>
            <Tex>SKY_TX</Tex>
        </TilingFixedBox>
        <TilingFixedBox>
            <Scale>40,1,1</Scale>
            <Rot>0,0,0</Rot>
            <Pos>0,0.5,19.5</Pos>
            <Tex>WALL_TX</Tex>
        </TilingFixedBox>
        <TilingFixedBox>
            <Scale>40,1,1</Scale>
            <Rot>0,0,0</Rot>
            <Pos>0,0.5,-19.5</Pos>
            <Tex>WALL_TX</Tex>
        </TilingFixedBox>
        <TilingFixedBox>
            <Scale>40,1,1</Scale>
            <Rot>0,XM_PIDIV2,0</Rot>
            <Pos>19.5,0.5,0</Pos>
            <Tex>WALL_TX</Tex>
        </TilingFixedBox>
        <TilingFixedBox>
            <Scale>40,1,1</Scale>
            <Rot>0,XM_PIDIV2,0</Rot>
            <Pos>-19.5,0.5,0</Pos>
            <Tex>WALL_TX</Tex>
        </TilingFixedBox>
    </GameObjects>
    <CellMap>0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,2,0,0,0,0,1,0,0,0,0,0,0,0,0,0,A,0,0,0,0,0,0,0,0,0,1,0,0,0,0,2,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</CellMap>
</GameStage>
 このようにCSVでは2つのファイルに分かれていたデータが1つのファイルに収まっています。気を付けたいのは<CellMap>タグの部分です。余分な改行が入り込まないようにしています。

 前項のCsv読み込みにはCsvFileクラスがあったように、BaseCrossでは、XMLの読み込みXmlDocReaderクラスがあります。もしXMLの保存をしたければXmlDocクラスを使用します。XmlDocReaderクラス読み込み専用のクラスです。
 XmlDocReaderクラスコンストラクタXMLファイル名、またはXMLが記述されているwstring型のメモリを渡す必要があります。ですので、今回はGamaStage内のインスタンスには、ポインタを使います。GameStage.hに、以下のように記述します。
class GameStage : public Stage {
    //XMLリーダー
    unique_ptr<XmlDocReader> m_XmlDocReader;
    //以下略
};
 このように準備しておいて、GameStage::OnCreate()で以下のように記述します。
void GameStage::OnCreate() {
    try {
        wstring DataDir;
        App::GetApp()->GetDataDirectory(DataDir);
        //XMLの読み込み
        m_XmlDocReader.reset(new XmlDocReader(DataDir + L"GameStage.xml"));
        //中略
    }
    catch (...) {
        throw;
    }
}
 このように準備しておけば、各オブジェクトの構築時にXMLから取得できるようになります。
 まずセルマップに記載されるオブジェクトの読み込みですが、GameStage::CreateXmlObjects()関数に記述されています。
//XMLのオブジェクトの作成
void GameStage::CreateXmlObjects() {
    //オブジェクトのグループを作成する
    auto group = CreateSharedObjectGroup(L"SeekGroup");
    //セルマップのノードを取得
    auto CellmapNode = m_XmlDocReader->GetSelectSingleNode(L"GameStage/CellMap");
    if (!CellmapNode) {
        throw BaseException(
            L"GameStage/CellMapが見つかりません",
            L"if (!CellmapNode)",
            L"GameStage::CreateEnemy()"
        );
    }
    //内容の文字列を取得
    wstring MapStr = XmlDocReader::GetText(CellmapNode);
    vector<wstring> LineVec;
    //最初に改行をデリミタとした文字列の配列にする
    Util::WStrToTokenVector(LineVec, MapStr, L'\n');
    for (size_t i = 0; i < LineVec.size(); i++) {
        //トークン(カラム)の配列
        vector<wstring> Tokens;
        //トークン(カラム)単位で文字列を抽出(L','をデリミタとして区分け)
        Util::WStrToTokenVector(Tokens, LineVec[i], L',');
        for (size_t j = 0; j < Tokens.size(); j++) {
            //XとZの位置を計算
            float XPos = (float)((int)j - 19);
            float ZPos = (float)(19 - (int)i);
            if (Tokens[j] == L"1") {
                AddGameObject<SeekObject>(Vec3(XPos, 0.125f, ZPos));

            }
            else if (Tokens[j] == L"2") {
                AddGameObject<MoveBox>(
                    Vec3(1.0f, 1.0f, 1.0f),
                    Vec3(0.0f, 0.0f, 0.0f),
                    Vec3(XPos, 0.5f, ZPos));

            }
        }
    }
}
 まず最初に、
    //セルマップのノードを取得
    auto CellmapNode = m_XmlDocReader->GetSelectSingleNode(L"GameStage/CellMap");
 のように、CellMapタグノードを取得します。XMLはタグを使った入れ子構造になっています。それぞれのタグは内部的にはノードと表現されまず。
 ノードを検索するにはL"GameStage/CellMap"のように/で区切って階層を指定します。ディレクトリ階層のような感じです。GetSelectSingleNode()関数1つのノードという意味です。複数のノードを指定することもできます(後ほど出てきます)。
 CellMapタグノードが取得できたら、
    //内容の文字列を取得
    wstring MapStr = XmlDocReader::GetText(CellmapNode);
 でその中身の文字列を取得します。ここではスタティック関数を利用します。取り出したノードの情報にアクセスするのにはm_XmlDocReaderインスタンスは必要がありません。また後述しますがサブノードを取得する場合もスタティック関数を使用します。
 これでMapStrにはすべてのマップデータが入ります。
 この後、
    vector<wstring> LineVec;
    //最初に改行をデリミタとした文字列の配列にする
    Util::WStrToTokenVector(LineVec, MapStr, L'\n');
 という形で改行をデリミタにして文字列の配列に分割します。これでLineVecには行の文字列の配列が作成されます。
 この後の操作はCSVの読み込みと同じです。1行,をデリミタにして分割して、各セルの内容をスキャンします。

 続いて、固定ボックスですが、こちらはGameStage::CreateFixedBox()関数に記述されています。
//ボックスの作成
void GameStage::CreateFixedBox() {
    auto BoxNodes = m_XmlDocReader->GetSelectNodes(L"GameStage/GameObjects/TilingFixedBox");
    if (!BoxNodes) {
        throw BaseException(
            L"GameStage/GameObjects/TilingFixedBoxが見つかりません",
            L"if (!BoxNodes)",
            L"GameStage::CreateFixedBox()"
        );
    }
    long CountNode = XmlDocReader::GetLength(BoxNodes);
    for (long i = 0; i < CountNode; i++) {
        auto Node = XmlDocReader::GetItem(BoxNodes, i);
        auto ScaleNode = XmlDocReader::GetSelectSingleNode(Node, L"Scale");
        auto RotNode = XmlDocReader::GetSelectSingleNode(Node, L"Rot");
        auto PosNode = XmlDocReader::GetSelectSingleNode(Node, L"Pos");
        auto TexNode = XmlDocReader::GetSelectSingleNode(Node, L"Tex");
        wstring ScaleStr = XmlDocReader::GetText(ScaleNode);
        wstring RotStr = XmlDocReader::GetText(RotNode);
        wstring PosStr = XmlDocReader::GetText(PosNode);
        wstring TexStr = XmlDocReader::GetText(TexNode);
        //トークン(カラム)の配列
        vector<wstring> Tokens;
        //トークン(カラム)単位で文字列を抽出(L','をデリミタとして区分け)
        Util::WStrToTokenVector(Tokens, ScaleStr, L',');
        //各トークン(カラム)をスケール、回転、位置に読み込む
        Vec3 Scale(
            (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());
        Tokens.clear();
        Util::WStrToTokenVector(Tokens, PosStr, L',');
        Vec3 Pos(
            (float)_wtof(Tokens[0].c_str()),
            (float)_wtof(Tokens[1].c_str()),
            (float)_wtof(Tokens[2].c_str())
        );
        //各値がそろったのでオブジェクト作成
        AddGameObject<TilingFixedBox>(Scale, Rot, Pos, 1.0f, 1.0f, TexStr);
    }
}
 まず
    auto BoxNodes = m_XmlDocReader->GetSelectNodes(L"GameStage/GameObjects/TilingFixedBox");
 と、TilingFixedBoxノードを取り出します。XMLを見ればわかる通り、このノードは複数存在します。ですのでGetSelectNodes()関数を使用します。
 こうして取得したノードは複数ですので、スキャンする場合は何らかのループの機能が必要です。ですので
    long CountNode = XmlDocReader::GetLength(BoxNodes);
    for (long i = 0; i < CountNode; i++) {
 のように、取得したノード数をCountNodeに取り出し、その数だけループします。あとは
    for (long i = 0; i < CountNode; i++) {
        auto Node = XmlDocReader::GetItem(BoxNodes, i);
        auto ScaleNode = XmlDocReader::GetSelectSingleNode(Node, L"Scale");
        auto RotNode = XmlDocReader::GetSelectSingleNode(Node, L"Rot");
        auto PosNode = XmlDocReader::GetSelectSingleNode(Node, L"Pos");
        auto TexNode = XmlDocReader::GetSelectSingleNode(Node, L"Tex");
        wstring ScaleStr = XmlDocReader::GetText(ScaleNode);
        wstring RotStr = XmlDocReader::GetText(RotNode);
        wstring PosStr = XmlDocReader::GetText(PosNode);
        wstring TexStr = XmlDocReader::GetText(TexNode);
 のように、サブノード(取得したノードからの相対ノード)を取得して、各データにアクセスします。
 赤くなっているところのように、サブノードの取得はXmlDocReader::GetItem(BoxNodes, i);XmlDocReader::GetSelectSingleNode(Node, L"Scale");のように、スタティック関数を使用します。一度ルートからのノードを取得した後はm_XmlDocReaderインスタンスは必要ありません。
 この後はCSV場合と同様、スケール、回転、移動、テクスチャを取り出しオブジェクトを構築します。この際注意したいのは、
        Tokens.clear();
        Util::WStrToTokenVector(Tokens, RotStr, L',');
 のようにUtil::WStrToTokenVector()関数を呼び出す前にTokens配列をクリアしています。これはTokens配列を使いまわしするために必要な処理です。Util::WStrToTokenVector()関数は、渡された配列に追加するので、クリア処理が必要になります。

 今項ではXMLデータを読み込んで表示するサンプルを紹介しました。前項のCSVから読み込む場合と比べてみてください。
 XMLを使用するメリットは複雑なデータに対処できるということです。例えば、前項のCSVではセルマップデータとして扱う場合と1行1オブジェクトで扱う場合で、ファイルを分けていました。1つのファイルで作成できないことはないですが、処理が複雑になります。
 しかし、XMLを使えば複雑なデータ処理に耐えうるファイルを1つのファイルで作成することも可能です。
 ちなみにBaseCrossでも対応をとっているSpriteStdioのデータXML形式になっています。部位に分かれたキャラクター動的にアニメーションするという複雑な形式なので、XMLを使うのは、実に理に合った構造と思います。