Making a RTS game #46: Saving our game scene data… (Unity/C#)

Today, let’s continue our RTS and discuss serialisation for our game scene data!

This article is also available on Medium.

A couple of episodes ago, we prepared a basic main menu to create new game sessions, and we now have a multi-scene workflow with cross-scene data.

But so far, we haven’t discussed how to reload a game once it’s been created!

So, today, we are going to see how to store the important data from our game scene into binary files, just like we did for our game parameters a while ago, so that we can keep track of the current state of our various game sessions. Then, next week, we’ll work on the counterpart and talk about loading back this data from the main menu, with a new “load game” panel.

Note: and afterwards, we’ll see in a bonus interlude how to take advantage of our map minimap screenshot tool to show the games’ updated minimaps 🙂

A quick peek at our save mechanism

Earlier in this series, we saw how to use binary serialisation to store our Scriptable Objects. We discussed how to extract the proper info from our objects, how to convert them to “binary-formattable” content and how to write short binary files from there.

We dived pretty deep into C# reflection and discovered how we can create agnostic functions that work on various objects, taking into account inheritance, and do some introspection on the instances to react at runtime. In particular, we created the BinarySerializable and BinarySerializableData util classes to quickly turn our objects into binary-formattable things.

In this tutorial, we are going to rely on the same mechanics and create some basic C# classes to feed a binary formatter. Then, we’ll simply instantiate these classes when need be and call our serialisation routine.

Ready? Let’s go! 🙂

Preparing the data classes

A little foreword

Because I don’t want to overload this article will super-long snippets, I won’t necessarily store all the info about everything in my game scene; for example, I won’t remember the exact position of the sun. So we won’t be able to get back the exact same scene when we load back. But I will focus on storing (and later on restoring) the most important stuff, and the complex data: namely, the current resources of each player and the units they created, in their current state.

All this to say: don’t be surprised if I’m not saving hundreds of tiny details – it’s more about conveying the general idea, and you should feel free to adapt/extend these data classes to your liking to store more or less info on your game sessions 🙂

Updating our data directory

First things first: let’s change our data paths a bit to get our Scriptable Objects and our game data binary files in the same place, but with a logical name!

At the moment, we defined a private static string in our BinarySerializableScriptableObject class, _scriptableObjectsDataDirectory, that we use to compute our load and save paths when we process our serialisable Scriptable Objects.

What I want to do instead is declare a public static string in my BinarySerializable class that will be used both for Scriptable Objects and other simple C# classes that need serialisation:

Then, I’ll update my BinarySerializableScriptableObject class to make use of it:

Adding save and load to the BinarySerializable class

While I’m at it, I’ll also prepare very basic functions in my BinarySerializable to load and save my data (we won’t use the Load() method in this episode, but we can already prepare it!):

The functions simply open streams to a file in the session’s directory (i.e. the subfolder with the unique game UID inside the data directory) and uses the C# binary formatter to convert the object from or to the binary format.

Note: we’ll talk more about it next time but, of course, in the Load() function, we simply keep the defaults if the file doesn’t exist yet.

Now, we need to design our game data classes and make sure that they can be serialised… or create custom serialisers for the non-serialisable types!

Designing the data classes

My game data storage will work with three levels of classes:

  1. the GamePlayerUnitData class: the low-level, that stores info about a specific unit for one player in the game (either a Building or a Character) – this info is a mix of “general” data such as the uid or the unit type, and more “contextual” data such as the current amount of healthpoints of the unit
  2. the GamePlayerData class: the mid-level, that stores info about each player in the game, like the current amount of resources and all the units on the map for this player (as an array of GamePlayerUnitData)
  3. the GameData class: the root level, that stores the overall session data and in particular an array of GamePlayerData

I don’t want to bother too much with complex data types, so for all that is resource-related, I won’t store dictionaries like in my main game logic. Rather, I’ll define an ordered list of keys for my in-game resources in my Globals script:

And now, I’ll be able to use it as reference and I can only store the current amount of each resource as an integer (completely ignoring the resource type, because I’ll assume the index of my value can be matched uniquely to one resource type thanks to my friend-variable GAME_RESOURCE_KEYS).

That’s why it is important that this array is ordered and remains the same: I need to be sure that the value in the first cell of my values array always corresponds to the same resource type when I save or load my data. And we can’t rely on the order we gave our GAME_RESOURCES sub-dictionaries keys in, because C# dictionaries are non-ordered data collections that do not guarantee the order of the keys!

So, in a nutshell, in my case, my resource-related arrays of ints will always store (in this exact order): the current amount of gold, the current amount of wood and the current amount of stone:

With all that ready, we can now code our three data classes in a new script, GameData.cs:

“Improving” our data classes and our serialisers

Of course, we want the root level (the GameData class) to be serialisable, so we’ll want it to inherit from our BinarySerializable class; but we also need to make sure that the GamePlayerUnitData inherits from it, because it contains some fields that require special treatment (like the Vector3 position), and we have to designate the different levels as serialisable thanks to the [System.Serializable] attribute:

Then, we’ll want to redefine an “intermediary” Save() function that is based on the one from the BinarySerializable class but auto-fills the file path:

Just a quick note though: because we don’t really want to serialise this DATA_FILE_PATH field, we should make sure that we ignore static fields in our serialisation/deserialisation process – so let’s do early returns in the methods of our BinarySerializable if need be:

Also, remember that, at the moment, our serialisers:

  • don’t know GamePlayerData and GamePlayerUnityData are serialisable classes
  • can’t handle Vector3 typed-variables

If you try and run serialisation with this code, you’ll get various errors from the sub-classes, and from the position fields of the GamePlayerUnitData instances.

To fix this, we need to update our BinarySerializableData script so that it considers our two classes as serialisable and that, in the _Serialize() and Deserialize() methods, it transforms Vector3 to/from arrays of floats (just like Colors, except it has only three components):

Calling the serialiser

Boosting our DataHandler

Now that we can create objects to represent our current game state, it’s time to actually use it somewhere! I’m going to update my DataHandler class so that it can “parse” the scene and extract the relevant info from it, store it in a new GameData instance, and finally call its Save() method to actually run the serialisation process.

The overall process is the following:

Now, let’s take care of actually populating the GameData instance with our current game session state.

First, we’ll add the info on the resources for each player – remember we only need to store the current amount because we can use our GAME_RESOURCE_KEYS to get a fixed order:

Then, we’ll store the info on the units!

To make it easier to list all of the units for each player on the map, let’s first add a little static variable in our Unit class to “auto-register” the instances when they are created, by owner:

This auto-registration technique is a common dev pattern and it’s pretty useful whenever you want to easily refer to instances of a specific class later on – you’re sure you don’t forget any (since they take care of registering on their own) but you still benefit from the benefits of a static variable (no duplicates in memory and easy access from anywhere).

Note: but of course, just like Singletons, it only works for variables that are global to the entire scene! 😉

Now, we can quickly get the units for a given player using this dictionary, and so we’ll be able to create our GamePlayerUnitData objects, and insert them in the rest of the structure:

All that’s left to do is to fire this SaveGameData() function from somewhere in our scene!

Updating our UI

So, finally, let’s wrap this up by adding a basic menu panel in our GameScene. This panel will work similarly to the settings panel: we’ll toggle it on by clicking on a button in the top-right corner, and then toggle it back off by clicking on the “Resume” button.

I won’t detail all the UI objects in the scene – but you can check it out on Github! 🚀

Basically, I simply copied my settings panel and replaced the “Content” part. Inside, I put a vertical layout with a few buttons and then assigned these buttons new functions I declared in my UIManager – I’ve also added some references and init logic:

The ToggleMainMenuPanel() function is called by my brand new “Menu” button at the right of the top bar:

The four button functions are pretty self-explanatory – now, we just need to fill them accordingly (I’ll leave the LoadGame empty for now):

If you try to create a new game session and pop the menu panel open, then you’ll be able to “click” the “Save” button to create a new .data file in your application’s data folder…

Conclusion

In this episode, we’ve gone back to doing a bit of binary serialisation and we’ve seen how to store the relevant data from our current game session.

Next time, we’ll continue working on our game data and see how to deserialise these neat binary files, so that we can actually reload our game sessions! We’ll also update our main menu scene to add a “load game” panel to easily access all the existing saves… 🙂

Leave a Reply

Your email address will not be published.