XML serialisaton. Error: XmlException: Root element is missing.

MaksimMaltsev

Member
Joined
Mar 29, 2021
Messages
9
Programming Experience
1-3
Hi all. I am making a game like minecraft. I need to keep the world when the player exits the game. Chunk (it is a place 16 by 16 blocks) and world contain a lot datas, that don't serialize can. so i have created classes WorldData and ChunkData, that contain only serializable varibles. But i have a error when world is serialized: XmlException: Root element is missing. This is the code:

WorldData:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.IO;
using System.Xml.Serialization;

public class WorldData{
    public static XmlSerializableDictionary<Vector2, ChunkData> bloksData = new XmlSerializableDictionary<Vector2, ChunkData>();//deserialized dictionary
    public static XmlSerializableDictionary<VectorConventer, ChunkData> bloksConvertData = new XmlSerializableDictionary<VectorConventer, ChunkData>();//serializible dictionary

    //get deserialized world <Vector2, ChunkData>
    public static XmlSerializableDictionary<Vector2, ChunkData> Deserialization(){
        XmlSerializer WorldXML = new XmlSerializer(typeof(XmlSerializableDictionary<VectorConventer, ChunkData>));
        using (FileStream fs = new FileStream("New_World.xml", FileMode.OpenOrCreate)){
            bloksConvertData = (XmlSerializableDictionary<VectorConventer, ChunkData>)WorldXML.Deserialize(fs); //Error. XmlException: Root element is missing.
        }
        foreach(var item in bloksData){
            bloksConvertData[new VectorConventer(item.Key)] = item.Value;
        }
        return bloksData;
    }

    //serialisation
    static void SaveWorld(){
        foreach(var item in bloksData){
            VectorConventer keyVec = new VectorConventer(item.Key);
            bloksConvertData[keyVec] = item.Value;
        }
        XmlSerializer WorldXML = new XmlSerializer(typeof(XmlSerializableDictionary<VectorConventer, ChunkData>));
        using (FileStream fs = new FileStream("New_World.xml", FileMode.OpenOrCreate)){
            WorldXML.Serialize(fs, bloksConvertData);
        }
    }

    //add new chunk to save
    public static void AddNewChunk(Vector2 position){
        bloksData.Add(position, new ChunkData());
    }
}
CunkData:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using System.IO;
using System.Runtime.Serialization;
using System.Xml.Serialization;


[Serializable]
public class ChunkData
{
    public ChunkData(){}
    public BlockType [] bloks = new BlockType [165888];
    public bool isGenetated;
    
    public float posX;
    public float posZ;
    
    public void SetValues(BlockType [ , , ] bloks3d, Vector2 position){
        bloks = ToSimpleArray(bloks3d);
        posX = position.x;
        posZ = position.y;
    }

    public BlockType [ , , ] GetArray(){
        return To3dArray(bloks);
    }
    //to three dimensional array
    private static BlockType [ , , ] To3dArray(BlockType [] Array){
        BlockType [ , , ] resultArray = new BlockType [18, 512, 18];
        for(int i = 0; i < 18; i++){
            for(int j = 0; j < 512; j++){
                for(int k = 0; k < 18; k++){
                    resultArray[i, j, k] = Array[i*512*18 + j*18 + k];
                }
            }
        }
        return resultArray;
    }
    //to one dimensional array
    private static BlockType [] ToSimpleArray(BlockType [ , , ] Array)
    {
        BlockType [] resultArray = new BlockType [18 * 512 * 18];
        int index = 0;
        BlockType [] SimpleArray = new BlockType [27];
        for(int i = 0; i < 18; i++){
            for(int j = 0; j < 512; j++){
                for(int k = 0; k < 18; k++){
                    SimpleArray[index] = Array[i, j, k];
                    index++;
                }
            }
        }
        return resultArray;
    }
}
World:
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using UnityEngine;

public class World : MonoBehaviour
{
    public static Dictionary<Vector2, Chunck> chunks = new Dictionary<Vector2, Chunck>();

    public List <Vector2> toGenerate;
    public List <Vector2> toDelete;
    public static XmlSerializableDictionary<Vector2, Chunck> loadedChuncks = new XmlSerializableDictionary<Vector2, Chunck>();
    //<...>

    void Start() //called when the game starts
    {
        LoadWorld();
        //<...>
    }

    public void LoadWorld(){
        Dictionary<Vector2, ChunkData> world = WorldData.Deserialization();
        foreach (var item in world)
        {
            loadedChuncks.Add(item.Key, new Chunck());
            loadedChuncks[item.Key].Data = item.Value;
            loadedChuncks[item.Key].SetValues();
        }
    }
    //<...>
}
Chunck:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Chunck : MonoBehaviour
{
    public ChunkData Data;
    public BlockType [ , , ] bloks  = new BlockType [18, 512, 18]; //data of the blocks in the chunk
    
    public Vector2 Position = new Vector2();
    //<...>

    //generation
    public void ChunckGeneration(){
        //<...>
    }

    public void Optimize() //build voxels
    {   
        //<...>
    }

    //set data to serialisation
    void ChunckToSave(){
        Data.SetValues(this.bloks, Position);
    }

    //get data
    public void SetValues(){
        this.bloks = Data.GetArray();
        this.isGenetated = false;
    }
}
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
3,762
Location
Chesapeake, VA
Programming Experience
10+
Chunk (it is a place 16 by 16 blocks) and world contain a lot datas, that don't serialize can.
To me, that is a code smell that you are mixing in live View information in along with your Data model. Unless you have cycical references within your Data model, it should be able to serialize.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
3,762
Location
Chesapeake, VA
Programming Experience
10+
Anyway, have you set a breakpoint at the end of your SaveWorld() method? Have you inspected the contents of your "New_world.xml" file right after the save operation? Does it have a root node?
 

JohnH

C# Forum Moderator
Staff member
Joined
Apr 23, 2011
Messages
1,222
Location
Norway
Programming Experience
10+
Open the xml file in VS, you should quickly see the tree structure and problem with single root node. If the document isn't formatted with indenting you can do it with Edit > Advanced > Format Document to easier see the structure.
 

JohnH

C# Forum Moderator
Staff member
Joined
Apr 23, 2011
Messages
1,222
Location
Norway
Programming Experience
10+
Just had a quick look at the code, in serialization you use the above FileMode.OpenOrCreate. When you have an existing file new content may only partially overwrite it, for example if existing content was "abc" and you opened it and wrote "Z", the file would now contain "Zbc". Since serialization writes complete content (and in xml case single root) you should use FileMode.Create which creates a new file or completely overwrites the existing.
 

JohnH

C# Forum Moderator
Staff member
Joined
Apr 23, 2011
Messages
1,222
Location
Norway
Programming Experience
10+
If you haven't saved data yet, then you can't deserialize. Doesn't look like you're checking, but assume there is data on start.
You're also using FileMode.OpenOrCreate on deserialization, so if file doesn't exist it creates an empty one and crashes when attempting to read the empty file.
 

MaksimMaltsev

Member
Joined
Mar 29, 2021
Messages
9
Programming Experience
1-3
You're also using FileMode.OpenOrCreate on deserialization
Now I am already using FileMode.Create. But I vorgot to call function SaveWorld when quitting the game. Now i fixed it and have this in my xml file:
New_World:
<?xml version="1.0"?>
<Dictionary />
It's no already empty, but no data from the dictionary is there. So the error remained. What am I doing wrong?
 

JohnH

C# Forum Moderator
Staff member
Joined
Apr 23, 2011
Messages
1,222
Location
Norway
Programming Experience
10+
Debug the save method, is there actually any data going through that foreach loop?
 

MaksimMaltsev

Member
Joined
Mar 29, 2021
Messages
9
Programming Experience
1-3
Debug the save method, is there actually any data going through that foreach loop?
Yes, there are a datas of a chunks (I am debugging heir coordinates). But I vorgt add chunks to WorldData. Now I have this in my xml file:
New_World:
<?xml version="1.0"?>
<Dictionary>
  <item>
    <key>
      <VectorConventer xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <x>-80</x>
        <y>-80</y>
      </VectorConventer>
    </key>
    <value>
      <ChunkData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <bloks>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
            .........
            <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
          <v>Air</v>
        </bloks>
        <isGenetated>false</isGenetated>
        <posX>0</posX>
        <posZ>0</posZ>
      </ChunkData>
    </value>
  </item>
</Dictionary>
looks like the algorithm for saving the chunk after generation does not work correctly, but now it's not important. The xml file size is 431,3 mb. It's too big, and there are only one chunk that has coordinates -80 -80, but I need to save ~200 chunks. Need to reduce stze of xml file. If I use char, not enum, does it help? Can write dictionary, that seves instead of many elements only their number and value? How to do it? Now XmlSerializableDictionary script contains this code:
XmlSerializableDictionary:
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
 
    [XmlRoot("Dictionary")]
public class XmlSerializableDictionary<TKey, TValue>
    : Dictionary<TKey, TValue>, IXmlSerializable
{
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }
 
    public void ReadXml(System.Xml.XmlReader reader)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));
        bool wasEmpty = reader.IsEmptyElement;
        reader.Read();
        if (wasEmpty)
            return;
        while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
        {
            reader.ReadStartElement("item");
            reader.ReadStartElement("key");
            TKey key = (TKey)keySerializer.Deserialize(reader);
            reader.ReadEndElement();
            reader.ReadStartElement("value");
            TValue value = (TValue)valueSerializer.Deserialize(reader);
            reader.ReadEndElement();
            this.Add(key, value);
            reader.ReadEndElement();
            reader.MoveToContent();
        }
        reader.ReadEndElement();
    }
 
    public void WriteXml(System.Xml.XmlWriter writer)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));
        foreach (TKey key in this.Keys)
        {
            writer.WriteStartElement("item");
            writer.WriteStartElement("key");
            keySerializer.Serialize(writer, key);
            writer.WriteEndElement();
            writer.WriteStartElement("value");
            TValue value = this[key];
            valueSerializer.Serialize(writer, value);
            writer.WriteEndElement();
            writer.WriteEndElement();
        }
    }
}
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
3,762
Location
Chesapeake, VA
Programming Experience
10+
XML is a very verbose serialization format. Why can't you use a binary serializer instead?
 

JohnH

C# Forum Moderator
Staff member
Joined
Apr 23, 2011
Messages
1,222
Location
Norway
Programming Experience
10+
Json may generate 1/10th of filesize of xml, even smaller than BinaryFormatter serialization, and file still be human readable as text. Serialize JSON to a file
If you need even smaller files just add compression (GZipStream or DeflateStream).
Json is also by far the fastest of the three.
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
3,762
Location
Chesapeake, VA
Programming Experience
10+
If the data is repetitive, it will compress well.
 

MaksimMaltsev

Member
Joined
Mar 29, 2021
Messages
9
Programming Experience
1-3
If the data is repetitive, it will compress well.
i replaced xml with json serialization. I addn't compression yet, but size of the file is already smal. But the serialisation takes so long, like in xml. 6 minuts. I need to reduce time to 5 seconds. How to do it?
Now my serialization class looks so:
WorldData:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.IO;
using System.Xml.Serialization;

public class WorldData{
    public static Dictionary<Vector2, ChunkData> bloksData = new Dictionary<Vector2, ChunkData>();//deserialized dictionary
    public static Dictionary<VectorConventer, ChunkData> bloksConvertData = new Dictionary<VectorConventer, ChunkData>();//serializible dictionary


    //get deserialized world <Vector2, ChunkData>
    public static Dictionary<Vector2, ChunkData> Deserialization(){
        DataContractJsonSerializer serializer= new DataContractJsonSerializer(typeof(Dictionary<VectorConventer, ChunkData>));
        using (FileStream fs = new FileStream("New_World.json", FileMode.Create)){
            bloksConvertData = (Dictionary<VectorConventer, ChunkData>)serializer.ReadObject(fs); //Error. XmlException: Root element is missing.
        }
        foreach(var item in bloksData){
            bloksConvertData[new VectorConventer(item.Key)] = item.Value;
        }
        return bloksData;
    }

    //serialisation
    public static void SaveWorld(){
        //преобразование векторов
        foreach(var item in bloksData){
            VectorConventer keyVec = new VectorConventer(item.Key);
            bloksConvertData[keyVec] = item.Value;
        }
        //сама сериализация
        DataContractJsonSerializer serializer= new DataContractJsonSerializer(typeof(Dictionary<VectorConventer, ChunkData>));
        using (FileStream fs = new FileStream("New_World.json", FileMode.Create)){
            serializer.WriteObject(fs, bloksConvertData);
        }
    }

    //add new chunk to save
    public static void AddNewChunk(Vector2 position){
        bloksData.Add(position, new ChunkData());
    }
}
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
3,762
Location
Chesapeake, VA
Programming Experience
10+
Does your ChunkData still look like your original post where it contains an array of 165,888 BlockType? Is that array a sparse array? If so, have you consisdered changing the implementation of the class so that it exposes a something like a linked list or dictionary that only contains elements that are not empty?
 

MaksimMaltsev

Member
Joined
Mar 29, 2021
Messages
9
Programming Experience
1-3
Does your ChunkData still look like your original post where it contains an array of 165,888 BlockType?
Yes. I just added [DataContract] and [DataMember]
Is that array a sparse array?
As I understand it, a sparse array is array that doesn't contain empty elements. In my array there aren't empty elements, each cell of the array contain a type of block. But array has elements of the same type. Is it possibly to sava only different elements and their numbers? Is the conversion algorithm to and from a compressed array fast?
 

Skydiver

Staff member
Joined
Apr 6, 2019
Messages
3,762
Location
Chesapeake, VA
Programming Experience
10+
From the same you gave earlier, it looked like most of your BlockType values were Air. If you treated Air as the default (e.g. empty) value and only stored the non-Air values you could potentially save a lot of time and space. So when you are serializing you would only save the non-Air indexes, and on deserialization, you would only need to get these values put back into the array. (Recall that C# will initialize value types arrays with the value.)
 

MaksimMaltsev

Member
Joined
Mar 29, 2021
Messages
9
Programming Experience
1-3
From the same you gave earlier, it looked like most of your BlockType values were Air. If you treated Air as the default (e.g. empty) value and only stored the non-Air values you could potentially save a lot of time and space. So when you are serializing you would only save the non-Air indexes, and on deserialization, you would only need to get these values put back into the array. (Recall that C# will initialize value types arrays with the value.)
I tried to do so, but not succenssful. Now it works like this:
ChunkData:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;


[DataContract]
public class ChunkData
{
    public ChunkData(){}
    [DataMember]
    public BlockType [] bloks = new BlockType [165888];
    public bool isGenetated;
    
    public float posX;
    public float posZ;
    
    public void SetValues(BlockType [ , , ] bloks3d, Vector2 position){
        bloks = ToSimpleArray(bloks3d);
        posX = position.x;
        posZ = position.y;
    }

    public BlockType [ , , ] GetArray(){
        return To3dArray(bloks);
    }
    //to three dimensional array
    private static BlockType [ , , ] To3dArray(BlockType [] Array){
        BlockType [ , , ] resultArray = new BlockType [18, 512, 18];
        for(int i = 0; i < 18; i++){
            for(int j = 0; j < 512; j++){
                for(int k = 0; k < 18; k++){
                    resultArray[i, j, k] = Array[i*512*18 + j*18 + k];
                }
            }
        }
        return resultArray;
    }
    //to one dimensional array
    private static BlockType [] ToSimpleArray(BlockType [ , , ] Array)
    {
        Debug.Log("It's here");
        BlockType [] resultArray = new BlockType [18 * 512 * 18];
        int index = 0;
        for(int i = 0; i < 18; i++){
            for(int j = 0; j < 512; j++){
                for(int k = 0; k < 18; k++){
                    if(Array[i, j, k] != BlockType.Air){
                        resultArray[index] = Array[i, j, k];
                    }
                    index++;
                }
            }
        }
        return resultArray;
    }
}
WorldData:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.IO;
using System.Xml.Serialization;

public class WorldData{
    public static Dictionary<Vector2, ChunkData> bloksData = new Dictionary<Vector2, ChunkData>();//deserialized dictionary
    public static Dictionary<VectorConventer, ChunkData> bloksConvertData = new Dictionary<VectorConventer, ChunkData>();//serializible dictionary


    //get deserialized world <Vector2, ChunkData>
    public static Dictionary<Vector2, ChunkData> Deserialization(){
        DataContractJsonSerializer serializer= new DataContractJsonSerializer(typeof(Dictionary<VectorConventer, ChunkData>));
        using (FileStream fs = new FileStream("New_World.json", FileMode.Create)){
            bloksConvertData = (Dictionary<VectorConventer, ChunkData>)serializer.ReadObject(fs); //Error. XmlException: Root element is missing.
        }
        foreach(var item in bloksData){
            bloksConvertData[new VectorConventer(item.Key)] = item.Value;
        }
        return bloksData;
    }

    //serialisation
    public static void SaveWorld(){
        //преобразование векторов
        foreach(var item in bloksData){
            VectorConventer keyVec = new VectorConventer(item.Key);
            bloksConvertData[keyVec] = item.Value;
        }
        
        DataContractJsonSerializer serializer= new DataContractJsonSerializer(typeof(Dictionary<VectorConventer, ChunkData>));
        using (FileStream fs = new FileStream("New_World.json", FileMode.Create)){
            serializer.WriteObject(fs, bloksConvertData);
        }
    }

    //add new chunk to save
    public static void AddNewChunk(Vector2 position){
        if(!bloksData.ContainsKey(position)){
            bloksData.Add(position, new ChunkData());
        }
        if(World.chunks.ContainsKey(position)){
            World.chunks[position].Data = bloksData[position];
        }
    }
}
But I didn't think, that it helps to reduce serialisation time.
 
Top Bottom