Как прочитать данные из XML?
Сделал сохранение некоторых объектов сцены Unity в XML файл для того, чтобы в любой момент загрузить это состояние и продолжить игру. Сохранение в файл проходит без проблем, но вот с чтением всё плохо - код читает XML до первого конечного объекта в структуре, после чего завершает чтение, хотя это менее 1% от общего файла. Вот примера файла без компонентов и т.п.:
<?xml version="1.0"?>
<ArrayOfSerializableObject xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SerializableObject>
<Name>1</Name>
<Tag></Tag>
<Position></Position>
<Rotation></Rotation>
<Scale></Scale>
<Children>
<SerializableObject>
<Name>2</Name>
<Tag></Tag>
<Position></Position>
<Rotation></Rotation>
<Scale></Scale>
<Children>
<SerializableObject>
<Name>3</Name>
<Tag></Tag>
<Position></Position>
<Rotation></Rotation>
<Scale></Scale>
<Children>
<SerializableObject>
<Name>4</Name>
<Tag></Tag>
<Position></Position>
<Rotation></Rotation>
<Scale></Scale>
<Children />
<Components></Components>
</Children>
<Components></Components>
</SerializableObject>
<SerializableObject>
<Name>5</Name>
<Tag></Tag>
<Position></Position>
<Rotation></Rotation>
<Scale></Scale>
<Children />
<Components></Components>
</SerializableObject>
</Children>
<Components></Components>
</SerializableObject>
</Children>
<Components />
</SerializableObject>
</ArrayOfSerializableObject>
В итоге я получу объекты в такой структуре:
-1
--2
---3
----4
А 5 и далее уже читаться не будут, так как код якобы дошел до конца файла.
И вот, собственно, сам код сохранения и чтения (ну или, правильнее сказать, его огрызок):
using System.Collections.Generic;
using System;
using System.IO;
using System.Xml.Serialization;
using UnityEngine;
using System.Collections;
[Serializable]
public class SerializableObject
{
public string Name;
public string Tag;
public Vector3 Position;
public Quaternion Rotation;
public Vector3 Scale;
public List<SerializableObject> Children = new List<SerializableObject>();
public List<SerializableComponent> Components = new List<SerializableComponent>();
}
[Serializable]
public class SerializableComponent
{
public string TypeName;
public SerializableDictionary Properties = new SerializableDictionary();
}
[Serializable]
public class SerializableDictionary : IXmlSerializable
{
public Dictionary<string, string> Dictionary = new Dictionary<string, string>();
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(System.Xml.XmlReader reader)
{
reader.Read();
while(reader.NodeType != System.Xml.XmlNodeType.EndElement)
{
string key = reader.GetAttribute("Key");
string value = reader.GetAttribute("Value");
Dictionary.Add(key, value);
reader.Read();
}
reader.ReadEndElement();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
foreach(var kvp in Dictionary)
{
writer.WriteStartElement("Item");
writer.WriteAttributeString("Key", kvp.Key);
writer.WriteAttributeString("Value", kvp.Value);
writer.WriteEndElement();
}
}
}
public class SaveLoadManager : MonoBehaviour
{
public Transform rootObject;
public string path, sceneName;
private List<SerializableObject> loadedData;
void Awake()
{
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX
path = System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath, "..", "..", "Files", "Saves", sceneName, "savefile.xml"));
#else
string documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
path = System.IO.Path.GetFullPath(System.IO.Path.Combine(documentsPath, "MTS", "Files", "Saves", sceneName, "savefile.xml"));
#endif
}
// Сохранение
public void SaveToXML()
{
List<SerializableObject> data = new List<SerializableObject>();
foreach(Transform child in rootObject)
{
data.Add(SaveObject(child));
}
XmlSerializer serializer = new XmlSerializer(typeof(List<SerializableObject>));
using(FileStream stream = new FileStream(path, FileMode.Create))
{
serializer.Serialize(stream, data);
}
}
private SerializableObject SaveObject(Transform obj)
{
SerializableObject serializableObject = new SerializableObject
{
Name = obj.name,
Tag = obj.tag,
Position = obj.localPosition,
Rotation = obj.localRotation,
Scale = obj.localScale
};
foreach(Component component in obj.GetComponents<Component>())
{
if(component.GetType() != typeof(Transform))
{
serializableObject.Components.Add(SaveComponent(component));
}
}
foreach(Transform child in obj)
{
serializableObject.Children.Add(SaveObject(child));
}
return serializableObject;
}
private SerializableComponent SaveComponent(Component component)
{
SerializableComponent serializableComponent = new SerializableComponent
{
TypeName = component.GetType().AssemblyQualifiedName
};
foreach(var field in component.GetType().GetFields())
{
if(field.IsPublic)
{
serializableComponent.Properties.Dictionary[field.Name] = field.GetValue(component)?.ToString();
}
}
return serializableComponent;
}
// Загрузка
public void LoadFromXML()
{
XmlSerializer serializer = new XmlSerializer(typeof(List<SerializableObject>));
using(FileStream stream = new FileStream(path, FileMode.Open))
{
loadedData = (List<SerializableObject>)serializer.Deserialize(stream);
}
foreach(Transform child in rootObject)
{
Destroy(child.gameObject);
}
foreach(var data in loadedData)
{
LoadObject(data, rootObject);
}
}
private void LoadObject(SerializableObject data, Transform parent)
{
GameObject obj = new GameObject(data.Name);
obj.tag = data.Tag;
obj.transform.SetParent(parent);
obj.transform.localPosition = data.Position;
obj.transform.localRotation = data.Rotation;
obj.transform.localScale = data.Scale;
foreach(var childData in data.Children)
{
LoadObject(childData, obj.transform);
}
}
}
Ответы (2 шт):
Проверьте как, производится загрузка.
// Загрузка
public void LoadFromXML()
{
XmlSerializer serializer = new XmlSerializer(typeof(List<SerializableObject>));
using(FileStream stream = new FileStream(path, FileMode.Open))
{
try
{
// Десериализация данных
loadedData = (List<SerializableObject>)serializer.Deserialize(stream);
// Выводим в консоль количество загруженных объектов
Debug.Log("Количество загруженных объектов: " + loadedData.Count);
}
catch (InvalidOperationException ex)
{
Debug.LogError("Ошибка десериализации: " + ex.Message);
return; // Выходим из метода, если произошла ошибка
}
}
foreach(Transform child in rootObject)
{
Destroy(child.gameObject);
}
foreach(var data in loadedData)
{
LoadObject(data, rootObject);
}
}
UPD:
Проверила структуру вложенности полного содержимого файла.xml
from lxml import etree
from rich import print, inspect
def parseXML(xmlFile):
"""
Парсинг XML и преобразование в список словарей
"""
with open(xmlFile) as fobj:
xml = fobj.read()
root = etree.fromstring(xml)
def xml_to_dict(element):
"""
Рекурсивная функция для преобразования XML-элемента в словарь
"""
result = {}
for child in element:
if len(child) > 0: # Если у элемента есть дочерние элементы
child_data = xml_to_dict(child)
if child.tag in result:
# Если тег уже существует, добавляем в список
if isinstance(result[child.tag], list):
result[child.tag].append(child_data)
else:
result[child.tag] = [result[child.tag], child_data]
else:
result[child.tag] = child_data
else:
result[child.tag] = child.text if child.text else "None"
return result
xml_list = []
for obj in root:
xml_list.append(xml_to_dict(obj))
xml_dict = xml_to_dict(root)
return xml_list, xml_dict
if __name__ == "__main__":
xml_list, xml_dict = parseXML(r"C:\KWORK\C-sharp\ssd.xml")
print(len(xml_list[0]))
root = len(xml_dict)
children = list(map(len, xml_dict.values()))
nested_elements = map(len, *xml_dict.values())
print(
{
root
:
{c: n for c, n in zip(range(1, children[0]+1), nested_elements)}
}
)
output:
(.venv) PS C:\KWORK> & c:/KWORK/C-sharp/dump_load_xml.py
7
{1: {1: 4, 2: 3, 3: 8, 4: 8, 5: 5, 6: 8, 7: 10}}
(.venv) PS C:\KWORK>
Суть работы старого когда заключалась в том, что он сохранял все дочерние объекты указанного объекта со всеми компонентами, значениями компонентов и т.д., из-за чего средний файл с сохранением мог весить около 25-30 МБ, что довольно многовато для XML (хотя не мне судить). Но, так как в игре все сохраняемые объекты уже имеют свои префабы, кроме того, игроки и спавнят все эти объекты из префабов, я решил сделать сохранение только базовых параметров, таких как название объекта, его положение, маршрут и т.п.
А для загрузки решил использовать как раз тот самый спавнер этих объектов: сначала загружаю все объекты из XML в отдельный List<>
, а затем из этого списка вызываю проверку на соответствие имени объекта с тем, что есть в спавнере, и, если всё корректно, то объект спавнится внутри указанного объекта (как раз того, с которого и сохранялось) и переносится в нужное положение с заданием нужно маршрута и прочих деталей.
Вот такой код в итоге получился:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
using UnityEngine;
using UnityEngine.UI;
[System.Serializable]
public class SaveData
{
public List<ObjectData> objects = new List<ObjectData>();
}
[System.Serializable]
public class ObjectData
{
public string name;
public Vector3 position;
public Quaternion rotation;
public string pathToPathObject;
public int currentWaypoint;
}
public class SaveLoadManager : MonoBehaviour
{
public GameObject parentObject; //Отсюда берутся все сохранённые троллейбусы и сюда же они загружаются
public SpawnerRemover spawnerRemover; // Спавнер, который содержит в себе префабы для спавна троллейбусов
public InMapSpeedometers speedometers;
public PauseSoundManager pauseSoundManager;
public List<ObjectData> loadedObjects = new List<ObjectData>();
private string filePath;
public string mapName = "Default"; // Имя карты, так как имя сцены выглядит некрасиво, а на некоторых сценах карта одна и та же и сохранения будут севместимы между сценами
public InputField inputField;
public Dropdown dropdown;
// Инициализация путей сохранения и поиск уже имеющихся сохранений
public void InitializeSaveSystem()
{
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX
filePath = System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath, "..", "..", "Files", "Saves", mapName));
#elif UNITY_ANDROID
filePath = System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.persistentDataPath, "Saves", mapName));
#else
filePath = System.IO.Path.Combine(Application.persistentDataPath, "Saves", mapName);
#endif
dropdown.ClearOptions();
if(Directory.Exists(filePath))
{
string[] textureFiles = Directory.GetFiles(filePath, "*.xml");
foreach(string textureFile in textureFiles)
{
string savename = System.IO.Path.GetFileNameWithoutExtension(textureFile);
Dropdown.OptionData optionData = new Dropdown.OptionData(savename);
dropdown.options.Add(optionData);
}
}
}
// Сохранение
public void Save()
{
if(inputField.text == "") return;
SaveData saveData = new SaveData();
FindAndSaveTrolleybuses(parentObject.transform, saveData);
string saveFilePath = System.IO.Path.Combine(filePath, $"{inputField.text}.xml");
XmlSerializer serializer = new XmlSerializer(typeof(SaveData));
using(FileStream stream = new FileStream(saveFilePath, FileMode.Create))
{
serializer.Serialize(stream, saveData);
}
InitializeSaveSystem();
}
private void FindAndSaveTrolleybuses(Transform parent, SaveData saveData)
{
foreach(Transform child in parent)
{
Trolleybus trolleybus = child.gameObject.GetComponent<Trolleybus>();
Bot bot = child.gameObject.GetComponent<Bot>();
PlayerAISwitcher switcher = child.gameObject.GetComponent<PlayerAISwitcher>();
if(trolleybus != null && bot != null && switcher != null)
{
ObjectData data = new ObjectData
{
name = switcher.thisTrollPrefab.gameObject.name,
position = child.position,
rotation = child.rotation,
pathToPathObject = bot.path != null ? GetFullPath(bot.path.transform) : null,
currentWaypoint = bot.path != null ? switcher.wp : -1
};
saveData.objects.Add(data);
}
if(child.childCount > 0)
{
FindAndSaveTrolleybuses(child, saveData);
}
}
}
// Лучше этой реализации, думаю, не найти, но это не точно...
private string GetFullPath(Transform obj)
{
string path = obj.name;
while(obj.parent != null)
{
obj = obj.parent;
path = obj.name + "/" + path;
}
return path;
}
// Загрузка
public void Load()
{
string saveFilePath = System.IO.Path.Combine(filePath, $"{dropdown.options[dropdown.value].text}.xml");
if(File.Exists(saveFilePath))
{
XmlSerializer serializer = new XmlSerializer(typeof(SaveData));
using(FileStream stream = new FileStream(saveFilePath, FileMode.Open))
{
SaveData saveData = (SaveData)serializer.Deserialize(stream);
loadedObjects = saveData.objects;
}
SpawnTrolleybus();
pauseSoundManager.ReloadList();
}
else
{
Debug.LogError("Файл сохранения не найден! Путь к файлу: " + filePath);
}
}
private GameObject prefabToSpawn, spawnedObject;
// Спавн загруженных троллейбусов
private void SpawnTrolleybus()
{
Switcher switcher = parentObject.GetComponent<Switcher>();
PlayerAISwitcher[] trollsToRemove = switcher.trolls;
// Сначала удаляем все имеющиеся на сцене троллейбусы
for(int i = switcher.trolls.Length - 1; i >= 0; i--)
{
if(switcher.trolls[i] != null)
{
switcher.trolls = switcher.trolls.Where(e => e != switcher.trolls[i]).ToArray();
Destroy(trollsToRemove[i].thisTrollPrefab);
}
}
// А теперь спавним загруженные троллейбусы
foreach(ObjectData obj in loadedObjects)
{
spawnedObject = null;
prefabToSpawn = null;
foreach(TrolleybusProperties properties in spawnerRemover.trolleybusProperties)
{
if(properties.trollPrefab.gameObject.name == obj.name)
{
prefabToSpawn = properties.trollPrefab;
break;
}
}
spawnedObject = Instantiate(prefabToSpawn, parentObject.transform);
spawnedObject.transform.position = obj.position;
spawnedObject.transform.rotation = obj.rotation;
spawnedObject.transform.localScale = Vector3.one;
spawnedObject.name = prefabToSpawn.name;
// Если у родительского объекта в префабе нет нужных компонентов, ищем их в его дочерних объектах
if(spawnedObject.GetComponent<PlayerAISwitcher>() == null)
{
spawnedObject.GetComponentInChildren<StartWP>().WP = obj.currentWaypoint;
if(obj.pathToPathObject != null) spawnedObject.GetComponentInChildren<Bot>().path = GameObject.Find(obj.pathToPathObject).GetComponent<Path>();
spawnedObject.GetComponentInChildren<Trolleybus>().StartRoute();
spawnedObject.GetComponentInChildren<Speedometer>().speedometers = speedometers;
}
else
{
spawnedObject.GetComponent<StartWP>().WP = obj.currentWaypoint;
if(obj.pathToPathObject != null) spawnedObject.GetComponent<Bot>().path = GameObject.Find(obj.pathToPathObject).GetComponent<Path>();
spawnedObject.GetComponent<Trolleybus>().StartRoute();
spawnedObject.GetComponent<Speedometer>().speedometers = speedometers;
}
}
// А теперь нужно подождать конца кадра, иначе все удалённые из массива 'switcher.trolls' троллейбусы вернутся
StartCoroutine(WaitAndClearList(switcher));
}
private IEnumerator WaitAndClearList(Switcher switcher)
{
yield return new WaitForEndOfFrame();
switcher.currentTroll = 0;
switcher.UpdateTrollsList();
switcher.Switch();
}
}
И в итоге средний XML-файл сохранения выглядит так, и весит считанные килобайты (вот это я понимаю оптимизация):
<?xml version="1.0"?>
<SaveData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<objects>
<ObjectData>
<name>682B</name>
<position>
<x>-39.45192</x>
<y>-1.60337508</y>
<z>53.69084</z>
</position>
<rotation>
<x>8.849667E-05</x>
<y>-0.0167291034</y>
<z>-1.74560489E-06</z>
<w>0.9998601</w>
<eulerAngles>
<x>0.0101362066</x>
<y>358.0829</y>
<z>-0.000369652931</z>
</eulerAngles>
</rotation>
<pathToPathObject>Game/Routes/1</pathToPathObject>
<currentWaypoint>104</currentWaypoint>
</ObjectData>
<ObjectData>
<name>T1</name>
<position>
<x>-177.5658</x>
<y>-1.412122</y>
<z>322.0922</z>
</position>
<rotation>
<x>0.0008149497</x>
<y>-0.00769202</y>
<z>-1.33202675E-05</z>
<w>0.999970138</w>
<eulerAngles>
<x>0.09337187</x>
<y>359.118561</y>
<z>-0.002244677</z>
</eulerAngles>
</rotation>
<pathToPathObject>Game/Routes/2</pathToPathObject>
<currentWaypoint>52</currentWaypoint>
</ObjectData>
<ObjectData>
<name>5265</name>
<position>
<x>-176.4925</x>
<y>-1.53232145</y>
<z>277.225</z>
</position>
<rotation>
<x>-5.476603E-06</x>
<y>0.999998331</y>
<z>-8.621062E-05</z>
<w>-0.00183899445</w>
<eulerAngles>
<x>0.009880146</x>
<y>180.210739</y>
<z>-0.000609404</z>
</eulerAngles>
</rotation>
<pathToPathObject>Game/Routes/3</pathToPathObject>
<currentWaypoint>0</currentWaypoint>
</ObjectData>
<ObjectData>
<name>280T</name>
<position>
<x>76.53992</x>
<y>-1.51780808</y>
<z>340.023224</z>
</position>
<rotation>
<x>-0.0008974281</x>
<y>-0.697024</y>
<z>0.0009232392</z>
<w>-0.717046738</w>
<eulerAngles>
<x>0.147481531</x>
<y>88.3775253</y>
<z>-0.00417994242</z>
</eulerAngles>
</rotation>
<pathToPathObject>Game/Routes/4</pathToPathObject>
<currentWaypoint>52</currentWaypoint>
</ObjectData>
<ObjectData>
<name>5265</name>
<position>
<x>399.226</x>
<y>-1.53230739</y>
<z>336.4656</z>
</position>
<rotation>
<x>-6.775821E-05</x>
<y>0.7171943</y>
<z>-5.94431731E-05</z>
<w>-0.696873248</w>
<eulerAngles>
<x>0.0102961874</x>
<y>268.353363</y>
<z>-0.000821786758</z>
</eulerAngles>
</rotation>
<pathToPathObject>Game/Routes/5</pathToPathObject>
<currentWaypoint>132</currentWaypoint>
</ObjectData>
<ObjectData>
<name>82</name>
<position>
<x>-119.590714</x>
<y>-1.68457687</y>
<z>340.381866</z>
</position>
<rotation>
<x>4.44578745E-06</x>
<y>0.692119658</y>
<z>-3.298495E-05</z>
<w>0.7217828</w>
<eulerAngles>
<x>0.00298378384</x>
<y>87.59627</y>
<z>-0.00237559224</z>
</eulerAngles>
</rotation>
<pathToPathObject>Game/Routes/1</pathToPathObject>
<currentWaypoint>69</currentWaypoint>
</ObjectData>
<ObjectData>
<name>14TR</name>
<position>
<x>-68.84237</x>
<y>-1.406132</y>
<z>124.329124</z>
</position>
<rotation>
<x>-0.000111638816</x>
<y>-0.72072953</y>
<z>0.000113228089</z>
<w>-0.693216443</w>
<eulerAngles>
<x>0.01821968</x>
<y>92.2294846</y>
<z>0.000225723881</z>
</eulerAngles>
</rotation>
<pathToPathObject>Game/Routes/2</pathToPathObject>
<currentWaypoint>31</currentWaypoint>
</ObjectData>
<ObjectData>
<name>E183</name>
<position>
<x>-166.316238</x>
<y>-1.497369</y>
<z>119.943764</z>
</position>
<rotation>
<x>-5.60864573E-05</x>
<y>0.7186203</y>
<z>-6.35707256E-05</z>
<w>-0.695402741</w>
<eulerAngles>
<x>0.009704288</x>
<y>268.118652</y>
<z>0.000447181</z>
</eulerAngles>
</rotation>
<pathToPathObject>Game/Routes/3</pathToPathObject>
<currentWaypoint>24</currentWaypoint>
</ObjectData>
<ObjectData>
<name>O405GTZ</name>
<position>
<x>-39.8570557</x>
<y>-1.39628959</y>
<z>191.1665</z>
</position>
<rotation>
<x>0.000736159855</x>
<y>-0.0035411038</y>
<z>2.109816E-05</z>
<w>0.9999935</w>
<eulerAngles>
<x>0.0843657553</x>
<y>359.5942</y>
<z>0.00211893814</z>
</eulerAngles>
</rotation>
<pathToPathObject>Game/Routes/4</pathToPathObject>
<currentWaypoint>135</currentWaypoint>
</ObjectData>
</objects>
</SaveData>
Пусть и не получилось заставить работать изначальный код, но всё равно спасибо всем, кто попытался помочь!