Как прочитать данные из 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 шт):

Автор решения: AnnaBazueva

Проверьте как, производится загрузка.

// Загрузка
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>
→ Ссылка
Автор решения: Kostyan_Sigaev

Суть работы старого когда заключалась в том, что он сохранял все дочерние объекты указанного объекта со всеми компонентами, значениями компонентов и т.д., из-за чего средний файл с сохранением мог весить около 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>

Пусть и не получилось заставить работать изначальный код, но всё равно спасибо всем, кто попытался помочь!

→ Ссылка