Making a RTS game #20: Saving the player’s data properly (Unity/C#)

Let’s continue our RTS: we’re going to take a look at JSON serialisation!

This article is also available on Medium.

In the last episodes, we’ve talked about our game parameters and how to modify them at runtime using an in-game UI settings panel. We’ve played around with our Scriptable Objects quite a lot and we’ve even managed to display them in a custom editor… but we have missed out on one key thing: what happens when we build our game?

Currently, if we build our RTS game, you’ll see that any changes we make to our game parameters in the game are completely forgotten the next time around; the fancy Scriptable Objects updates we implemented last time seem to be totally useless for data storage here… and, in truth, they are!

So today, let’s focus on this data saving problem 😉

Why are our game parameters changes retained in the editor but not in a build?

In the Unity docs for the Scriptable Object, we can read that:

When you use the Editor, you can save data to ScriptableObjects while editing and at run time because ScriptableObjects use the Editor namespace and Editor scripting. In a deployed build, however, you can’t use ScriptableObjects to save data, but you can use the saved data from the ScriptableObject Assets that you set up during development.

Alright, so what does that mean exactly? Let’s look at the two sentences one after the other.

  • “When you use the Editor, you can save data to ScriptableObjects while editing and at run time because ScriptableObjects use the Editor namespace and Editor scripting”: in other words, ScriptableObjects are not scene objects but assets – they are saved inside the project and not inside your current scene context. Any change that you make at runtime in the editor is automatically saved on disk in the project itself, which is why the updates you make to a ScriptableObject while you’re testing your game remain after you’ve quit the preview.
  • “In a deployed build, however, you can’t use ScriptableObjects to save data, but you can use the saved data from the ScriptableObject Assets that you set up during development.”: there are two important parts to this sentence. First, the data is not saved when you quit a build game; this is because ScriptableObjects, just like other resources, are bundled and packaged during the build so they can be shipped with the game, but at that point they are no longer modifiable (in the build version of your game, assets stay the same at vitam aeternam). Second, you get back the values the devs and game designers decided on during development in your final build – basically, all the tweaks you did in the Unity editor are saved in the ScriptableObjects (since they are assets) and they are packaged along with the other resources.

So, in short: Scriptable Objects are great during dev but since they’re bundled during the build, they are not modifiable anymore in the final game. If we want to remember the modifications we made at runtime in the game build, we have to store the data on disk in handmade files that our program will be able to read upon start up and create (or edit) when we quit the game.

This process of writing your data in a well-defined readable manner is called serialisation. The reverse process of reloading the data from the files is naturally called deserialisation. There are various file formats for serialising your Unity objects but the common ones are JSON or XML. I myself prefer JSON because I find it very human-readable – you can even easily update your files by hand during your tests to try things out!

If you’re not used to the JSON format, here is an example of the serialisation of our GameSoundParameters asset we will end up with at the end of this tutorial:

As you can see, JSON uses “Javascript objects”-like format: you have key-value pairs of data, you can mix different value types in the same objects, you can nest objects inside of each other, you can have arrays…

Adding a save/load system to (some of) our Scriptable Objects

Looking at this StackOverflow thread, for example, we can design a basic save/load system for our Scriptable Objects. We are going to create our own “JSON serialisable Scriptable Object parent class” and have all of our current Scriptable Objects classes that need updating and saving inherit from it. We don’t need to overload all of them with this specific behaviour: only the ones that may actually change during runtime should have this save/load feature (no need to supercharge objects with abilities they never use, right?).

In our game, at the moment, only game parameters can be modified at runtime. So let’s proceed in two steps:

  • first, we’ll create a new class called JSONSerializableScriptableObject that implements this save/load system and is a child class of the Unity built-in ScriptableObject class
  • then, we’ll simply have the GameParameters class inherit from JSONSerializableScriptableObject instead of the basic ScriptableObject class

Since the GameGlobalParameters and GameSoundParameters classes both inherit from the GameParameters class, they’ll automatically benefit from the new feature.

Serialising our data and saving it to the disk

In Unity, JSON serialisation is pretty easy to do thanks to a built-in tool, the JSONUtility class. This class lets us directly write or read Unity objects to and from JSON files. During the write, it will properly export all required data; and during the load, it will fill everything in the object’s fields with the data it finds (you can ask it to merge the contents with the default values or completely overwrite the object).

Using it is as simple as calling ToJson() and FromJson() on the object we want to serialise.

But wait – where are we going to store the data? How can we get a somewhat “robust” data path that works both during our tests and after the game build? Once again, Unity has got us covered with the built-in Application.dataPersistentPath. As explained in the docs, this variable is different depending on your OS but it is “specified” enough so that the application can consistently access the same data path, even after an update. This means that the project in itself has a unique id that is included in the path and insures the reference remains even after releases. So even if you make a patch and ship to your players, the game will have access to the previous data and the users won’t lose anything 🙂

After this short introduction, here is the script to put in a new C# file, JSONSerializableScriptableObject.cs:

Besides applying what we talked about before, this snippet of code also contains several things that are worth discussing:

  • first of all, I’ve decided to use a subfolder for all my Scriptable Objects so that, if someone takes a look at the game data on disk, they won’t get a big pile of files at the root of the application’s data path; this subfolder is defined as a static string on our JSONSerializableScriptableObject class so that each instance of this class (or of derived classes) can access it directly, and there is only one copy in memory
  • in both the saving and the loading logic, we check the paths to the directories and the files we access; if we are writing data and the paths are incomplete, we create all the required elements before writing to them – if we are reading and some paths are incomplete, we ignore this data and revert to the default settings
  • when creating our file, we need to be careful to release the lock on the file stream after we’ve created it – this is done using the Dispose() method of our File object. By default, C# retains a handle on the stream so we can write to it more easily. But here, we are using the WriteAllText function that acquires a lock on the File object too when it starts its process; so if we don’t dispose of the File first, we will have a “sharing violation” error.

In the GameParameters script, we just have to change the base class:

Calling the serialiser when the game starts and ends

Remember our old DataHandler script? The one that we said so long ago would help us save and load the player’s data while he/she’s playing? Well, it’s time to update it! 😉

We need to add two bits of logic: in the LoadGameData(), we should get the game parameters JSON data if it exists (else the Scriptable Objects will use their default value); and then, we should create a new function called SaveGameData() that is called whenever we quit the game.

The loading and saving steps simply use the functions we just created (that are inherited from the parent classes) on each of the game parameters resources we can list in the given directory.

And thankfully, Unity provides us with an easy way to access the “someone quit the application” event: the OnApplicationQuit() callback. It’s called by Unity whenever you stop the game (either in the editor or in production build). Let’s implement it in our GameManager script – it just forwards the call to the DataHandler to have the game parameters saved on disk:

That’s great! If it’s our first time around playing the game, we’ll get the default “factory settings”, i.e. the values that the developers shipped in their build game for the Scriptable Objects; but if we restart the game, we’ll get back all the options we modified during our last play session instead, they will overwrite the defaults.

You can check this out by doing the following (in the Unity editor):

  • run the game a first time: you should get the default Scriptable Objects values that you set in the custom editor by hand
  • now, modify some of the exposed settings in the in-game settings panel
  • and quit the preview runtime
  • because you’re in the editor, the Scriptable Objects will have automatically retain the changes: to simulate a build setup, restore them to their default value
  • finally, restart the game a second time – since there is data to load from the disk, the settings will have the updated values instead of the default ones

Fixing it for the builds

There is, however, a problem with our current setup. The save/load system works great… but the data in dev and production modes are actually incompatible! More specifically, the way other assets are referenced in our Scriptable Objects is different between the editor and the build game.

If you try and build the game as is, when you run it, the screen will freeze at the very beginning, before the initial building is placed. If you build it with the “Development build” option on, you’ll see in the console that there is a null reference on your initial building. This is because the build game is trying to read the data that we saved from the editor previously. Now, stop the game – the build game will overwrite the data in the application’s data folder. If you restart your preview runtime in the editor, you’ll get this null reference again, this time because the editor is trying to read the data saved from the build game and crashing. You can see in the Inspector that the initialBuilding variable of your GameGlobalParameters asset is set to “None”.

So – what’s going on?

If you take a look at the data saved by your game when it is run in the editor, you’ll see something like this:

The assets that you dragged in the Inspector to the public fields of your Scriptable Object are stored by their instanceIDs. This is a unique id that Unity assigns the object in your project to track them and link them throughout the program.

On the other hand, in a build game, you’d get (for the same object) something like:

Most of the data is the same but here, the assets are referenced using the m_FileID and m_PathID keys.

In other words, the file written by the editor cannot be read by the build game, and vice-versa. Even worse, the files will be “corrupted” in the sense that they will create bugs in the editor or the build game, and further intensify whenever we quit one or the other!

There is a basic solution to avoid this issue altogether: save the dev and prod data in two different folders. In the above snippet, we saw that we had a _scriptableObjectsDataDirectory static variable that defines the subfolder we store the serialised assets in. So what we want to do is to set this variable to a different value if we’re running the game in the editor or in build mode.

This is fairly straight-forward to implement using Unity’s preprocessing directives. In C#, the preprocessor directives look a bit like the C/C++ macros, but they are used only for conditional compilation. They are interpreted by the program at compile time and they allow us to insert either one bit of code or the other in the final software depending on some environment condition.

Note: for more info on using preprocessor directives in C#, you can check out a recent article I wrote on this topic 😉

It will be clearer with the example at hand; here is the updated code:

As you can see, we are adding 3 statements: #if UNITY_EDITOR, #else and #endif. The sharp sign # at the beginning of the line indicates that this line is a preprocessor directive. Then, we have a regular if/else logic that will execute one branch or the other depending on the value of the test. In our case, we are using UNITY_EDITOR for the conditional – this is a built-in value that Unity automatically computes and gives us to say whether the code is currently running in the editor or not.

The important thing to remember is that preprocessing directives do not appear in the compiled code! There are interpreted earlier on and are directly replaced or “executed” to modify the final output. If we were to take a look at the interpreted code of our game (i.e. the intermediate version, before it is compiled down to binary code), we would have two versions:

  • when it’s interpreted for the Unity editor:
  • all the other cases, in particular when it’s interpreted for the production build:

With this little trick, we have now separated the dev from the prod data and there is no issue of null references anymore!

If you want to completely disable the saving of the data while in dev mode, you can use the same preprocessor directive in the GameManager script:

This is probably a good idea, otherwise whatever changes you make to your game parameters assets in the editor could be overridden by previously saved game data!… 🙂

Final fix: getting our sound parameters loaded up properly

Another problem we have currently is that Unity audio mixer snapshots cannot be updated via scripting – so, for now, we have two possible states:

  • either we haven’t changed the sound parameters from the in-game settings panel and the audio snapshots are still working fine
  • or we have changed our volumes from the settings panel and this is overriding the snapshots, so the sounds won’t be affected anymore when going in or out of the UI panel

Not great, right?

Plus, regardless of this issue, the sound parameters we’ve stored in our JSON data aren’t initially loaded by our game!

I wanted to introduce the notion of audio mixers last time because they’re a really great tool for quick sound effects swapping in a basic sound context. But they are not really adapted to our setup. Here, to fix this, we’ll have to get rid of the snapshots altogether and re-mimic their transition function using coroutines:

It’s pretty similar to things we’ve done in previous tutorials – just make sure to use the soundParameters variable so that the from and to values in the coroutines are computed according to the player’s sound settings (even if he/she just changed them in the in-game settings panel).

Be aware that we’re losing the slight “low-pass” filter effect because I didn’t want to supercharge this part with details and long updates, but you could definitely expose this filter as a parameter on your masterMixer as well and have it be modified in the _TransitioningVolume() coroutine (probably by separating this function in two for the music and the SFX volumes respectively).

Note: I’ve also taken this opportunity to fix a little bug – in our _OnUpdateSfxVolume() callback, we want to cancel the SFX volume update if we’re currently in the “paused” state because in this state, all SFX should be completely silent 🙂

To sum up

At that point, we’ve successfully managed to save our data both in development and production mode. This data is saved as JSON files inside of the folder matching the application’s persistent data path (which exact value depends on the computer’s OS).

While we’re in the editor, the changes we make to our game parameters during runtime are directly applied to the Scriptable Objects, and they are saved to a subfolder of the app’s data path called ScriptableObjects_Dev/. Or the data saving step can be ignored, if we add the right preprocessing directive to our GameManager.

While we’re in a build of our game, the default data is the value the Scriptable Objects had when the build was made, and any modifications are saved to another subfolder of the app’s data path: ScriptableObjects/.

If we re-run the game in any of those two modes, the data from the previous session is restored if available, else the default values are used.

Important note: from that point on, if you have some null references when you start a build version of your game, it might be because of incorrect values in the on-disk data (for example if you ran it once with a little bug somewhere before it was fixed). In that case, make sure to delete the JSON files to start from a blank slate – it should fix the issue 🙂

Of course, we are not done yet with saving the player’s data! For example, we still need to save the current state of the game map with all the buildings and other specific changes the player might have done during the play session. But next time, we’ll work on another important part of our RTS: implementing a system of players and adding units ownership!

Leave a Reply

Your email address will not be published. Required fields are marked *