用于 Unity 对话系统目的的 XML 解析具有属性的嵌套对象

Posted

技术标签:

【中文标题】用于 Unity 对话系统目的的 XML 解析具有属性的嵌套对象【英文标题】:XML parsing nested objects with attributes for Unity dialogues system purpose 【发布时间】:2021-09-06 12:40:47 【问题描述】:

我正在尝试从如下所示的 XML 文档开始构建一个在 Unity 中使用的基本对话系统:

<?xml version="1.0" encoding="utf-8"?>
    
    <speech id="1">
      
        <bubble id="1" isQuestion="no">
          Hi!
        </bubble>
    
      <bubble id="2" isQuestion="no">
         It's been a while!
        </bubble>
    
      <bubble id="3" isQuestion="no">
         Have a look at my wares!
        </bubble>
    
      <bubble id="4" isQuestion="yes">
          Do you want to trade?
          <option id="1"> true </option>
          <option id="2"> false </option>
        </bubble>
      
      <bubble id="5" isQuestion="no">
        Goodbye!
      </bubble>
      
    </speech>
    <speech id="2">
     ...
   </speech>

这里的概念是将每一行存储在一个“气泡”节点中,并使用 id 属性来定位语音中的节点,并使用一个布尔变量来了解气泡是否提出问题。

读到我尝试过这样的事情:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Xml;

public class DialogueManager 

  
    public List<Speech> LoadSpeechs(XmlDocument doc)
    
        doc.Load();
        List<Bouble> Boubles = new List<Bouble>();
        List<Bouble> Speechs = new List<Speech>();

        foreach (xmlNode node in doc.DocumentElement)
        
            int id = node.Attributes[0].Value;
            foreach (xmlNode node in doc.DocumentElement)
            
                int id = node.Attributes[0].Value;
                bool isQuestion = node.Attributes[1].Value;
                string content = string.Parse(node["bouble"].InnerText);

                bouble = new Bouble(id, isQuestion, value);
                Boubles.Add(bouble);
            
            speech = new Speech(id, Boubles);
            Speechs.Add(speech);
        
        return Speechs;
    

    public class Speech
    
        public int SpeechID  get; set; 

        public Speech(int m_speechID, List<Bouble> bobules)
        
            SpeechID = m_speechID;
            Boubles = bobules;
        
    

    public class Bouble
    
        public bool IsQuesion  get; set; 
        public int NodeID  get; set; 
        public string Content  get; set; 

        public Bouble(int m_nodeID, bool m_isQuestion, string m_value)
        
            NodeID = m_nodeID;
            IsQuesion = m_isQuestion;
            Content = m_value;
        
    

问题是我得到了大量的错误,单独很难理解。 所以我在这里。我应该提一下,我并没有试图实现一些特别的东西,只是学习范式和 Unity,所以我更喜欢一些评论和解释,说明它是如何工作的以及我错在哪里而不是其他方式,但欢迎所有回复: )

我正计划添加更多属性,但现在我想弄清楚如何使其正常工作。

【问题讨论】:

【参考方案1】:

根据我的测试,我发现以下问题,您可以进行一些更改。

首先,当我们要读取 xml 文件时,我们应该有 root 节点。 这是我测试的 xml:

第二,如果我们想将一种类型转换为另一种类型,我们需要使用类型转换。 如以下代码:

  int sid = Convert.ToInt32(node.Attributes[0].Value);
  string content = Lnode.InnerText;(InnerText will return a string type, there is no need to convert again)

第三,我们可以使用下面的代码将“yes”或“no”转换成bool

 text = Lnode.Attributes[1].Value.ToString();
 if(text=="yes")
 
  isquestion = true;
 
 else
 
 isquestion = false;
 

第四,我们需要将Speechs.Add(speech);放到内循环之外。

最后,您可以参考以下完整的代码示例将xml转换为列表。

代码:

 public class DialogueManager
    

        public List<Speech> LoadSpeechs(XmlDocument doc)
        
            doc.Load("test.xml");
            List<Bouble> Boubles = new List<Bouble>();
            List<Speech> Speechs = new List<Speech>();
            string text = string.Empty;
            bool isquestion = false;
            Speech speech = null;
            foreach (XmlNode node in doc.DocumentElement)
            
                if (node.Name == "speech")
                
                    int sid = Convert.ToInt32(node.Attributes[0].Value);
                    foreach (XmlNode Lnode in node.ChildNodes)
                    
                        int bid = Convert.ToInt32(Lnode.Attributes[0].Value);
                        text = Lnode.Attributes[1].Value.ToString();
                        if(text=="yes")
                        
                            isquestion = true;
                        
                        else
                        
                            isquestion = false;
                        
                        string content = Lnode.InnerText;

                        Bouble bouble = new Bouble(bid, isquestion, content);
                        Boubles.Add(bouble);
                    
                    speech = new Speech(sid, Boubles);

                
           
                Speechs.Add(speech);

            
            return Speechs;
        

        public class Speech
        
            public int SpeechID  get; set; 

            public List<Bouble> Boubles  get; set; 

            public Speech(int m_speechID, List<Bouble> bobules)
            
                SpeechID = m_speechID;
                Boubles = bobules;
            
        

        public class Bouble
        
            public bool IsQuesion  get; set; 
            public int NodeID  get; set; 
            public string Content  get; set; 

            public Bouble(int m_nodeID, bool m_isQuestion, string m_value)
            
                NodeID = m_nodeID;
                IsQuesion = m_isQuestion;
                Content = m_value;
            
        
    
    

测试代码和结果:(我在控制台应用中测试过)

static void Main(string[] args)
        
            DialogueManager manager = new DialogueManager();
            XmlDocument doc = new XmlDocument();
            var list=manager.LoadSpeechs(doc);
            foreach (DialogueManager.Speech speech in list)
            
                Console.WriteLine(speech.SpeechID);
                foreach (DialogueManager.Bouble bouble in speech.Boubles)
                
                    Console.WriteLine(bouble.NodeID);
                    Console.WriteLine(bouble.IsQuesion);
                    Console.WriteLine(bouble.Content);

                
            

            Console.ReadKey();
        

【讨论】:

好的,尝试了这个,通过一些小的修复,我能够完成使其适合我的系统。非常感谢! @Phazertron,很高兴听到您的问题已经解决,如果您不介意可以点击“✔”将我的回复标记为答案。它还将帮助其他人解决类似的问题。【参考方案2】:

首先,您的 XML 文件存在一个问题:您需要一个 根元素,格式类似于例如

<?xml version="1.0"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <speech>
    ...
  </speech>
  <speech>
    ...
  </speech>
</Root>

那么您可以/应该使用XMLSerializer,而不是“手动”解析这一切。

这允许您完全灵活地添加更多元素和属性,而无需一直更改解析器方法。你宁愿已经通过attributes直接在类定义中定义了整个XML方案结构@

并做类似的事情,例如

// In general make your classes Serializable and use fields instead of properties
// This way you can also see them in the Unity Inspector
// See https://docs.unity3d.com/Manual/script-Serialization.html
[Serializable]
// That generates/interprets the general XML root element
[XmlRoot]
public class Root

    // By matching this element name with the root name of the Speech class (see below)
    // This is treated as array without the need for an additional wrapper tag
    [XmlElement(ElementName = "speech")] public List<Speech> speeches = new List<Speech>();


[Serializable]
// By using the same ElementName as root for this
// It will not read/write an additional array wrapper tag but rather direct children of the Root
[XmlRoot(ElementName = "speech")]
public class Speech

    // Makes this an attribute without the speech tag
    // NOTE: I think you wouldn't really need these
    //       They are elements in an array so you could rely on the index itself
    [XmlAttribute] public int id;

    [XmlElement("bubble")] public List<Bubble> bubbles = new List<Bubble>();


[Serializable]
[XmlRoot(ElementName = "bubble")]
public class Bubble

    [XmlAttribute] public int id;
    
    // This treats the label as the actual text between the tags
    [XmlText] public string label;

    // NOTE: Here I thought your bool is quite redundant, you don't need it
    // simply check if there are options elements -> if so it is automatically a question
    // if there are no options anyway -> there is no question
    public bool isQuestion => options.Count != 0;

    // If there are none in your XML file the list will simply stay empty
    [XmlElement(ElementName = "option")] public List<Option> options = new List<Option>();


[Serializable]
[XmlRoot(ElementName = "option")]
public class Option

    [XmlAttribute] public int id;

    [XmlText] public string label;

    // Optionally you could use some return value like a bool or enum
    // but again, you can also simply go by index

这需要您的 XML 文件稍作更改,并且看起来像例如

<?xml version="1.0"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <speech id="1">
    <bubble id="1">Hallo</bubble>
    
    <bubble id="2">World?
      <option id="1">Yes</option>
      <option id="2">Nope</option>
    </bubble>
  </speech>
  
  <speech id="2">
    <bubble id="1">Hi</bubble>
    <bubble id="2">There!</bubble>
  </speech>
</Root>

最后你可以简单地使用两种方法,例如

public class Example : MonoBehaviour

    [Header("Input")]
    [Tooltip("The FULL path to your file - for the example below I cheated ;) ")] 
    public string xmlFileUrl = Path.Combine(Application.streamingAssetsPath, "Example.xml");

    [Header("Result")] 
    [Tooltip("The deserialized c# classes")]
    public Root root;

    // This allows you to call this method via the Inspector Context Menu
    [ContextMenu(nameof(LoadFile))]
    public void LoadFile()
    
        // Open the file as a stream
        using (var stream = File.Open(xmlFileUrl, FileMode.Open, FileAccess.Read, FileShare.Read))
        
            // create an XMLSerializer according to the root type
            var serializer = new XmlSerializer(typeof(Root));

            // Deserialize the file according to your implemented Root class structure
            root = (Root) serializer.Deserialize(stream);
        
    

    [ContextMenu(nameof(WriteFile))]
    public void WriteFile()
    
        // Delete the existing file
        if (File.Exists(xmlFileUrl)) File.Delete(xmlFileUrl);

        // create the StreamingAsset folder if not exists
        // NOTE: Later in your app you want to use the StreamingAssets only 
        //       if your file shall be read-only!
        //       otherwise use persistentDataPath
        if(!Directory.Exists(Application.streamingAssetsPath)) Directory.CreateDirectory(Application.streamingAssetsPath);

        // Create a new file as stream
        using (var stream = File.Open(xmlFileUrl, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
        
            var serializer = new XmlSerializer(typeof(Root));

            // serialize the current Root class into the XML file
            serializer.Serialize(stream, root);
        

#if UNITY_EDITOR 
        // in the editor refresh the AssetDataBase of Unity 
        // so you see the added files in the Project View
        UnityEditor.AssetDatabase.Refresh();
#endif
    

现在您可以直接访问root 及其所有属性,例如

var isQuestion = root.speeches[0].bubbles[1].isQuestion;

您也可以直接通过 Unity 中的 Inspector 进行整个准备和编辑。


然后作为最后一点个人风格,我宁愿使用

[Serializable]
[XmlRoot]
public class Root

    [XmlElement(ElementName = nameof(Speech))] public List<Speech> speeches = new List<Speech>();


[Serializable]
[XmlRoot(ElementName = nameof(Speech))]
public class Speech

    ...

    [XmlElement(nameof(Bubble))] public List<Bubble> bubbles = new List<Bubble>();


[Serializable]
[XmlRoot(ElementName = nameof(Bubble))]
public class Bubble

    ...
    
    [XmlElement(ElementName = nameof(Option))] public List<Option> options = new List<Option>();


[Serializable]
[XmlRoot(ElementName = nameof(Option))]
public class Option

    ...

在 XML 中使用大写的类名

<?xml version="1.0"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Speech id="1">
    <Bubble id="1">Hallo</Bubble>
    
    <Bubble id="2">World?
      <Option id="1">Yes</Option>
      <Option id="2">Nope</Option>
    </Bubble>
  </Speech>
  
  <Speech id="2">
    <Bubble id="1">Hi</Bubble>
    <Bubble id="2">There!</Bubble>
  </Speech>
</Root>

【讨论】:

给我一些时间来研究这个,因为你正在使用我还不知道的属性。一旦我明白了这一点,我会告诉你的。到目前为止,非常感谢!

以上是关于用于 Unity 对话系统目的的 XML 解析具有属性的嵌套对象的主要内容,如果未能解决你的问题,请参考以下文章

Unity编辑器拓展(GraphView制作对话系统编辑器)

Unity中用Mono插件解析xml文件

在 Oracle SQL 中解析具有未知名称空间的 XML

Unity3d数据存储 PlayerPrefs,XML,Json数据的存储与解析

解决sentinel key not found (h0007) Unity对话框

解决sentinel key not found (h0007) Unity对话框