Saving player information to a file. Which is the best file type?

I need to save the players information to a file. Info such as current score, and various other information. I imaging I can just write it to a text file, or maybe even an XML file. I might actually need two or three such files for the game. I don’t have any problem writing C# code to parse text or XML files, and update them when needed.

I thought I would ask some experienced developers in the Monogame community if they have a preferred file type that might work best? The file types do not have to be XML or text file format, that is just all I can think of that might work best for the game.

I expect to deploy the game to window/android and maybe IOS platforms, so this is something else that will need to be taken into consideration.

Can’t speak for how it compares to JSON but I’ve just managed all the code to do it in XML if you want to give it a go.

How to read/write xml to Android app? - Android - Community | MonoGame

XML or JSON doesn’t really matter… I find XML a little easier to read; however, I’ve found the JSON.NET a lot easier to work with than the system XML libraries.

The only time it really matters is if file size becomes an issue, then you’re looking at binary formats, or even compressed formats. Not worth worrying about unless you’re really hitting large files, or bandwidth issues (for mobile).

My preference is JSON. Much more compact than XML and easier to read (YMMV). Newtonsoft makes serialization easy.

I’m seeing others using Json more and more… Including 3rd party software like animation tools and stuff.

Maybe it’s time to make the switch?

The binary format is one of the best ways to make it almost impossible for a user to make changes manually, right?
And if a programmer reaches the level of starting network programming, it is strongly recommended to use it in real time.

3 Likes

I wouldn’t say impossible. Hex editors can be used to edit binary files. It’s just more difficult since it’s not obvious as to what the data means. I usually never bother, if people want to edit my config files then go ahead. If it’s a multiplayer type scenario, you probably don’t want to store those kinds of configs locally, or compare file hashes to validate file integrity.

Maybe not others cup of tea, but I preferred plain text file over XML or JSON with simple custom row structure delimited by comma, you can convert it to binary and vice very easily

E.g:

0,Dex,99
1,1212,1,XX,YY
2,blah,blah,33434,4444

Pseudocode code only :

string[] m_DataRows = null;
//
if( _WINDOWS )
{
     m_DataRows = File.ReadAllLines("SomeData.Dat");
}
else
{
    //
}
//
foreach (var m_Row in m_DataRows )
{
    string[] m_Columns = m_Row.Split(',');
    //
    if( m_Columns[0] == "0" ) // Players info eg.
    {
         _PlayerName = m_Columns[1];
         _Level      = m_Columns[2];
         // Etc..
    }
    //
    if( m_Columns[0] == "1" ) // 
    {
        // Etc..
    }
   // Etc.
}
3 Likes

Being able to change files was one of those things that lead me to modding, which lead me to programming :slight_smile: I make a POINT of making my game-files human readable! … I think of notepad as the user-end level-editor they already have installed !

2 Likes

Ok, here is a very simple and practical example of the difference…

I have two classes:

    [Serializable()]
    public class HighScoreEntry
    {
        public DateTime DateAchieved = DateTime.UtcNow;
        public int Level = 0;
        public int Difficulty = 0;
        public long Score = 0;
        public int Rank = 0;
        public string Initials = "";

        //TODO: Add Game Stats object

        public HighScoreEntry()
        {
        }

    }

and then just a simple collection:

	public class HighScoreCollection : List<HighScoreEntry>
	{

	}

So Serialize the Collection in XML I use:

        /// <summary>
        /// Returns a string of XML that represents the object passed in.
        /// </summary>
        /// <param name="p_object">The object to serialize.</param>
        /// <returns>XML string representing object</returns>
        /// <remarks></remarks>
        public static string XMLSerialize(object p_object)
        {

            System.IO.MemoryStream tempMemStream = new System.IO.MemoryStream();
            System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings();
            string tempString;

            settings.Indent = true;
            settings.Encoding = System.Text.Encoding.UTF8;
            settings.NewLineChars = System.Environment.NewLine;
            settings.NewLineHandling = System.Xml.NewLineHandling.Replace;

            using (System.IO.StringWriter stringWriter = new System.IO.StringWriter())
            {
                using (System.Xml.XmlWriter xmlWriter = System.Xml.XmlWriter.Create(stringWriter, settings))
                {
                    System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(p_object.GetType());
                    serializer.Serialize(xmlWriter, p_object);

                    tempString = stringWriter.ToString();
                    xmlWriter.Close();
                }

                stringWriter.Close();
            }

            return tempString;
        }

and I get the output of

<?xml version="1.0" encoding="utf-16"?>
<ArrayOfHighScoreEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>19800000</Score>
    <Rank>1</Rank>
    <Initials>AAA</Initials>
  </HighScoreEntry>
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>18000</Score>
    <Rank>2</Rank>
    <Initials>BBB</Initials>
  </HighScoreEntry>
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>17000</Score>
    <Rank>3</Rank>
    <Initials>CCC</Initials>
  </HighScoreEntry>
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>16000</Score>
    <Rank>4</Rank>
    <Initials>DDD</Initials>
  </HighScoreEntry>
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>15000</Score>
    <Rank>5</Rank>
    <Initials>EEE</Initials>
  </HighScoreEntry>
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>14000</Score>
    <Rank>6</Rank>
    <Initials>FFF</Initials>
  </HighScoreEntry>
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>13000</Score>
    <Rank>7</Rank>
    <Initials>GGG</Initials>
  </HighScoreEntry>
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>12000</Score>
    <Rank>8</Rank>
    <Initials>HHH</Initials>
  </HighScoreEntry>
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>11000</Score>
    <Rank>9</Rank>
    <Initials>III</Initials>
  </HighScoreEntry>
  <HighScoreEntry>
    <DateAchieved>2020-12-29T03:21:15.4197663Z</DateAchieved>
    <Level>0</Level>
    <Difficulty>0</Difficulty>
    <Score>10000</Score>
    <Rank>10</Rank>
    <Initials>JJJ</Initials>
  </HighScoreEntry>
</ArrayOfHighScoreEntry>

The JSON serialization is a little trickier as it uses a Contract Resolver…(at least in my case).

This allows me to include or exclude properties as I need. 99% of the time, I just serialize everything.

    public class JsonContractResolver : DefaultContractResolver
    {
        System.Collections.Generic.List<string> m_excludePropertyNames = null;
        System.Collections.Generic.List<string> m_includePropertyNames = null;

        public JsonContractResolver(System.Collections.Generic.List<string> p_excludePropertyNames, System.Collections.Generic.List<string> p_includePropertyNames)
        {
            m_excludePropertyNames = p_excludePropertyNames;
            m_includePropertyNames = p_includePropertyNames;
        }

        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
            string propertyName = "";
            string className = "";

            for (int i = properties.Count - 1; i >= 0; i--)
            {
                propertyName = properties[i].PropertyName;

                switch (propertyName)
                {
                    default:
                        propertyName = properties[i].PropertyName;
                        className = type.FullName;

                        if (m_excludePropertyNames != null)
                        {

                            if (m_excludePropertyNames.Contains(propertyName) || m_excludePropertyNames.Contains(String.Format("{0}.{1}", className, propertyName)))
                            {
                                properties.RemoveAt(i);
                            }

                        }
                        if (m_includePropertyNames != null)
                        {
                            if (!m_includePropertyNames.Contains(propertyName) && !m_includePropertyNames.Contains(String.Format("{0}.{1}", className, propertyName)))
                            {
                                properties.RemoveAt(i);
                            }
                        }

                        break;
                }
            }
            return properties;
        }
    }

Then to serialize the object I use:

        /// <summary>
        /// This takes an object and serializes it using the Newton Json Library.
        /// </summary>
        /// <param name="p_gameObject">The object to serialize</param>
        /// <returns></returns>
        public static string JsonSerialize(object p_gameObject)
        {
            return JsonSerialize(p_gameObject, null, null);
        }

        /// <summary>
        /// This takes an object and serializes it using the Newton Json Library.
        /// </summary>
        /// <param name="p_gameObject">The object to serialize</param>
        /// <param name="p_excludePropertyNames">A list of property names to exclude from the serialization.  You can specify properties on children as well if you fully qualify the property with the full type.</param>
        /// <param name="p_includePropertyNames">A list of property names to include from the serialization.  You can specify properties on children as well if you fully qualify the property with the full type.</param>
        /// <returns></returns>
        public static string JsonSerialize(object p_gameObject, System.Collections.Generic.List<string> p_excludePropertyNames, System.Collections.Generic.List<string> p_includePropertyNames)
        {
            return JsonConvert.SerializeObject(p_gameObject, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new JsonContractResolver(p_excludePropertyNames, p_includePropertyNames) });
        }

I then have code that loads and saves my high scores (This is Windows Centric, so you can modify for different platforms as needed)

       public static string SaveHighScores()
        {
            string jsonData = "";
            string fileName = "";

            try
            {
                jsonData = JsonSerialize(GlobalInfo.HighScores);

                fileName = GetAssetFilename(AssetTypes.Settings, "HighScores.json");
                if (fileName == "")
                {
                    return "Invalid HighScores Filename";
                }

                System.IO.File.WriteAllText(fileName, jsonData);
            }
            catch (Exception ex)
            {
                return ex.ToString();
            }

            return string.Empty;
        }

        public static string LoadHighScores()
        {
            string jsonData = "";
            string fileName = "";

            try
            {
                fileName = GetAssetFilename(AssetTypes.Settings, "HighScores.json");
                if (fileName == "")
                {
                    return "Invalid HighScores Filename";
                }

                if (System.IO.File.Exists(fileName))
                {
                    jsonData = System.IO.File.ReadAllText(fileName);
                    GlobalInfo.HighScores = (HighScoreCollection)JsonDeserialize(jsonData, typeof(HighScoreCollection));
                }
                else
                {
                    GlobalInfo.HighScores = new HighScoreCollection();
                    GlobalInfo.HighScores.Add(new HighScoreEntry("AAA", 1, 19800000, null));
                    GlobalInfo.HighScores.Add(new HighScoreEntry("BBB", 2, 18000, null));
                    GlobalInfo.HighScores.Add(new HighScoreEntry("CCC", 3, 17000, null));
                    GlobalInfo.HighScores.Add(new HighScoreEntry("DDD", 4, 16000, null));
                    GlobalInfo.HighScores.Add(new HighScoreEntry("EEE", 5, 15000, null));
                    GlobalInfo.HighScores.Add(new HighScoreEntry("FFF", 6, 14000, null));
                    GlobalInfo.HighScores.Add(new HighScoreEntry("GGG", 7, 13000, null));
                    GlobalInfo.HighScores.Add(new HighScoreEntry("HHH", 8, 12000, null));
                    GlobalInfo.HighScores.Add(new HighScoreEntry("III", 9, 11000, null));
                    GlobalInfo.HighScores.Add(new HighScoreEntry("JJJ", 10, 10000, null));

                    SaveHighScores();
                }

            }
            catch (Exception ex)
            {
                return ex.ToString();
            }

            return string.Empty;
        }
 

The GetAssetFilename is just a method I have that returns the path to where I load/save settings, etc…

So when I serialize the collection in JSON I get

[
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 19800000,
    "Rank": 1,
    "Initials": "AAA"
  },
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 18000,
    "Rank": 2,
    "Initials": "BBB"
  },
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 17000,
    "Rank": 3,
    "Initials": "CCC"
  },
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 16000,
    "Rank": 4,
    "Initials": "DDD"
  },
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 15000,
    "Rank": 5,
    "Initials": "EEE"
  },
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 14000,
    "Rank": 6,
    "Initials": "FFF"
  },
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 13000,
    "Rank": 7,
    "Initials": "GGG"
  },
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 12000,
    "Rank": 8,
    "Initials": "HHH"
  },
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 11000,
    "Rank": 9,
    "Initials": "III"
  },
  {
    "DateAchieved": "2020-12-29T03:21:15.4197663Z",
    "Level": 0,
    "Difficulty": 0,
    "PlayerStats": null,
    "Score": 10000,
    "Rank": 10,
    "Initials": "JJJ"
  }
]

The XML file is 2,533 bytes, the JSON is 1,897 bytes. The JSON file is 26% smaller in this case. When the files get very large then the savings really make a difference.

1 Like

2020-12-29T03:21:15.4197663Z,0,0, ,19800000,1,AAA
2020-12-29T03:21:15.4197663Z,0,0, ,18000,2,BBB
2020-12-29T03:21:15.4197663Z,0,0, ,17000,3,CCC
2020-12-29T03:21:15.4197663Z,0,0, ,16000,4,DDD
2020-12-29T03:21:15.4197663Z,0,0, ,15000,5,EEE
2020-12-29T03:21:15.4197663Z,0,0, ,14000,6,FFF
2020-12-29T03:21:15.4197663Z,0,0, ,13000,7,GGG
2020-12-29T03:21:15.4197663Z,0,0, ,12000,8,HHH
2020-12-29T03:21:15.4197663Z,0,0, ,11000,9,III
2020-12-29T03:21:15.4197663Z,0,0, ,10000,10,JJJ

Let’s include text file comma delimited with 484 bytes only :wink:

1 Like

Yes, yours is a lot more compact, however it has a few issues.

  1. Looking at the file you don’t know what each column is.
  2. If your class changes you have to make changes to serialize/de-serialize the new information.
  3. How do you deal with data that has a , in it?
  4. How do you deal with data that has a new line in it?

Hi Arcadenut,

Since the OP seeking an advice whether to use XML, Text file or best file type the reason I posted my suggestions that I wont use any heavy weight reader/writer with this simple task on saving players information, the more the solution the merrier ^_^y

Anyway :

Looking at the file you don’t know what each column is.

  • If I wrote that game surely I will know it : )

If your class changes you have to make changes to serialize/de-serialize the new information.

  • Of course.

How do you deal with data that has a , in it?

  • I wont do that since it’s a comma delimited and of course there’s a work around.

How do you deal with data that has a new line in it?

  • AppendText ? or even rewrting 100 players text info to file is a breeze.

How many players are we talking about here? if this is my project and were talking about millions of players information where concurrent reading and writing or data is needed will use my own ManagedDbf.

Cheers ^_^Y

Didn’t know about the Contract Resolver. I don’t have used serialization that often. I think last time I excluded one or two things because it was redundant data only included in the class for convenience reasons. Good to know about the possibility of a Contract Resolver. Is this something only available with Newtonsofts JSON serializer? Last time I used the .NET JSON serializer it worked very good too. (before I also used Newtonsofts JSON serializer)

I think I used annotations with the .NET JSON serializer to exclude some of the data the last time.

For the player information I would use JSON because it is structured and an easy enough format to edit by hand also.

For simple things that don’t change that much like a simple 3D format text should also be fine which leaves the possibility to change the data by hand.

I would say it depends all on the specific use case.

1 Like

I use TXT at first until the project’s done (then I use binary). It’s easy to read, compact, fast, and easy to add new things to it (ie: changes to class or struct data to write out can be modified at any time and it still works). No matter what happens, it’s forward & reverse compatible by using switches with tags. Here’s a really rough example to give some idea of what I mean:
ie: (using Split on ‘,’ to put into array str):

// ie: Switch(str[0]) { case “AREA”: level_area = str[1].toInt(); level_sector = strs[2];
AREA,25,14
SCORE,10273,
LEVEL,2,
PLAYER_REC,20,30,100,200,
PLAYER,1,
STR,14,INT,10,DEX,9,HP,250,MP,200, //ie: case “STR”: player[player_index].str = strs[1].toInt(); etc…
WEAPONS,12,44,33,65,9,15,
AMMO,50,100,150,10,16,330,

Once I know I’ll never add or remove anything from the game’s data layout, I’d make the switch to binary serializer. In my experience so far, the TXT switch method is fast enough that it really doesn’t matter if you don’t - depends if you wanted to ensure some data is private(so ppl can’t cheat).

I actually miss using the CPP way of doing it cuz you could write out the state of the entire game to binary by putting the memory state of the class/struct(s) to file in one easy shot. Super easy.

2 Likes

( NOTE: This reply is not directed at you, DexterZ, I’m just building off what you said :smiley: )

And herein lies the crux of the matter.

Everything in software development is a tradeoff and this is no different. If space is of the utmost importance, use a binary file. If it’s less important but you still want to hand edit it, use a compact text file. And so up on the chain, all the way to XML, which is more readable than JSON (as it gives you the variable names built in) but is probably the largest file size of what’s been discussed here.

The point is, pick what’s right for you and the application you’re developing. There are lots of options here, which is great! However, options can be overwhelming, so it’s important to have an idea of what each one is for :wink:

1 Like

Text and binary for data if i want to be able to modify it or let the user modify it text for sure its super simple.
Also im used to doing it myself.

Binary if its big and if im doing some sort of compacting of data.

If its data i don’t want changed and need for sure i could just stick it directly in a class file as a array string ect…

2 Likes
  1. How do you deal with data that has a , in it?

I never understood why the comma is so common, I personally use this: |
And it’s just as easy for me to read, plus I can store full texts that contain the comma.

Read it the reverse of how you write it that or you have to make a class that prefix’s everything you write.
So it can arbitrarily search and read out the data but then your basically writing your own equivalent sort of xml type reader writer … which im not gonna lie ive got a class like that somewere and it’s not that hard just allow for all the data types you need.

Thanks everyone, great information!