How To Do Secure Saving with Binary – Unity C#

This tutorial assumes you have a basic understanding of coding in C#, but don’t know how to do saving and loading.

With most games, you want the player to be able to save their progress, but it’s not obvious how to do that in Unity without a bit of help. I will show you how to easily and securely create a save system in Unity using binary files.

There are multiple ways to do saving and loading in Unity: Player Prefs (which is built in to Unity) and creating JSON or XML files are the most common. They are fine for small projects, but the data is easy for anyone with a text editor to read and modify, so they’re not secure. With binary files, the data is stored as 0’s and 1’s, so it’s much harder for someone to modify the data, but it’s easy for you to create the save system.

How it Works

The steps are:

  1. Store the information you want in a classscript
  2. Save all of the data you want as public variables
  3. Mark the class as serializable
  4. Set up another class to handle the actual saving and loading
  5. Use the binary formatter to convert the code into binary to save, and back again when you want to load
  6. The final code is at the end of the document, but I suggest you read the whole tutorial to understand it.

Binary is the computers native language, also known as machine code, a bunch of 1’s and 0’s. Think of like a switch, either on or off. A computer will read a series of 1’s and 0’s and translate that information into something that we would recognize, but it’s very hard for a human to read it.

If you want to save the level the player is up to, level 21, we would store that in the variable int level.  

Then we use something called a binary formatter which would take that information and turn it into 1’s and 0’s and save it as a file. Later, when we want to load that information, we use the binary formatter again and it would turn the 1’s and 0’s back into something we understand, in this case, we would then have access to int level, and it would tell us the player is up to level 21.

What It Can and Can’t Do

Because we’re converting information into the primitive format of binary, we are limited in the type of information we can save. We can’t save any Unity specific types of information and variables. For example, we can’t save Vector3. But we could, however, store an array of 3 floats that we can convert into a Vector3. Mainly we will be able to save primitive data types that are native to C#, stuff like:

  • ints and floats
  • bools
  • arrays[]
  • strings

Some of it you will find out with trial and error, but if something isn’t saving, it might not be able to be converted into binary.

Begin – Game Data Class

We shall create a small test project that will save and load a score and print it out as a string. We will use key presses to change the score, save the game, wipe the score, and to load the previous score.

Let’s get started. Create a new project in Unity. Make a new class, and call it Game Data. All of the data in this class will become the save file. You could have multiple files, but for the sake of this exercise, we’ll use one file.

Get rid of the Start() and Update() functions, you don’t need them, and it saves the computer checking them. Remove mono behaviour from the class declaration. You should be left with something like this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameData : MonoBehaviour
{
}

If we don’t remove MonoBehaviour, we can’t serialize the class.

Now you need to make it serializable – add the code in blue:

...

[System.Serializable]
public class GameData 
{
}

From now on, three dots means there is code I haven’t bothered repeating. If I’ve got black and blue code, blue means new code

So what does serializable mean? To serialize something means to convert it to binary. This makes it easy to do different things with that information – there are various uses. In our case, it makes it easy for us to save the data we want and make it non-human readable.

When we mark a class as serializable, we are saying we want to convert that class into binary at some point.

Now let’s add our int variable for the score and start it at 0.

public class GameData
{
   public int score = 0;
}

Later we will want to add to or subtract from the score, and add an option to reset the score to 0. Let’s add two simple functions to do that:

...

public void AddScore(int points)
{
   score += points;
}

public void ResetData()
{
   score = 0;
}

score += points; means we will add the variable points to the score. We don’t need to create a subtract function, because if points is negative, it will automatically subtract from the score. When you add a negative number, you subtract. So 1 + -1 = 0.

Here’s the full code for the class (repeated at the end of the tutorial):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class GameData
{
  public int score = 0;

  public void AddScore(int points)
  {
    score += points;
  }

  public void ResetData()
  {
    score = 0;
  }
}

Save System Class – Saving

Now make a new class and call it SaveSystem. This will handle the actual saving and loading, whereas our previous class just holds data.

Remove the Start() and Update() functions.
Remove using Sytem.Collections; 
Remove using System.Collection.Generic;
Add the library extension using System.IO;
Add the library extension using System.Runtime.Serialization.Formatters.Binary;

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

public class SaveSystem : Monobehaviour 
{ 
}

System.IO is a code library to handle files, ie, creating and opening files.

System.Runtime.Serialization.Formatters.Binary is a code library to handle binary conversions.

Add the following code to your class:

public class SaveSystem : MonoBehaviour
{
  string filePath;

  private void Awake()
  {
    filePath = Application.persistentDataPath + "/save.data";
  }
}

The new string variable, filePath, is where our file will be saved.

We then create a new Awake function, which is similar to Start. The difference is when Unity calls the functions. Awake will always be called before Start, and is only ever called once in the lifetime of an object. Awake is only called on active game objects, however it will still be called if the script attached to the game object is not active. Read more about Awake here.

In Awake we set where the file will be saved. We have to do it in Awake, because Unity won’t let us do it when we declare the variable in our class. We could have written:

 filePath = "C:/System/save.data";

But that won’t work on Mac or mobile.

Application.persistentDataPath lets Unity choose a place in the system directory for you that won’t suddenly change, regardless of what device the game is being played on. It also makes it harder for someone to track down where your save data is to modify it.

“/save.data” is the name of the file you are creating and your own custom file type. /save is the name of the file, data is the file type we created. Because it is a binary file, you can give it any file type you want and it won’t matter. We could have called it “/save.smile”.

The next thing we want to do is create a function for saving. Create a new function called SaveGame():

public void SaveGame(GameData saveData)
{
}

We need it to be public so we can call it from other scripts, and we’re going to pass in a GameData variable. This way, we’ll let other scripts populate our GameData file, and then when it’s time to save, we’ll just pass the information in through the function paramater.

Now add the following lines of code to the function:

public void SaveGame(GameData saveData)
{
   FileStream dataStream = new FileStream(filePath, FileMode.Create);

   BinaryFormatter converter = new BinaryFormatter();
   converter.Serialize(dataStream, saveData);

   dataStream.Close();
}

FileStream is a stream or a bunch of data that contains all of the data in a file. We can use a particular file stream to read and write from a file, and that data will be stored within the variable.

We create a new FileStream called dataStream. We pass in the filePath, which is where the file will be created. We then choose a FileMode, which is what we want to do with the file, in this instance, we use FileMode.Create to create a new file.

The BinaryFormatter, quite simply, is what converts our human readable code into binary. First we make a new one, called converter, and then we use it.

converter.Serialize(dataStream, saveData);

The above line of code is where the actual conversion happens. To serialize means to convert to binary. The Serialize() function writes data to the file through the binary conversion process. Then we pass in our empty file which is stored in dataStream, and then we pass in saveData, the data we stored in our class, to be saved to that file. Now the file stored in dataStream is no longer empty, and it is stored out of the game.

dataStream.Close();

We need to close off the file to say we’ve stopped using it, or we will get strange errors. The above line of code says that we’re done writing data to dataStream.

Next we need to be able to load saved data.

Save System Class – Loading

Create a new function called LoadGame() and add the following code:

public GameData LoadGame()
{
  if(File.Exists(filePath))
  {
    // File exists  
  }
  else
  {
    // File does not exist
  }
}

We add an if / else statement to check if there is a save file when we want to load the game. Otherwise, if we try loading the game and there is no save file, we will get strange errors.

File.Exists(filePath) will check to see if a file exists at the location given to it, in this case, filePath, and then it will return either true or false.

First, lets add the code for when the save file is found. Add the following code to your if statement:

if(File.Exists(filePath))
{
  // File exists 
  FileStream dataStream = new FileStream(filePath, FileMode.Open);

  BinaryFormatter converter = new BinaryFormatter();
  GameData saveData = converter.Deserialize(dataStream) as GameData;

  dataStream.Close();
  return saveData;
}

So first we make a new FileStream to contain the data for the file. This time, however, instead of FileMode.Create, we use FileMode.Open because we are opening an existing file. If there is a file there, dataStream will now contain all of that data.

We then make a new BinaryFormatter so that we can convert the binary save file back into human readable code.

Then we have to create a variable, saveData, which is a copy of our GameData class, to store all of the information.

 GameData saveData = converter.Deserialize(dataStream) as GameData;

We use the converter to deserialize the saved data in dataStream. To deserialize means to convert back from binary into human readable code. We have to say at the end:

as GameData;

This is to convert the binary file into a GameData class – otherwise Unity wouldn’t know what to do with the data, what kind of information it was, and you would get errors.

Finally we close off the data stream, because we are finished writing to the file, and then we return the saved data file we found. Return means that, whenever we use this function and find a save file, it will give us that save file so that we can use it. You can read more about returning from functions here.

Next , if there is no save file, we need to know that, so we will create a custom error message. Add the code in blue to your else statement:

if(File.Exists(filePath))
{
  // File exists 
  ...
}
else
{
  // File does not exist
  Debug.LogError("Save file not found in " + filePath);
  return null;
}

You might have used Debug.Log() before to write custom messages to the console. Debug.LogError() will show a custom error written in Red. In this case, we announce the save file wasn’t found, and then add the filePath to the error message, telling us where Unity is looking for the file.

We then return null because no save file was found – when a function has a return type that isn’t void, you have to always return something, in this case, null will do, because we found nothing.

We now need to make this class into a singleton. A singleton simply means that you can only have one copy of that class in your scene at any time. Your code will check to make sure there are no other copies of the class in the scene, and if there are, they will destroy themselves. Having multiple save systems in your scene would cause weird errors with saving and loading.

To create a singleton, we need to add a new variable to the start of our class called instance, and add some code to the Awake function:

public class SaveSystem : MonoBehaviour
{
  // Make this class a singleton / single instance
  static public SaveSystem instance;
  string filePath;

  private void Awake()
  {
    filePath = Application.persistentDataPath + "/save.data";

    // Check there are no other copies of this class in the scene
    if (instance == null)
    {
      instance = this;
    }
    else
    {
      Destroy(gameObject);
    }
  }
}

The terms instance and copy are interchangeable in this case. A single instance of an object in a scene is the same as saying there is a single copy of it in the scene. We create a new variable called instance to store a self reference, to say, “I exist.”

I will explain the term static in a minute.

Next we have an if / else statement to check if an instance or copy of this class does exist in the scene.

if (instance == null)

If instance is null, there is no copy of this class in the scene, so we will set instance to be a self reference. Now if a second copy of this class was made, it would go to the else statement.

If it goes to the else statement, it destroys itself. That is what a singleton is, leaving us with just one copy in the scene.

Now back to static. A static variable means there can only be one of this variable, it will always have the same value, no matter how many copies of the class there are. If it was an int set to 5, then every copy of the class would have that int set to 5. Changing one would change them all. There could still, however, be multiple instances of the class, so we use the if / else check to make sure that doesn’t happen.

A static variable also means that we can access it if we call the class through code without instantiating it, ie:

SaveSystem.instance.Save();

We can reference the instance variable directly because it is static, without creating a copy of the class in our code. You could not, however, do:

SaveSystem.filePath = "C:\hello.doc";

Becase filePath is not static, it can’t be accessed in that way. This will make more sense when we use the static variable later. There’s further reading on static here.

The simplest way to implement a singelton once created is to attach it to a game object in the scene, ideally something that is created at the start of the scene, or at the start of the game, and is never destroyed unless you no longer need it.

Here is the finished SaveSystem class (repeated again at the tutorials end):

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

public class SaveSystem : MonoBehaviour
{
  // Makes it a singleton / single instance
  static public SaveSystem instance;
  string filePath;

  private void Awake()
  {
    // Check there are no other instances of this class in the scene
    if (!instance)
    {
      instance = this;
    }
    else
    {
      Destroy(gameObject);
    }

    filePath = Application.persistentDataPath + "/save.data";
  }

  public void SaveGame(GameData saveData)
  {
    FileStream dataStream = new FileStream(filePath, FileMode.Create);

    BinaryFormatter converter = new BinaryFormatter();
    converter.Serialize(dataStream, saveData);

    dataStream.Close();
  }

  public GameData LoadGame()
  {
    if(File.Exists(filePath))
    {
      // File exists 
      FileStream dataStream = new FileStream(filePath, FileMode.Open);

      BinaryFormatter converter = new BinaryFormatter();
      GameData saveData = converter.Deserialize(dataStream) as GameData;

      dataStream.Close();
      return saveData;
    }
    else
    {
    // File does not exist
    Debug.LogError("Save file not found in " + filePath);
    return null;
    }
  }
}

Putting It Together – Game Master Class

Let’s make a new class called GameMaster, the Master class that will control our game.

Remove the Start function but leave Update there. Now, we’ll make a function called PrintScore and a variable called saveData.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameMaster : MonoBehaviour
{
  GameData saveData = new GameData();

  Update()
  {
  }

  void PrintScore()
  {
    Debug.Log("The current score is " + saveData.score);
  }
}

saveData will be where we store all of our games data. You could have multiple files if you wanted, but we’ll just have one. In this case it contains our score. In this class, we will use the Update function to add or subtract from our score through keyboard input, and it will put the score changes directly into saveData.

When we want to save, we’ll save whatever score is in our copy of saveData to a binary file on our computer. Later when we want to load it, we will  update our copy of saveData from the file.

The PrintScore function simply will print a string to the console telling us the score stored in saveData.

Now let’s add some keyboard input to Update. First, let’s make it so that we can add to and subtract from the score.

void Update()
{
  if(Input.GetKeyDown(KeyCode.UpArrow))
  {
    saveData.AddScore(1);
    PrintScore();
  }
  if (Input.GetKeyDown(KeyCode.DownArrow))
  {
    saveData.AddScore(-1);
    PrintScore();
  }
}

Our if statement checks if the up or down arrow is being pressed. GetKeyDown checks if a key has been pressed. GetKey (which we’re not using) checks if a key is being held down. KeyCode is how we work out which key is being pressed.

Then, we simply add 1 or -1 to the score, and then print the score to the console. Remember when I said that if you add a negative number, you do a subtraction?
So 3 + -1 = 2. You could rewrite it as 3 – 1 = 2.

Now lets add the saving and loading.

void Update()
{
  ...

  if(Input.GetKeyDown(KeyCode.S))
  {
    SaveSystem.instance.SaveGame(saveData);
    Debug.Log("Saved data.");
  }
  if(Input.GetKeyDown(KeyCode.L))
  { 
    saveData = SaveSystem.instance.LoadGame();
    Debug.Log("Loaded data.");
    PrintScore();
  }
}

If you press the S key, we will call our Save function from the SaveSystem. We don’t have to create an instance of SaveSystem in our code to access the variable instance because we made instance static. That should make sense to you now why we made it static. Otherwise we would have had to do something like:

SaveSystem system = new SaveSystem();
sytem.instance.SaveGame(saveData);

This avoids that, making our code tidier and neater, not just because there are less lines of code, but because instance is static, and because SaveSystem is a singleton, we will never accidentally have multiple copies of our save system running around causing weird errors.

We have just one last thing to add to Update – not essential, but it’s nice. The ability to reset the score to 0.

void Update()
{
  ...

  if(Input.GetKeyDown(KeyCode.X))
  {
    saveData.ResetData();
    PrintScore();
  }
}

So if we press the X key, it will reset our score back to 0, and print that new score. That will give us a quick way to test if the code works.

Here is the final code for the class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameMaster : MonoBehaviour
{
  GameData saveData = new GameData();

  // Update is called once per frame
  void Update()
  {
    if(Input.GetKeyDown(KeyCode.UpArrow))
    {
      saveData.AddScore(1);
      PrintScore();
    }
    if (Input.GetKeyDown(KeyCode.DownArrow))
    {
      saveData.AddScore(-1);
      PrintScore();
    }
    if(Input.GetKeyDown(KeyCode.S))
    {
      SaveSystem.instance.SaveGame(saveData);
      Debug.Log("Saved data.");
    }
    if(Input.GetKeyDown(KeyCode.L))
    {
      saveData = SaveSystem.instance.LoadGame();
      Debug.Log("Loaded data.");
     PrintScore();
    }
    if(Input.GetKeyDown(KeyCode.X))
    {
      saveData.ResetData();
      PrintScore();
    }
  }

  void PrintScore()
  {
    Debug.Log("The current score is " + saveData.score);
  }
}

Test It

Now it’s time to test it. Two quick things to do before we test. First, make sure you can see the console. Down where the Project tab is at the bottom of the screen, next to that should be Console.

Console

If it’s there, good. Make sure Collapse is not selected. If you can’t see the console, go up the top of the screen to Window > General > Console and turn it on.

Now try the game out. Press the up and down arrows to change the score, press S to save, then change the score again or wipe it pressing X, then press to load. Did it work? If not, double check your code – and I’ve got all of the finished code below.

If it worked, close the game, then play it again. Before you do anything, try loading the score. It should be the same one you saved. Saving should be 100% working. This is because all of the save data is stored in an external file, so it doesn’t disappear when the game is closed.

Congratulations, you can now save and load! Take this knowledge and make some great games.

All of the Code

Game Data Class

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class GameData
{
  public int score = 0;

  public void AddScore(int points)
  {
    score += points;
  }

  public void ResetData()
  {
    score = 0;
  }
}

Save System Class

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

public class SaveSystem : MonoBehaviour
{
  // Makes it a singleton / single instance
  static public SaveSystem instance;
  string filePath;

  private void Awake()
  {
    // Check there are no other instances of this class in the scene
    if (!instance)
    {
      instance = this;
    }
    else
    {
      Destroy(gameObject);
    }

    filePath = Application.persistentDataPath + "/save.data";
  }

  public void SaveGame(GameData saveData)
  {
    FileStream dataStream = new FileStream(filePath, FileMode.Create);

    BinaryFormatter converter = new BinaryFormatter();
    converter.Serialize(dataStream, saveData);

    dataStream.Close();
  }

  public GameData LoadGame()
  {
    if(File.Exists(filePath))
    {
      // File exists 
      FileStream dataStream = new FileStream(filePath, FileMode.Open);

      BinaryFormatter converter = new BinaryFormatter();
      GameData saveData = converter.Deserialize(dataStream) as GameData;

      dataStream.Close();
      return saveData;
    }
    else
    {
      // File does not exist
      Debug.LogError("Save file not found in " + filePath);
      return null;
    }
  }
}

Game Master Class

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameMaster : MonoBehaviour
{
  GameData saveData = new GameData();

  // Update is called once per frame
  void Update()
  {
    if(Input.GetKeyDown(KeyCode.UpArrow))
    {
      saveData.AddScore(1);
      PrintScore();
    }
    if (Input.GetKeyDown(KeyCode.DownArrow))
    {
      saveData.AddScore(-1);
      PrintScore();
    }
    if(Input.GetKeyDown(KeyCode.S))
    {
      SaveSystem.instance.SaveGame(saveData);
      Debug.Log("Saved data.");
    }
    if(Input.GetKeyDown(KeyCode.L))
    {
      saveData = SaveSystem.instance.LoadGame();
      Debug.Log("Loaded data.");
      PrintScore();
    }
    if(Input.GetKeyDown(KeyCode.X))
    {
      saveData.ResetData();
      PrintScore();
    }
  }

  void PrintScore()
  {
    Debug.Log("The current score is " + saveData.score);
  }
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s