Hi, this is the second in my tutorial series, and todays topic is saving and loading of game data. So, Xenomorph is an RPG game at heart, and thus has a number of player stuff that you would expect in a typical RPG game… stats, items, crafting etc. All of this data needs to be saved out for the player to be able to save and load, but that’s not all there is to it, there is also data about the game too. For example, has this door been unlocked, have you found this unique item, is this wall destroyed etc. Now, Unity3D has a built-in system for saving data that is super simple to use, and that is PlayerPrefs, but this has some serious limitations.
- PlayerPrefs only supports the following types of variable (float, int and string).
- PlayerPrefs saves to the registry (certainly on PC) and its in plain text, so very easy to be changed by the end-user
So this is not a solution for saving out player data, as you certainly do not want a plain text entry for “Player Cash” and have the player change it to whatever they want 😉 Now there is an excellent system for encrypting the PlayerPrefs which you can find here, I have used this before for very simple games where a full-blown save and load system is not required, but again, it’s not a solution that will be useable for a sizeable game.
So in light of this, I decided to create my own system for Xenomorph. Now again, my usual disclaimer applies, I have written a system that works for me and the game I am making, it may not be the perfect or best practise approach, but it does work, so if you can use parts or all of this in your own game, then that is awesome 🙂
Ok, so there were a few considerations that I wanted make sure that I fully covered in my approach.
- Save and load must be a file written out to local storage
- The file must be fully encrypted
- The save and load system must support not only the players stats, but the states of various objects in the game
With that in mind, here is how I approached the task.
Saving the player data out is actually rather simple, as you are likely to only have one script that controls the player stats, lets say for example you have a playerManager script that has stats like health, ammo and gold. As you only have one instance of this script in the game, it’s very easy to implement this approach, but what about objects that share scripts? For example, lets say you have a doorManager script, that manages whether or door is locked or unlocked. In one level in your game, you could have 50 instances of that script, and therefore you need a way to track whether or not a specific door has been unlocked, so I needed to create a way to reference the target script in the save data. This system also needed to be scaleable so that if I added more objects that needed to be tracked in the save data, it was easy to add more.
Ok, so now we get down to how I actually wrote the save system. First we need to make a save game manager that will handle all of this for us. The save game manager will be responsible for checking if a save exists, loading saves, deleting saves and writing save data.
So, first things first we need to create a new script called SaveGameManager.cs to act as our save manager for the game.
We are going to need a bunch of namespaces to use here:
This allows us to create the lists we will be saving object references to, and the binary formatter we need to create the save file.
Now we are going to create a few things we need for this save game manager class to work:
The FileStream is the savedata file we are going to create, basically this will write all serializable data out to a binary file on the storage device in an encrypted format. This script on its own though wont really do anything, unless we give it some data to actually write out, so let’s do that now.
Lets assume for this tutorial we have a Player script that contains the following variables:
Ok, so we have this script on our player, but we need to tell the save game manager that we want to save out that data, and for that we need a new class for the player save data. So lets add that into our save game manager and create a new instance of it.
Note that we have now created a new class called SaveData, this is basically a class containing all the variables of the player that we wish to save out. Now you may at this point ask why I didn’t simply serialize the actual class used on the player? Well, you may have far more variables and references on that script that you may not want to save out, and possibly cannot be serialized. Note how I have added the system.serializable above the class, this tells Unity that we wish to be able to serialize this class to a file.
So, we have the save manager class and the data we wish to save out, but first we need to actually populate the save data. Thats nice and simple, we add a reference to the player script and a public method that reads from it.
So, now if we were to call the ReadPlayerData() method, we will populate the save data with the current player stats. So now that we have the stats, we need to write them out to a file. What we are going to do is create a new BinaryFormatter (needed to create the save file), then create a save file in a set location, then stream the serialized binary data from the SaveData class into that file. And finally, we then need to close the file that we have created, otherwise you are going to get errors later if you try to read or write the file again and Unity still has it open. Note that I have added the ReadPlayerData() method to this method, so that when we simply call SaveGame() it automatically reads the current player stats.
Whilst this might look complicated, it’s actually very simple, so let me break down into steps what we have done here:
- Read the current player stats and write them into our SaveData class.
- Created a new binary formatter for Unity to use to serialize the data to our save file.
- Used File.Create to create a blank file ready for the data
- Used the binary formatter to send the SaveData class variables to the file
- Closed the file
You will note the file creation line I had to give it a filepath and name, If you use Application.persistentDataPath, this will use Unity’s default location to save data. On PC that is C:/”UserName”/AppData/LocalLow/”YourCompany”/ I then gave it a filename, I used save.dat to make it easy to recognise, but you can call it whatever you want… buscuit.tin if you fancied it 😉
Thats it, we have now created a method to save data to a file, but now we also need a method to load the data, and also then make write the player stats from the save file. Fortunately, this is nice and simple too, we just need another method to write the data, and another method to find the save file and read from it back into the SaveData class.
So lets break down our loading process too:
- Create a new binary formatter
- Instead of creating the file, we are now using File.Open to load the file, specifying the exact same location and filename as before
- Using the binary formatter to de-serialize the data that we previously serialized into a file.
- Close the file
- Write the variables back to the player script using the WritePlayerData() method
Here is our final completed script:
So there you have it, a simple and secure way to save and load player stats. This should be relatively simple to put into any existing project now that you understand the fundamentals of the process, but lets look at expanding this further to multiple scripts in the scene as we mentioned before.
Expanding the design
Ok, so now lets look at expanding the script out to our door example I gave at the beginning. So, you have a door script that perhaps has an enum that lists its current state, something like this:
Now, if you had say 50 of these doors in your scene, they could all have completely different states and you want them to persist when the player changes back and forth between levels, or reloads their savegame. In order to do this, we must give each door a unique reference, and they must have a default state applied on game start. You will need to create a string called uniqueID that will hold the unique reference to this object, and give your script a default door state. For example :
You can choose whatever method you like to set the uniqueID, either manually type door1, dooor2 etc in the inspector… personally, I wrote an editor script that uses System.GUID to give them a unique GUID string to do this all for me.
So, this assumes that in our example, all doors are initially locked. That means that whenever a player unlocks a door, we need to tell the save data that the door with this uniqueID is now unlocked. To do that, we need to create a list in our savedata to hold the uniqueID’s of every unlocked door and a method to add our door to that list.
So, now we have added a new list of strings to our savedata for the doors that are unlocked, and all we need to do in the game is on our door script, when the player unlocks the door, simply call the AddUnlockedDoor() method and pass in the uniqueID of the door. This will add that door to the saveData, but we still need a way to setup all the doors in the scene on load, and the easiest way to do that is in the Start() function of the save game manager, and create a new method to find all the doors in the scene, see if their uniqueID is in our savedata list, and set the door to unlocked if that is true.
Lets assume that our door script is called DoorManager for this example:
So all we are doing here, is creating an array of DoorManagers, finding all of those in the current scene, and iterating through the list of unlocked doors to see if any of the uniqueID’s match in the scene, and if they do, unlocking that door. Now you just call this from the start function of the save game manager and you have a way now of saving that door state to a file.
You may also want a way to remove the unlocked door should the player lock it again, and its as simple as creating another method for this and calling it from the door script where needed:
We must finally now check to see if a file is present when the game starts, and if there is one, then load the savegame, setup the player and then unlock all the relevant doors.
In order to check the save file, we can use File.Exists and pass it the location and name:
So now we have a method of checking to see if the save exists, so we simply change the start function to check for the save, and if it is present, then load the game and unlock the doors, and here is our final code:
So, whilst I appreciate this example only covers a tiny amount of data, you can use this basis to extend this system to handle all types of data you may have in your game. My savedata manager is now large and complex for Xenomorph, but it still is based on these simple methods and principles.
I hope this has helped you in some way, please feel free to comment and let me know if this has been helpful, or if I have made some mistakes or typos here that prevent you from following it!