Saving and Loading in Unity3D using C# Tutorial

//Saving and Loading in Unity3D using C# Tutorial

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.

  1. PlayerPrefs only supports the following types of variable (float, int and string).
  2. 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 🙂

Considerations

Ok, so there were a few considerations that I wanted make sure that I fully covered in my approach.

  1. Save and load must be a file written out to local storage
  2. The file must be fully encrypted
  3. 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.

Approach

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.

Design

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:

using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

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:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

public class SaveGameManager : MonoBehaviour
{
    public FileStream saveFile;
}

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:

public string name;
public int health;
public int ammo;
public int gold;

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.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

[System.Serializable]
public class SaveData
{
    public string name;
    public int health;
    public int ammo;
    public int gold;
}

public class SaveGameManager : MonoBehaviour
{
    public FileStream saveFile;
    public SaveData saveData = new SaveData();
}

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.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

[System.Serializable]
public class SaveData
{
    public string name;
    public int health;
    public int ammo;
    public int gold;
}

public class SaveGameManager : MonoBehaviour
{
    public FileStream saveFile;
    public SaveData saveData = new SaveData();
    PlayerManager playerScript;
}

void Start()
{
    playerScript = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();
}

public void ReadPlayerData()
{
    saveData.name = playerScript.name;
    saveData.health = playerScript.health;
    saveData.ammo = playerScript.ammo;
    saveData.gold = playerScript.gold;
}

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.

public void SaveGame()
{
    ReadPlayerData();
    BinaryFormatter bf = new BinaryFormatter();
    saveFile = File.Create(Application.persistentDataPath + "/save.dat");
    bf.Serialize(saveFile, saveData);
    saveFile.Close();
    Debug.Log("Save Created :" + saveFile.Name);
}

Whilst this might look complicated, it’s actually very simple, so let me break down into steps what we have done here:

  1. Read the current player stats and write them into our SaveData class.
  2. Created a new binary formatter for Unity to use to serialize the data to our save file.
  3. Used File.Create to create a blank file ready for the data
  4. Used the binary formatter to send the SaveData class variables to the file
  5. 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.

public void LoadGame()
{
    BinaryFormatter bf = new BinaryFormatter();
    saveFile = File.Open(Application.persistentDataPath + "/save.dat", FileMode.Open);
    saveData = (SaveData)bf.Deserialize(saveFile);
    saveFile.Close();
    WritePlayerData();
    Debug.Log("Save Loaded :" + saveFile.Name);
}

public void WritePlayerData()
{
    playerScript.name = saveData.name;
    playerScript.health = saveData.health;
    playerScript.ammo = saveData.ammo;
    playerScript.gold = saveData.gold;
}

So lets break down our loading process too:

  1. Create a new binary formatter
  2. Instead of creating the file, we are now using File.Open to load the file, specifying the exact same location and filename as before
  3. Using the binary formatter to de-serialize the data that we previously serialized into a file.
  4. Close the file
  5. Write the variables back to the player script using the WritePlayerData() method

Here is our final completed script:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

[System.Serializable]
public class SaveData
{
    public string name;
    public int health;
    public int ammo;
    public int gold;
}

public class SaveGameManager : MonoBehaviour
{
    public FileStream saveFile;
    public SaveData saveData = new SaveData();
    PlayerManager playerScript;
}

void Start()
{
    playerScript = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();
}

public void ReadPlayerData()
{
    saveData.name = playerScript.name;
    saveData.health = playerScript.health;
    saveData.ammo = playerScript.ammo;
    saveData.gold = playerScript.gold;
}

public void SaveGame()
{
    ReadPlayerData();
    BinaryFormatter bf = new BinaryFormatter();
    saveFile = File.Create(Application.persistentDataPath + "/save.dat");
    bf.Serialize(saveFile, saveData);
    saveFile.Close();
    Debug.Log("Save Created :" + saveFile.Name);
}

public void LoadGame()
{
    BinaryFormatter bf = new BinaryFormatter();
    saveFile = File.Open(Application.persistentDataPath + "/save.dat", FileMode.Open);
    saveData = (SaveData)bf.Deserialize(saveFile);
    saveFile.Close();
    WritePlayerData();
    Debug.Log("Save Loaded :" + saveFile.Name);
}

public void WritePlayerData()
{
    playerScript.name = saveData.name;
    playerScript.health = saveData.health;
    playerScript.ammo = saveData.ammo;
    playerScript.gold = saveData.gold;
}

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:

public enum DoorState { Locked, Unlocked }
public DoorState currentState;

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 :

public enum DoorState { Locked, Unlocked }
public DoorState currentState = DoorState.Locked;
public string uniqueID;

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.

[System.Serializable]
public class SaveData
{
    public string name;
    public int health;
    public int ammo;
    public int gold;

    public List<string> doorsUnlocked;
}

public void AddUnlockedDoor(string id)
{
    saveData.doorsUnlocked.Add(id);
}

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:

void UnlockAllDoors()
{
    DoorManager[] doorScripts = GameObject.FindObjectsOfType<DoorManager>();

    for (int i = 0; i < saveData.doorsUnlocked.Count; i++)
    {
        foreach (DoorManager door in doorScripts)
        {
            if (door.uniqueID == saveData.doorsUnlocked[i])
            {
                door.currentState = door.DoorState.Unlocked;
            }
        }
    }
}

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:

public void RemoveUnlockedDoor(string id)
{
    saveData.doorsUnlocked.Remove(id);
}

Finishing up

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:

void CheckSave()
{
    if (File.Exists(Application.persistentDataPath + "/save.dat"))
    {
        Debug.Log("Save File Found");
    }
    else
    {
        Debug.Log("No save file found");
    }
}

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:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

[System.Serializable]
public class SaveData
{
    public string name;
    public int health;
    public int ammo;
    public int gold;

    public List<string> doorsUnlocked;
}

public class SaveGameManager : MonoBehaviour
{
    public FileStream saveFile;
    public SaveData saveData = new SaveData();
    PlayerManager playerScript;
	bool fileExists;
}

void Start()
{
    playerScript = GameObject.FindGameObjectWithTag("Player").GetComponent<Player>();
	CheckSave();
	if(fileExists)
	{
		LoadGame();
		UnlockAllDoors();
	}
}

public void ReadPlayerData()
{
    saveData.name = playerScript.name;
    saveData.health = playerScript.health;
    saveData.ammo = playerScript.ammo;
    saveData.gold = playerScript.gold;
}

public void SaveGame()
{
    ReadPlayerData();
    BinaryFormatter bf = new BinaryFormatter();
    saveFile = File.Create(Application.persistentDataPath + "/save.dat");
    bf.Serialize(saveFile, saveData);
    saveFile.Close();
    Debug.Log("Save Created :" + saveFile.Name);
}

public void LoadGame()
{
    BinaryFormatter bf = new BinaryFormatter();
    saveFile = File.Open(Application.persistentDataPath + "/save.dat", FileMode.Open);
    saveData = (SaveData)bf.Deserialize(saveFile);
    saveFile.Close();
    WritePlayerData();
    Debug.Log("Save Loaded :" + saveFile.Name);
}

public void WritePlayerData()
{
    playerScript.name = saveData.name;
    playerScript.health = saveData.health;
    playerScript.ammo = saveData.ammo;
    playerScript.gold = saveData.gold;
}

public void AddUnlockedDoor(string id)
{
    saveData.doorsUnlocked.Add(id);
}

public void RemoveUnlockedDoor(string id)
{
    saveData.doorsUnlocked.Remove(id);
}

void UnlockAllDoors()
{
    DoorManager[] doorScripts = GameObject.FindObjectsOfType<DoorManager>();

    for (int i = 0; i < saveData.doorsUnlocked.Count; i++)
    {
        foreach (DoorManager door in doorScripts)
        {
            if (door.uniqueID == saveData.doorsUnlocked[i])
            {
                door.currentState = door.DoorState.Unlocked;
            }
        }
    }
}

void CheckSave()
{
    if (File.Exists(Application.persistentDataPath + "/save.dat"))
    {
        Debug.Log("Save File Found");
		fileExists = true;
    }
    else
    {
		fileExists = false;
        Debug.Log("No save file found");
    }
}

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!

 

James Stone
Indie Game Dev at NerdRage Studios
Indie Game Dev @ NerdRage Studios Ltd.

Self taught game developer, left the IT industry after 20 years in corporate IT management and now out on his own, making games like Xenomorph, Eggstraction and VRbloX. From the UK, currently living just outside Shanghai in China.

Huge retro gaming nerd and collector of stuff which was rubbish in the day, but thanks to eBay and feelings of nostalgia, is now considered worth something ;)

@EpicNerdRage
2016-12-30T04:01:58+00:00

7 Comments

  1. Nimrod April 19, 2017 at 6:44 am - Reply

    Great tutorial! Could you share how to handle backward compatibility when you want to remove or rename serialized variable? From my experience, you can’t and that’s the main con if this approach.
    I.e. If you rename ammo variable to bullets, load function will fair because it can’t deserialize the saved data.
    Thanks!

    • James Stone June 9, 2017 at 11:39 am - Reply

      Hi, sorry didnt realise I had comments waiting in moderation, sorry!

      You are right, with this approach there is no way to get past this, so if you do rename your variables, you will have to delete the save file before playing again, otherwise you will likely crash the game. backwards compatability can be done, but you would have to program in a way to look for all old variable names… likely just easier to delete the save and move on. Not what you want to do if you game is already released though!

      • Alan July 12, 2018 at 10:45 am - Reply

        Hi – I believe you can rename your variables by adding the [FormerlySerializedAs] attribute before your variable declaration.

        See this unity blog post https://blogs.unity3d.com/2015/02/03/renaming-serialized-fields/

        Sorry to necropost! This is a very useful article which still appear high in google searches so I just wanted to add this for anyone else finding this page.

  2. tomas June 7, 2017 at 8:20 am - Reply

    How could I do this with a scene?

    • James Stone June 9, 2017 at 11:41 am - Reply

      Hi, cannot be done by default as Unity specific things like GameObjects, Transforms, Audio Sources…. you know all the Unity stuff is not able to be serialized.

      There are plugins in the asset store that allow you to do this though.

      If you wanted to have a go, you could serailise things like Vector3… you just need to save out each axis to a float, and then load back into the Vector3 again, or save names out to strings. Not ideal and unlikely to be a useable solution, but its the only way it will work without a plugin.

  3. Scott June 30, 2017 at 10:28 am - Reply

    Hello,

    This doesn’t seem to work, under void Start I get the error “A namespace cannot directly contain members such as fields or methods. Same goes for all the other voids.

    • James Stone August 13, 2017 at 4:04 am - Reply

      Hi, it sounds like you have place the voids methods outside of the namespace, check your syntax and ensure that they are inside the defined class.

Leave A Comment