HomeLearnHow-toSaving Data in Unity3D Using Files

Saving Data in Unity3D Using Files

Updated: Feb 10, 2022 |

Published: Feb 09, 2022

  • Realm
  • C#
  • .NET
  • ...

By Dominic Frei

Rate this article

(Part 2 of the Persistence Comparison Series)

Persisting data is an important part of most games. Unity offers only a limited set of solutions, which means we have to look around for other options as well.

In Part 1 of this series, we explored Unity's own solution: PlayerPrefs. This time, we look into one of the ways we can use the underlying .NET framework by saving files. Here is an overview of the complete series:

  • Part 1: PlayerPrefs
  • Part 2: Files (this tutorial)
  • Part 3: BinaryReader and BinaryWriter (coming soon)
  • Part 4: SQL
  • Part 5: Realm Unity SDK
  • Part 6: Comparison of all those options

Like Part 1, this tutorial can also be found in the https://github.com/realm/unity-examples repository on the persistence-comparison branch.

https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/00_project_structure_80449491b4.jpg

Each part is sorted into a folder. The three scripts we will be looking at are in the File sub folder. But first, let's look at the example game itself and what we have to prepare in Unity before we can jump into the actual coding.

#Example game

Note that if you have worked through any of the other tutorials in this series, you can skip this section since we are using the same example for all parts of the series so that it is easier to see the differences between the approaches.

The goal of this tutorial series is to show you a quick and easy way to make some first steps in the various ways to persist data in your game.

Therefore, the example we will be using will be as simple as possible in the editor itself so that we can fully focus on the actual code we need to write.

https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/01_capsule_in_scene_ded3b7afd6.jpg

A simple capsule in the scene will be used so that we can interact with a game object. We then register clicks on the capsule and persist the hit count.

https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/02_add_a_capsule_db40af08fb.jpg

When you open up a clean 3D template, all you need to do is choose GameObject -> 3D Object -> Capsule.

You can then add scripts to the capsule by activating it in the hierarchy and using Add Component in the inspector.

https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/03_add_script_866f718104.jpg

https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/04_script_name_992c707274.jpg

The scripts we will add to this capsule showcasing the different methods will all have the same basic structure that can be found in HitCountExample.cs.

1using UnityEngine;
2
3/// <summary>
4/// This script shows the basic structure of all other scripts.
5/// </summary>
6public class HitCountExample : MonoBehaviour
7{
8 // Keep count of the clicks.
9 [SerializeField] private int hitCount; // 1
10
11 private void Start() // 2
12 {
13 // Read the persisted data and set the initial hit count.
14 hitCount = 0; // 3
15 }
16
17 private void OnMouseDown() // 4
18 {
19 // Increment the hit count on each click and save the data.
20 hitCount++; // 5
21 }
22}

The first thing we need to add is a counter for the clicks on the capsule (1). Add a [SerilizeField] here so that you can observe it while clicking on the capsule in the Unity editor.

Whenever the game starts (2), we want to read the current hit count from the persistence and initialize hitCount accordingly (3). This is done in the Start() method that is called whenever a scene is loaded for each game object this script is attached to.

The second part to this is saving changes, which we want to do whenever we register a mouse click. The Unity message for this is OnMouseDown() (4). This method gets called every time the GameObject that this script is attached to is clicked (with a left mouse click). In this case, we increment the hitCount (5) which will eventually be saved by the various options shown in this tutorials series.

#File

(See FileExampleSimple.cs in the repository for the finished version.)

One of the ways the .NET framework offers us to save data is using the File class:

Provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of FileStream objects.

Besides that, the File class is also used to manipulate the file itself, reading and writing data. On top of that, it offers ways to read meta data of a file, like time of creation.

When working with a file, you can also make use of several options to change FileMode or FileAccess.

The FileStream mentioned in the documentation is another approach to work with those files, providing additional options. In this tutorial, we will just use the plain File class.

Let's have a look at what we have to change in the example presented in the previous section to save the data using File:

1using System;
2using System.IO;
3using UnityEngine;
4
5public class FileExampleSimple : MonoBehaviour
6{
7 // Resources:
8 // https://docs.microsoft.com/en-us/dotnet/api/system.io.file?view=net-5.0
9
10 [SerializeField] private int hitCount = 0;
11
12 private const string HitCountFile = "hitCountFile.txt";
13
14 private void Start()
15 {
16 if (File.Exists(HitCountFile))
17 {
18 var fileContent = File.ReadAllText(HitCountFile);
19 hitCount = Int32.Parse(fileContent);
20 }
21 }
22
23 private void OnMouseDown()
24 {
25 hitCount++;
26
27 // The easiest way when working with Files is to use them directly.
28 // This writes all input at once and overwrites a file if executed again.
29 // The File is opened and closed right away.
30 File.WriteAllText(HitCountFile, hitCount.ToString());
31 }
32
33}

First we define a name for the file that will hold the data (1). If no additional path is provided, the file will just be saved in the project folder when running the game in the Unity editor or the game folder when running a build. This is fine for the example.

Whenever we click on the capsule (2) and increment the hit count (3), we need to save that change. Using File.WriteAllText() (4), the file will be opened, data will be saved, and it will be closed right away. Besides the file name, this function expects the contents as a string. Therefore, we have to transform the hitCount by calling ToString() before passing it on.

The next time we start the game (5), we want to load the previously saved data. First we check if the file already exists (6). If it does not exist, we never saved before and can just keep the default value for hitCount. If the file exists, we use ReadAllText() to get that data (7). Since this is a string again, we need to convert here as well using Int32.Parse() (8). Note that this means we have to be sure about what we read. If the structure of the file changes or the player edits it, this might lead to problems during the parsing of the file.

Let's look into extending this simple example in the next section.

#Extended example

(See FileExampleExtended.cs in the repository for the finished version.)

The previous section showed the most simple example, using just one variable that needs to be saved. What if we want to save more than that?

Depending on what needs to saved, there are several different approaches. You could use multiple files or you can write multiple lines inside the same file. The latter shall be shown in this section by extending the game to recognize modifier keys. We want to detect normal clicks, Shift+Click, and Control+Click.

First, update the hit counts so that we can save three of them:

1[SerializeField] private int hitCountUnmodified = 0;
2[SerializeField] private int hitCountShift = 0;
3[SerializeField] private int hitCountControl = 0;

We also want to use a different file name so we can look at both versions next to each other:

1private const string HitCountFileUnmodified = "hitCountFileExtended.txt";

The last field we need to define is the key that is pressed:

1private KeyCode modifier = default;

The first thing we need to do is check if a key was pressed and which key it was. Unity offers an easy way to achieve this using the Input class's GetKey function. It checks if the given key was pressed or not. You can pass in the string for the key or to be a bit more safe, just use the KeyCode enum. We cannot use this in the OnMouseClick() when detecting the mouse click though:

Note: Input flags are not reset until Update. You should make all the Input calls in the Update Loop.

Add a new method called Update() (1) which is called in every frame. Here we need to check if the Shift or Control key was pressed (2) and if so, save the corresponding key in modifier (3). In case none of those keys was pressed (4), we consider it unmodified and reset modifier to its default (5).

1private void Update() // 1
2{
3 // Check if a key was pressed.
4 if (Input.GetKey(KeyCode.LeftShift)) // 2
5 {
6 // Set the LeftShift key.
7 modifier = KeyCode.LeftShift; // 3
8 }
9 else if (Input.GetKey(KeyCode.LeftControl)) // 2
10 {
11 // Set the LeftControl key.
12 modifier = KeyCode.LeftControl; // 3
13 }
14 else // 4
15 {
16 // In any other case reset to default and consider it unmodified.
17 modifier = default; // 5
18 }
19}

Now to saving the data when a click happens:

1private void OnMouseDown() // 6
2{
3 // Check if a key was pressed.
4 switch (modifier)
5 {
6 case KeyCode.LeftShift: // 7
7 // Increment the Shift hit count.
8 hitCountShift++; // 8
9 break;
10 case KeyCode.LeftCommand: // 7
11 // Increment the Control hit count.
12 hitCountControl++; // 8
13 break;
14 default: // 9
15 // If neither Shift nor Control was held, we increment the unmodified hit count.
16 hitCountUnmodified++; // 10
17 break;
18 }
19
20 // 11
21 // Create a string array with the three hit counts.
22 string[] stringArray = {
23 hitCountUnmodified.ToString(),
24 hitCountShift.ToString(),
25 hitCountControl.ToString()
26 };
27
28 // 12
29 // Save the entries, line by line.
30 File.WriteAllLines(HitCountFileUnmodified, stringArray);
31}

Whenever a mouse click is detected on the capsule (6), we can then perform a similar check to what happened in Update(), only we use modifier instead of Input.GetKey() here.

Check if modifier was set to KeyCode.LeftShift or KeyCode.LeftControl (7) and if so, increment the corresponding hit count (8). If no modifier was used (9), increment the hitCountUnmodified.

As seen in the last section, we need to create a string that can be saved in the file. There is a second function on File that accepts a string array and then saves each entry in one line: WriteAllLines().

Knowing this, we create an array containing the three hit counts (11) and pass this one on to File.WriteAllLines().

Start the game, and click the capsule using Shift and Control. You should see the three counters in the Inspector.

https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/07_hit_count_extended_editor_dc35120d75.jpg

After stopping the game and therefore saving the data, a new file hitCountFileExtended.txt should exist in your project folder. Have a look at it. It should look something like this:

https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/08_hit_count_extended_file_da0c062a3e.jpg

Last but not least, let's look at how to load the file again when starting the game:

1private void Start()
2{
3 // 12
4 // Check if the file exists. If not, we never saved before.
5 if (File.Exists(HitCountFileUnmodified))
6 {
7 // 13
8 // Read all lines.
9 string[] textFileWriteAllLines = File.ReadAllLines(HitCountFileUnmodified);
10
11 // 14
12 // For this extended example we would expect to find three lines, one per counter.
13 if (textFileWriteAllLines.Length == 3)
14 {
15 // 15
16 // Set the counters correspdoning to the entries in the array.
17 hitCountUnmodified = Int32.Parse(textFileWriteAllLines[0]);
18 hitCountShift = Int32.Parse(textFileWriteAllLines[1]);
19 hitCountControl = Int32.Parse(textFileWriteAllLines[2]);
20 }
21 }
22}

First, we check if the file even exists (12). If we ever saved data before, this should be the case. If it exists, we read the data. Similar to writing with WriteAllLines(), we use ReadAllLines (13) to create a string array where each entry represents one line in the file.

We do expect there to be three lines, so we should expect the string array to have three entries (14).

Using this knowledge, we can then assign the three entries from the array to the corresponding hit counts (15).

As long as all the data saved to those lines belongs together, the file can be one option. If you have several different properties, you might create multiple files. Alternatively, you can save all the data into the same file using a bit of structure. Note, though, that the numbers will not be associated with the properties. If the structure of the object changes, we would need to migrate the file as well and take this into account the next time we open and read the file.

Another possible approach to structuring your data will be shown in the next section using JSON.

#More complex data

(See FileExampleJson.cs in the repository for the finished version.)

JSON is a very common approach when saving structured data. It's easy to use and there are frameworks for almost every language. The .NET framework provides a JsonSerializer. Unity has its own version of it: JsonUtility.

As you can see in the documentation, the functionality boils down to these three methods:

  • FromJson: Create an object from its JSON representation.
  • FromJsonOverwrite: Overwrite data in an object by reading from its JSON representation.
  • ToJson: Generate a JSON representation of the public fields of an object.

The JsonUtility transforms JSON into objects and back. Therefore, our first change to the previous section is to define such an object with public fields:

1private class HitCount
2{
3 public int Unmodified;
4 public int Shift;
5 public int Control;
6}

The class itself can be private and just be added inside the FileExampleJson class, but its fields need to be public.

As before, we use a different file to save this data. Update the filename to:

1private const string HitCountFileJson = "hitCountFileJson.txt";

When saving the data, we will use the same Update() method as before to detect which key was pressed.

The first part of OnMouseDown() (1) can stay the same as well, since this part only increments the hit count in depending on the modifier used.

1private void OnMouseDown()
2{
3 // 1
4 // Check if a key was pressed.
5 switch (modifier)
6 {
7 case KeyCode.LeftShift:
8 // Increment the Shift hit count.
9 hitCountShift++;
10 break;
11 case KeyCode.LeftCommand:
12 // Increment the Control hit count.
13 hitCountControl++;
14 break;
15 default:
16 // If neither Shift nor Control was held, we increment the unmodified hit count.
17 hitCountUnmodified++;
18 break;
19 }
20
21 // 2
22 // Create a new HitCount object to hold this data.
23 var updatedCount = new HitCount
24 {
25 Unmodified = hitCountUnmodified,
26 Shift = hitCountShift,
27 Control = hitCountControl,
28 };
29
30 // 3
31 // Create a JSON using the HitCount object.
32 var jsonString = JsonUtility.ToJson(updatedCount, true);
33
34 // 4
35 // Save the json to the file.
36 File.WriteAllText(HitCountFileJson, jsonString);
37}

However, we need to update the second part. Instead of a string array, we create a new HitCount object and set the three public fields to the values of the hit counters (2).

Using JsonUtility.ToJson(), we can transform this object to a string (3). If you pass in true for the second, optional parameter, prettyPrint, the string will be formatted in a nicely readable way.

Finally, as in FileExampleSimple.cs, we just use WriteAllText() since we're only saving one string, not an array (4).

Then, when the game starts, we need to read the data back into the hit count:

1private void Start()
2{
3 // Check if the file exists to avoid errors when opening a non-existing file.
4 if (File.Exists(HitCountFileJson)) // 5
5 {
6 // 6
7 var jsonString = File.ReadAllText(HitCountFileJson);
8 var hitCount = JsonUtility.FromJson<HitCount>(jsonString);
9
10 // 7
11 if (hitCount != null)
12 {
13 // 8
14 hitCountUnmodified = hitCount.Unmodified;
15 hitCountShift = hitCount.Shift;
16 hitCountControl = hitCount.Control;
17 }
18 }
19}

We check if the file exists first (5). In case it does, we saved data before and can proceed reading it.

Using ReadAllText, we read the string from the file and transform it via JsonUtility.FromJson<>() into an object of type HitCount (6).

If this happened successfully (7), we can then assign the three properties to their corresponding hit count (8).

When you run the game, you will see that in the editor, it looks identical to the previous section since we are using the same three counters. If you open the file hitCountFileJson.txt, you should then see the three counters in a nicely formatted JSON.

https://mongodb-devhub-cms.s3.us-west-1.amazonaws.com/09_hit_count_extended_json_66a1e47fd6.jpg

Note that the data is saved in plain text. In a future tutorial, we will look at encryption and how to improve safety of your data.

#Conclusion

In this tutorial, we learned how to utilize File to save data. JsonUtility helps structure this data. They are simple and easy to use, and not much code is required.

What are the downsides, though?

First of all, we open, write to, and save the file every single time the capsule is clicked. While not a problem in this case and certainly applicable for some games, this will not perform very well when many save operations are made.

Also, the data is saved in plain text and can easily be edited by the player.

The more complex your data is, the more complex it will be to actually maintain this approach. What if the structure of the HitCount object changes? You have to change account for that when loading an older version of the JSON. Migrations are necessary.

In the following tutorials, we will (among other things) have a look at how databases can make this job a lot easier and take care of the problems we face here.

Please provide feedback and ask any questions in the Realm Community Forum.

Rate this article
MongoDB logo
© 2021 MongoDB, Inc.

About

  • Careers
  • Investor Relations
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2021 MongoDB, Inc.