用于 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制作对话系统编辑器)
Unity3d数据存储 PlayerPrefs,XML,Json数据的存储与解析