徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】XMLデータを読み込んでみよう。だがしかし争いに巻き込まれる

 UnityでXMLデータを使いたいな。
 だからXMLデータを読み込む処理を作らなくっちゃ。
 でもUnityの機能でXMLデータを読み込むようなものなんてあったけな。
 んー、無かったような気がするなぁ。

 でもC#.Net内の名前空間やクラスの中で、XMLの単語を見たことがあった気がする。
 おっしゃ、じゃあその辺を調べてパパっと組み込んでしまおうか。

 そう……、僕はこの時、軽い気持ちでXMLデータを読み込む処理を作ろうとしたんだ。
 それがまさか、あんな争いに巻き込まれるだなんて……。


この記事にはUnity2017.4.1f1を使用しています。

なぜかXMLを扱うクラスが複数種類ある

f:id:urahimono:20180422221240p:plain

 XMLは別に新しいフォーマットでもない。
 随分前からあるものだ。
 UnityやらC#で既に使っている人も多いことだろう。
 最先端の技術を学ぼうとしてるわけではないので、ネット上を調べればそこら中に情報が転がっているはずだ。
 ググってしらべるか。

 ……。

 うん、案の定いっぱい情報を入手できた。
 えーと何々、XmlDocumentというそのまんまの名前のクラスを使うことでXMLデータを読み込むことが出来そうだね。
 一つのページからだけでは情報が不確かなので、複数のページから情報を確認しておこう。
 んーと、こっちのページではXmlSerializerというクラスを使ってXMLデータを扱っているね。
 ん? こっちのページではXDocumentというクラスを使っているようだ。

 何故XMLを扱うクラスが複数個あるんだ!

  • XmlDocument
  • XDocument
  • XmlSerializer

 どのクラスもXMLデータを扱うという点は一緒なのに、お互いが干渉しあってない。
 各々が独自でXMLを読み込む処理を持っているようだ。
 そもそも名前空間が全部違うしな。
 そもそもXmlDocumentXDocumentなんて、名前が似すぎてて混乱しか招かないだろう!

 なんだこれ、なんかめんどくさそうだな。
 まあ、一つ一つ使ってみてみようか。

 とりあえず使うXMLのデータとして、このブログのRSSデータでも使おうか。

<?xml version="1.0"?>
<rss version="2.0">
  <channel>
    <title>うら干物書き</title>
    <link>http://www.urablog.xyz/</link>
    <description>ゲームを作りたい!</description>
    <lastBuildDate>Sat, 24 Mar 2018 14:04:23 +0900</lastBuildDate>
    <docs>http://blogs.law.harvard.edu/tech/rss</docs>
    <generator>Hatena::Blog</generator>
        <item>
          <title>【Unity】アセット読書会に行ってきたよ。NativeArrayってなんだろう?</title>
          <link>http://www.urablog.xyz/entry/2018/03/24/140423</link>
          <description>&lt;p&gt; 京都で行われるUnity技術者の集いである</description>
          <pubDate>Sat, 24 Mar 2018 14:04:23 +0900</pubDate>
          <guid isPermalink="false">hatenablog://entry/17391345971628867392</guid>
          <category>Unityメモ</category>
          <category>Unity</category>
          <enclosure url="https://cdn-ak.f.st-hatena.com/images/fotolife/u/urahimono/20180324/20180324135415.png" type="image/png" length="0" />
        </item>
        <item>
          <title>【Unity】プロジェクト内のデータをビルド出力時にそのままの形で</title>
          <link>http://www.urablog.xyz/entry/2018/02/18/212849</link>
          <description>&lt;p&gt; Unityで作ったゲームが、外部の実行</description>
          <pubDate>Sun, 18 Feb 2018 21:28:49 +0900</pubDate>
          <guid isPermalink="false">hatenablog://entry/17391345971617600609</guid>
          <category>Unityメモ</category>
          <category>Unity</category>
          <enclosure url="https://cdn-ak.f.st-hatena.com/images/fotolife/u/urahimono/20180218/20180218212318.png" type="image/png" length="0" />
        </item>
        <item>
          <title>【Unity】ゲーム中に常時必要なGameObjectがどのシーンから始めても存在するようにしてみよう</title>
          <link>http://www.urablog.xyz/entry/2018/02/11/164734</link>
          <description>&lt;p&gt; &lt;em&gt;ゲームジャム中の会話にて</description>
          <pubDate>Sun, 11 Feb 2018 16:47:34 +0900</pubDate>
          <guid isPermalink="false">hatenablog://entry/17391345971615406264</guid>
          <category>Unityメモ</category>
          <category>Unity</category>
          <enclosure url="https://cdn-ak.f.st-hatena.com/images/fotolife/u/urahimono/20180211/20180211164205.png" type="image/png" length="0" />
        </item>
  </channel>
</rss>

XmlDocumentを使ってみよう

https://msdn.microsoft.com/ja-jp/library/system.xml.xmldocument.aspx

 えーと、まずはXmlDocumentから見ていこう。
 名前空間はSystem.Xmlか。
 XmlDocumentにXMLデータを渡すと、各タグの情報はXmlElementなどのクラスなって、それがXmlNodeでつながっていると……。
 まあ、サンプルがいろいろと転がっているから、コードを作るのは簡単そうだ。

XMLController.cs

using UnityEngine;

public class XMLController : MonoBehaviour
{
    [SerializeField]
    private TextAsset   m_xmlAsset      = null;
    [SerializeField]
    private string      m_xmlFilePath   = null;


    private void Start()
    {
        if( m_xmlAsset != null )
        {
            LoadFromText( m_xmlAsset.text );
        }

        if( !string.IsNullOrEmpty( m_xmlFilePath ) )
        {
            LoadFromPath( m_xmlFilePath );
        }
    }

    /// <summary>
    /// xmlのテキストデータを読み込む場合。
    /// </summary>
    private void LoadFromText( string i_xmlText )
    {
        try
        {
            var xml = new System.Xml.XmlDocument();
            xml.LoadXml( i_xmlText );

            System.Xml.XmlElement root = xml.DocumentElement;

            // xmlデータをログに表示するよ。
            ShowData( root );

            // 名前を指定して検索する場合は、この関数を使おう!
            var titles = root.GetElementsByTagName( "title" );
            if( titles != null )
            {
                foreach( var title in titles )
                {
                    var titlelement = title as System.Xml.XmlElement;
                    if( titlelement != null )
                    {
                        string nameText     = titlelement.Name;
                        string valueText    = titlelement.FirstChild != null ? titlelement.FirstChild.Value : "";
                        Debug.LogFormat( "name:{0}, value:{1}", nameText, valueText );
                    }
                 }
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }

    /// <summary>
    /// xmlのファイルパスから読み込む場合。
    /// </summary>
    private void LoadFromPath( string i_path )
    {
        try
        {
            var xml = new System.Xml.XmlDocument();
            xml.Load( i_path );

            System.Xml.XmlElement root = xml.DocumentElement;

            // xmlデータをログに表示するよ。
            ShowData( root );

            // 名前を指定して検索する場合は、この関数を使おう!
            var titles = root.GetElementsByTagName( "title" );
            if( titles != null )
            {
                foreach( var title in titles )
                {
                    var titlelement = title as System.Xml.XmlElement;
                    if( titlelement != null )
                    {
                        string nameText = titlelement.Name;
                        string valueText = titlelement.FirstChild != null ? titlelement.FirstChild.Value : "";
                        Debug.LogFormat( "name:{0}, value:{1}", nameText, valueText );
                    }
                }
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }

    private void ShowData( System.Xml.XmlElement i_element )
    {
        string nameText     = i_element.Name;
        string valueText    = i_element.FirstChild != null ? i_element.FirstChild.Value : "";
        string atrText      = string.Empty;

        // アトリビュートがある場合。
        if( i_element.HasAttributes )
        {
            foreach( var attribute in i_element.Attributes )
            {
                var xmlAttribute = attribute as System.Xml.XmlAttribute;
                if( xmlAttribute != null )
                {
                    atrText += string.Format( "\t\t{0}={1}\n", xmlAttribute.Name, xmlAttribute.Value );
                }
            }
        }

        Debug.LogFormat( "name:{0}\n\tvalue:{1}\n\tattribute:\n{2}", nameText, valueText, atrText );

        // 子ノードも探そう。
        foreach( var child in i_element.ChildNodes )
        {
            var childElement = child as System.Xml.XmlElement;
            if( childElement != null )
            {
                ShowData( childElement );
            }
        }
    }

} // class XMLController

f:id:urahimono:20180422221111p:plain

 うん、読み込めたようだね。

XDocumentを使ってみよう

https://msdn.microsoft.com/ja-jp/library/system.xml.linq.xdocument.aspx

 次にXDocumentを見てみよう。
 ……何故こんな似たような名前になってしまったのだろう。
 名前空間はSystem.Xml.Linq
 XDocumentにデータを渡して、各タグの情報はXElementになっていると。
 使い方もXmlDocumentに似てるなぁ。

XMLController.cs

using UnityEngine;

public class XMLController : MonoBehaviour
{
    [SerializeField]
    private TextAsset   m_xmlAsset      = null;
    [SerializeField]
    private string      m_xmlFilePath   = null;


    private void Start()
    {
        if( m_xmlAsset != null )
        {
            LoadFromText( m_xmlAsset.text );
        }

        if( !string.IsNullOrEmpty( m_xmlFilePath ) )
        {
            LoadFromPath( m_xmlFilePath );
        }
    }

    /// <summary>
    /// xmlのテキストデータを読み込む場合。
    /// </summary>
    private void LoadFromText( string i_xmlText )
    {
        try
        {
            System.Xml.Linq.XDocument xml   = System.Xml.Linq.XDocument.Parse( i_xmlText );
            System.Xml.Linq.XElement root   = xml.Root;

            // xmlデータをログに表示するよ。
            ShowData( root );

            // 名前を指定して検索する場合は、この関数を使おう!
            var titles = root.Descendants( "title" );
            if( titles != null )
            {
                foreach( var title in titles )
                {
                    string nameText     = title.Name.LocalName;
                    string valueText    = title.Value;
                    Debug.LogFormat( "name:{0}, value:{1}", nameText, valueText );
                }
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }

    /// <summary>
    /// xmlのファイルパスから読み込む場合。
    /// </summary>
    private void LoadFromPath( string i_path )
    {
        try
        {
            System.Xml.Linq.XDocument xml   = System.Xml.Linq.XDocument.Load( i_path );
            System.Xml.Linq.XElement root   = xml.Root;

            // xmlデータをログに表示するよ。
            ShowData( root );

            // 名前を指定して検索する場合は、この関数を使おう!
            var titles = root.Descendants( "title" );
            if( titles != null )
            {
                foreach( var title in titles )
                {
                    string nameText     = title.Name.LocalName;
                    string valueText    = title.Value;
                    Debug.LogFormat( "name:{0}, value:{1}", nameText, valueText );
                }
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }

    private void ShowData( System.Xml.Linq.XElement i_element )
    {
        string nameText     = i_element.Name.LocalName;
        string valueText    = i_element.Value;
        string atrText      = string.Empty;

        // アトリビュートがある場合。
        if( i_element.HasAttributes )
        {
            foreach( var attribute in i_element.Attributes() )
            {
                atrText += string.Format( "\t\t{0}={1}\n", attribute.Name, attribute.Value );
            }
        }

        Debug.LogFormat( "name:{0}\n\tvalue:{1}\n\tattribute:\n{2}", nameText, valueText, atrText );

        // 子ノードも探そう。
        foreach( var child in i_element.Elements() )
        {
            ShowData( child );
        }
    }

} // class XMLController

f:id:urahimono:20180422221136p:plain

 うん、出来た。
 若干XmlDocumentと使い方が違うところもあったけど概ね同じように作れた。
 キャストする処理が必要ない分、少し楽にXmlDocumentより楽にかけた感はあるかな。

XmlSerializerを使ってみよう

https://msdn.microsoft.com/ja-jp/library/system.xml.serialization.xmlserializer.aspx

 最後にXmlSerializerを使おう。
 これは前述二つのクラスとは使い方が違うね。
 読み込むXMLデータの作りに対応したクラス構成を作成して、そのクラスに変換するといった感じだろうか。
 JsonデータをUnityのJsonUtilityを使って変換するときと同じだね。

 というわけでまずXMLデータに対応するクラスを作ってっと。

XMLData.cs

[System.Xml.Serialization.XmlRoot( "rss" )]
public class XMLData
{
    [System.Xml.Serialization.XmlElement( "channel" )]
    public ChannelData channel;

    public class ChannelData
    {
        [System.Xml.Serialization.XmlElement( "title" )]
        public string title;
        [System.Xml.Serialization.XmlElement( "link" )]
        public string link;
        [System.Xml.Serialization.XmlElement( "description" )]
        public string description;
        [System.Xml.Serialization.XmlElement( "lastBuildDate" )]
        public string lastBuildDate;
        [System.Xml.Serialization.XmlElement( "item" )]
        public ItemData item;
    }


    public class ItemData
    {
        [System.Xml.Serialization.XmlElement( "title" )]
        public string title;
        [System.Xml.Serialization.XmlElement( "link" )]
        public string link;
        [System.Xml.Serialization.XmlElement( "enclosure" )]
        public EnclosureData enclosure;
    }

    public class EnclosureData
    {
        [System.Xml.Serialization.XmlAttribute( "url" )]
        public string url;
        [System.Xml.Serialization.XmlAttribute( "type" )]
        public string type;

    }
}

 JsonUtilityを使う時は、SerializeFieldSystem.Serializableを指定したけど、
 今回の場合は、System.Xml.Serialization.XmlAttributeSystem.Xml.Serialization.XmlElementを指定するみたいだね。

 で、このクラスをXmlSerializerで使うっと。

XMLController.cs

using UnityEngine;

public class XMLController : MonoBehaviour
{
    [SerializeField]
    private TextAsset   m_xmlAsset      = null;
    [SerializeField]
    private string      m_xmlFilePath   = null;


    private void Start()
    {
        if( m_xmlAsset != null )
        {
            LoadFromText( m_xmlAsset.text );
        }

        if( !string.IsNullOrEmpty( m_xmlFilePath ) )
        {
            LoadFromPath( m_xmlFilePath );
        }
    }

    /// <summary>
    /// xmlのテキストデータを読み込む場合。
    /// </summary>
    private void LoadFromText( string i_xmlText )
    {
        try
        {
            using( var stringReader = new System.IO.StringReader( i_xmlText ) )
            {
                var serializer  = new System.Xml.Serialization.XmlSerializer( typeof( XMLData ) );
                XMLData xmlData = (XMLData)serializer.Deserialize( stringReader );
                if( xmlData != null )
                {
                    // クラスに変換後はご自由に。
                }
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }

    /// <summary>
    /// xmlのファイルパスから読み込む場合。
    /// </summary>
    private void LoadFromPath( string i_path )
    {
        try
        {
            using( var fileStream = new System.IO.FileStream( i_path, System.IO.FileMode.Open ) )
            {
                var serializer  = new System.Xml.Serialization.XmlSerializer( typeof( XMLData ) );
                XMLData xmlData = (XMLData)serializer.Deserialize( fileStream );
                if( xmlData != null )
                {
                    // クラスに変換後はご自由に。
                }
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }

} // class XMLController

 出来た出来た。
 Jsonの変換にJsonUtilityを使っている身としては、これが一番わかりやすいかな。

XmlDocumentとXDocumentと争いの火種

 調べ終わったけど、妙なのはXmlDocumentXDocumentの関係だ。
 ほぼほぼ一緒の気がするんだけど。
 何がどう違うのかって、使ってみたけど説明がしにくい。

 そしてどうやら、この辺りは争いの火種になっているようで……。

LINQ to XML vs. DOM (C#)
LINQ to XML vs. DOM | Microsoft Docs

 うわぁ……、コード上の争いはめんどくさいんだよ。
 どっちも譲ってくんないからなぁ。
 この手の争いには極力関わらないようにしたほうがいい気が……。

 まあ、もう少し見てみましょうか。

ToString()の違い

 ToString()を実装しておいてもらえると、デバッグログとか出すときにそのままクラスを渡せばいいだけなので楽で助かります。
 その辺どうなっているのでしょう。

XMLController.cs

using UnityEngine;

public class XMLController : MonoBehaviour
{
    [SerializeField]
    private TextAsset   m_xmlAsset      = null;


    private void Start()
    {
        if( m_xmlAsset != null )
        {
            UseXmlDocument( m_xmlAsset.text );
            UseXDocument( m_xmlAsset.text );
        }
    }

    private void UseXmlDocument( string i_xmlText )
    {
        try
        {
            var xml = new System.Xml.XmlDocument();
            xml.LoadXml( i_xmlText );

            System.Xml.XmlElement root = xml.DocumentElement;

            // 先頭の"title"のタグの情報のみをToStringを使って結果をみてみよう。
            var titles = root.GetElementsByTagName( "title" );
            foreach( var title in titles )
            {
                var titlelement = title as System.Xml.XmlElement;
                Debug.LogFormat( "XmlDocument:{0}", titlelement );
                break;
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }

    private void UseXDocument( string i_xmlText )
    {
        try
        {
            System.Xml.Linq.XDocument xml = System.Xml.Linq.XDocument.Parse( i_xmlText );
            System.Xml.Linq.XElement root = xml.Root;

            // 先頭の"title"のタグの情報のみをToStringを使って結果をみてみよう。
            var titles = root.Descendants( System.Xml.Linq.XName.Get( "title" ) );
            foreach( var title in titles )
            {
                Debug.LogFormat( "XDocument:{0}", title );
                break;
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }
    
} // class XMLController

f:id:urahimono:20180422221150p:plain

 うん、XmlDocumentは対応していないのでクラス名がそのまま出力されてるけど、XDocumentは対応しており、プロパティの情報が表示されている。
 やるではないか。
 まあ、MSDNを見る限りXDocumentの方が新しいものっぽいからなぁ。

Linqの違い

 C#を使うなら、Linqは使いたいですよね。
 その辺はどうでしょう。

XMLController.cs

using UnityEngine;
using System.Linq;
public class XMLController : MonoBehaviour
{
    [SerializeField]
    private TextAsset   m_xmlAsset      = null;


    private void Start()
    {
        if( m_xmlAsset != null )
        {
            UseXmlDocument( m_xmlAsset.text );
            UseXDocument( m_xmlAsset.text );
        }
    }

    private void UseXmlDocument( string i_xmlText )
    {
        try
        {
            var xml = new System.Xml.XmlDocument();
            xml.LoadXml( i_xmlText );

            System.Xml.XmlElement root = xml.DocumentElement;


            // エラー
            // XmlNodeListに対してLinqは使えない。
            var titles = root.ChildNodes.FirstOrDefault( value => value.Name == "item" );
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }

    private void UseXDocument( string i_xmlText )
    {
        try
        {
            System.Xml.Linq.XDocument xml = System.Xml.Linq.XDocument.Parse( i_xmlText );
            System.Xml.Linq.XElement root = xml.Root;


            // IEnumerable<XElement>で返ってくるため、Linqが使える。
            // 以下の場合は、"item"タグ内にある"title"のみを検索している。
            var titles = root.Descendants().Where( value => value.Name.LocalName == "item" ).Select( value => value.Element( "title" ) );
            foreach( var title in titles )
            {
                Debug.LogFormat( "XDocument:{0}", title );
            }
        }
        catch( System.Exception i_exception )
        {
            Debug.LogErrorFormat( "うーむ、このXML情報は読み込めなかったらしい。エラーの詳細を添付しておくよ。{0}", i_exception );
        }
    }
    
} // class XMLController

 うん、まあそうなるよね。
 そりゃ、XDocumentは名前空間的にLinqは使えるだろうね。
 多分だけど、XmlDocumentLinqで使えるようにしたのがXDocumentだろうね、恐らく。

そしてXMLへ

 まあ好きなものを使ってXMLを読めばいいんじゃないかな。

 えっ、僕は何派に入るんだって?
 いや、別にどの派閥に入るとかないんですが。
 そもそも僕がこのXML読み込み関連に関わったのは今回が初めてだしね。
 今来たばっかだから、このXML論争の歴史なんてさっぱりわからんわけですよ。

 その割にはXDocumentを擁護する文面が多いんじゃないかって?
 いや、それはたぶん気のせいだと思いますよ。
 別にXmlDocumentが使えないとか言ってるわけではないので。

 ……案の定めんどくさいことになった。
 僕はこの手の争い苦手なのに……。

 よし、じゃあこの際だ。
 この決着はバブルサッカーでつけようじゃないか。
 えっ、バブルサッカー知らない?
 透明のバランスボールみたいなのを装着して、サッカーするアレだよ。
 あれなら思いっきりぶつかっても、ケガとかしにくいから安全だと思うよ。
 えっ、僕はやんないのかって?
 うん、僕は腰痛持ちだから遠慮しておくよ。
 その代わりに審判をやるから心配しないで。
 それじゃ、キックオフ!