Making a RTS game #47: … and loading back our game scene data! (Unity/C#)

Let’s keep working on our game scene and see how to deserialise the previous session data on load!

This article is also available on Medium.

Last week, we talked about binary serialisation and we saw how to store info on our current game session in binary files. We focused on how to go from C# objects to binary-formattable data. Today, it’s time to do the reverse: we will implement our deserialisation process to load the data back and restore our previous state!

A note about the reload logic

Before we dive into the actual implementation, we just need to think for a while about execution order and dependencies.

The overall idea is obviously to:

  • check the game current UID
  • load the matching data file if it exists (or else ignore the load part and use the defaults)
  • use this info to setup the various scene parameters

The tricky thing, however, is that we shouldn’t apply the reloaded settings “too soon”, otherwise they will simply get overridden by the rest of the initialisation logic. To avoid this, we’ll need to store the loaded data at the beginning and parse it later, at the end of the init sequence.

Ok, with that said – let’s actually get to coding! 🙂

Working on our data classes

The first thing we have to do is update our various game data classes so that they properly deserialise the data from binary.

For now, if you try to call the deserialisation process on your session saved data, you’ll get an error saying that you don’t have the proper constructor for deserialising objects of the GameData type:

This is because for now, we are inheriting from the BinarySerializable but the code can’t spot all the required interfaces. Let’s fix this!

To begin with, let’s extract the deserialisation logic to a function in BinarySerializable so that we can call it from the child classes:

Then, we’ll just need to add the constructor and deserialisation interface explicitly in the GameData and GamePlayerUnitData classes, like this:

At that point, we can override the Load() method of the GameData to auto-fill the path as well as a static instance, so we can retrieve it later on:

Now, we can easily reload the game session data based on the current game UID (by checking the matching data folder), and we also keep track of it for future parsing.

Calling the deserialiser… at the right time!

Our classes are ready for deserialisation!

It’s time to update our DataHandler and to add a function to parse this data into actual settings or objects for our new game scene: DeserializeGameData().

The code is basically the counterpart of the SerializeGameData() method we coded up last time, and it just reads the data to set the resource values and the units of each player, and the camera position.

But there are some pretty important things:

  • the loading process should not conflict with the initialisation process
  • and we have to make sure the “auxiliary” data is properly computed (most notably when units level up, they normally set up the level up data for next time…)

To handle this, I’ve added a little boolean flag to various methods in my Unit and Building classes so they know if they are called from the reload process and skip some of the logic if need be:

Then, I’ve also added an if-check in the BuildingPlacer script so that it only creates start buildings on the spawnpoints if we are not loading back the data from a save:

And finally, I implemented the DeserializeGameData() function in the DataHandler and used these new boolean flags to create or set up the various units – for the unit’s level, I’m setting the level to the one below and then calling the LevelUp() method to fill the auxiliary data properly:

However, if you try to compile this code, you’ll still get several errors… because for now, there are some fields in our Unit and GameResource instances that we can’t set directly: for most of them, we only have getters!

To fix this, we simply have to modify the Unit and GameResource classes and also expose setters for these fields:

Finally, we have to make sure this data parsing is done at the end of the initialisation process. To control the init sequence accurately, we can rely on Unity’s script execution order settings. Basically, we are going to:

  1. turn our DataHandler into a MonoBehaviour and call the parser in its Start() method
  2. add the script to our “GAME” object in the GameScene, so it is run when the scene loads…
  3. … but also insure the DataHandler class is called after the others by putting it at the end of the execution order list

Pretty simple, all in all! First, here is the updated DataHandler to make it a MonoBehaviour:

Then, we can add it to the “GAME” object:

Finally, we can open our Project Settings window (from the Edit > Project Settings… menu), navigate to the “Script Execution Order” tab and add the DataHandler script at the bottom of the list:

Once you’re done setting your project’s scripts execution order, don’t forget to click the “Apply” settings so that they’re actually taken into account 😉

Now, everything’s in place for reloading a game session and restoring the saved data, but… we don’t really have any button to go back to a previous game session, at the moment! Let’s handle this in our main menu scene, by adding some new buttons and panels…

Creating the game load panel in the menu

To easily list and load game sessions from before, we’ll add a new panel in our main menu, and a “Load Game” button to show it:

As you can see, the load panel is pretty similar to the one for creating a new game. The difference is that instead of listing available maps, we list available game sessions, and that we can’t change the player parameters anymore.

For now, we will display the base minimap screenshot of the corresponding game’s map – but we’ll soon have a bonus interlude episode where we take advantage of our map minimap screenshot tool to show the games’ updated minimaps 🙂

The first step is therefore to get a list of all available game sessions. We will consider all game directories with a GameData.data inside and sort them in reverse chronological order (most recent saves are listed first); we’ll add this new function in our DataHandler class too:

Here, I’m using several neat features of C#:

  • I’m using the file tools to easily get the last modification time of my GameData.data file in the various map folders
  • I’m returning a list of tuples so that I can pack together the game folder path and the time it was last modified
  • I’m once again taking advantage of the Linq built-in package to easily filter and sort my list in various ways

Then, in the MainMenuManager, the logic for creating the loadable games panel is very similar to the one we had before for the “new game” panel. I’ll however refactor the script a little so that I only load the MapData Scriptable Objects once (in a new private variable _maps) and then use it for the creation of both panels:

I also use some C# utils to quickly display my modification times in the UI in a readable short-format.

The UI object I’m using is actually a copy of the “NewGamePanel”, except I’ve removed the players list from the “Details” sub-child (feel free to take a look at the Github for more details 🚀):

Don’t forget to disable it in the scene so it doesn’t show at first, and to assign all the necessary references in the MainMenuManager script slots 😉

Now that we’ve created each items in the left part of the load panel, we need to assign them a callback function, like we did for the list items in the new game panel.

In our case, “selecting a save” means remembering the game UID to load and updating the details info on the right; we can do all this with a code very similar to the one for the new game panel:

Finally, let’s implement our LoadGame() function to actually use this _selectedLoadGameUid and switch to the right map. The overall logic is quite straight-forward; we just need to make sure to update our CoreDataHandler class so we can pass it a game UID directly rather than have it compute it a new one:

All that’s left to do is assign this new LoadGame() function to our “Load Game” button at the bottom-right corner of the load panel, and we’re all set!

We can now create a new game, save it after creating and leveling up some units… and then reload it later, when we feel like playing again! 🙂

Conclusion

In the last two tutorials, we’ve seen how to use binary serialisation to store and reload our game scene data and re-use our previous sessions!

Of course, there are lots of improvements we could make to this system – for example, it would be nice to let the user pick the name of the save, or to allow for multiple saves in the game session (at the moment, it just overwrites the previous one)…

… but next time, we’ll already do a little addition to our load/save system and see how to use our minimap screenshot tool to actually show the updated minimap of the session in the menus, instead of the basic one 🙂

Leave a Reply

Your email address will not be published.