Data persistence or how to save / load game data in Unity

 by • Reading Time: 8 minutes

Introduction

While you work on your project in Unity, sooner or later you will come across a problem of data persistence. It may occur at the stage in your development when you have to maintain information between loading different scenes or even different game sessions. Save states, game configuration settings or characters statistics – these are just the examples of data you would ideally want to somehow store on a game’s platform for later use. That goes without saying that even the smallest game may require persistent data, which you can always access from various scripts inside your system’s architecture. Luckily, both C# and Unity provide few solutions to this problem. All of them have pros and cons and it is entirely up to developer to decide which one is the best for the project. In this tutorial I’ll describe the most important ones.

Table of contents:

  1. The Problem of data persistence
  2. The Solutions to data persistence

1. The Problem of data persistence

In order to tackle the problem, let’s first define it. All scripts and components that are attached to GameObject inside a Unity project are destroyed each time you load a scene. One may think that this can be easily solved by marking any of these objects as static. However, that is not the case and you will still be faced with losing the data. The way Unity deals with any kind of Object, especially Component or GameObject type instances, is that it destroys them every time you transit from one scene to another.

Similarly, the same principle applies whenever you start / stop the game. There are multiple ways of dealing with this problem. Each of them comes with pros and cons and it depends on what kind of data you want to preserve. Let me describe all the possible solutions that are commonly known to the best of my knowledge.

2. The Solutions to data persistence

In this section I will focus on the main commonly used solutions for data persistence in Unity games. I’ll try my best to structure this article to act as a reference point whenever you find yourself stuck with your project. Let’s go!

The ‘static’ approach

This one should be pretty obvious, but I noticed that some developers are not aware of it. It is entirely possible to write a public static class and access all it’s primitive components and member functions from any script in your scenes. This can be perceived as a very crude way of storing and accessing the data across your scenes. However, for a small project it can be quite a handy solution that doesn’t need too much elaboration.

  • inside the ‘Assets’ panel create a script called MySharedData.cs or similar
  • make sure that you do not import any of the UnityEngine libraries and make the class static
  • DO NOT make this class inherit from MonoBehaviour
  • declare all the variables and methods you want to use across the scripts in your game as public static

The below listing is an example of a script that has been placed under the project’s ‘Assets’:

public static class MySharedData 
{
    public static int score = 1234;
    public static bool audio = true;
    public static float clockTimer = 3432;

    public static int GetCurrentScore() {
        return score;
    }
}

Of course the GetCurrentScore() function above is redundant and I only used it for illustrative purposes. After that, I can access this data from any script I want regardless of the loaded scene the following way:

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

public class TestScript : MonoBehaviour
{

    private void Awake()
    {
        Debug.Log(MySharedData.score);
        MySharedData.score += 100;
        Debug.Log("The current score is now: " + MySharedData.GetCurrentScore());
    }

    // Update is called once per frame
    void Update()
    {
        ...
    }

}

At this point it is worth mentioning that this solution works only with built-in primitives, such as intboolstringfloatdouble. However, you cannot use it for anything that inherits from Object, Component and MonoBehaviour. That’s because Unity will destroy any of these objects whenever you load a new scene even when you declare them as static. Luckily, there’s another way of preserving these between the scenes, although it comes with a little caveat.

The DontDestroyOnLoad built-in method

Unity developers introduced the DontDestroyOnLoad method to preserve all objects that inherit from Object, Component or GameObject. In fact, when your object inherits from GameObject it will also save all Transform‘s children along with it. This is a neat little function that is often used with a Singleton pattern for practical reasons. However, when going for this method of data persistence we need to remember to place our objects at the root of our Hierarchy.

Let’s assume we have some kind of LevelManager script that keeps track and maintains the state of our game. Presumably we want to have only one instance of this object that we can access from any script. To that end, I’m going to create an Empty GameObject at the root of project Hierarchy and attach the LevelManager script component to it. Next, I’ll instantiate it using the Singleton pattern and make sure that it doesn’t get destroyed when loading a new scene with DontDestroyOnLoad. I’ll encapsulate the entire logic inside the Awake() method.

public class LevelManager : MonoBehaviour
{
    ...
    
    public int score = 1234;
    public bool audio = true;
    public float clockTimer = 3432;

    #region Singleton
    public static LevelManager instance;

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(this);
        }
        else
        {
            Destroy(this.gameObject);
        }
    }
    #endregion

    public int GetCurrentScore() {
        return score;
    } 
}

By implementing a class in this manner we can use its components from any script in our project. This will happen irrespective of whether we load a new scene or not. You can then use it in your scripts the following way:

Debug.Log(LevelManager.instance.score);
LevelManager.instance.score += 100;
Debug.Log("The current score is now: " + LevelManager.instance.GetCurrentScore());

The PlayerPrefs data persistence concept

The PlayerPref stands for “player preferences”. This API is usually used for storing player’s settings or preferences between game sessions. The data saved through this system lands inside a given platform registry. You can obtain the exact locations of files for each platform in the official Unity documentation. It is not safe for saving game sensitive data defining its state such as player statistics. The reason is that it can be easily edited via external applications giving the player a way to cheat and modification the game state. This not only gives an obvious advantage to the player but also provides means of potentially breaking the game. That’s why you should cautiously consider the data you will use it for. It’s mostly suitable for saving game settings of a global nature such as audio settings, game graphics etc.

The PlayerPrefs API provides a limited set of functions used for setting and getting the primitives to and from hard drive. The saving procedure can be narrowed down to the execution of these functions in the following way:

PlayerPrefs.SetInt("score", 1234);
bool isAudio= true;
PlayerPrefs.SetInt("audio", isAudio ? 1 : 0);
PlayerPrefs.SetFloat("clockTimer", 34.32f);
PlayerPrefs.SetString("username", "Pav");
PlayerPrefs.Save();

Please note that since the PlayerPrefs doesn’t provide a method for storing / accessing booleans you have to use the int primitive. On Lines 2-3 I gave an example of how you can do it with a conditional operator. Also, please remember to call the Save() method (Line 6) once you finish setting up all your variables. Otherwise they won’t be saved.

In order to read variables from PlayerPrefs all you have to do is to call its “getter” member methods like so:

int score = PlayerPrefs.GetInt("score");
float clockTimer = PlayerPrefs.GetFloat("clockTimer");
string username = PlayerPrefs.GetString("username");
 
bool isAudio= PlayerPrefs.GetInt("audio") == 1 ? true : false;

I generally recommend saving your data in OnDisable() methods and reading it in OnEnable() when using PlayerPrefs. That way you can maintain the consistency in passing the data between the scenes.

The ScriptableObject data persistence object

I’ve already shown several examples of using the scriptable objects to preserve data between scenes. A ScriptableObject is essentially a data container that can store data independently from the class instances. The most typical use case for it is to set up an object under your project’s ‘Assets’ and access the data that it holds at runtime. However, since the scriptable objects use the editor’s namespace you cannot use it to permanently save data in deployed builds. You can only use the data you set up during the development for a single game session.

In order to create a scriptable object write a class that inherits from ScriptableObject.

using UnityEngine;
 
[CreateAssetMenu(fileName = "MySharedData", menuName = "SharedData/MySharedData", order = 1)]
public class MySharedData : ScriptableObject
{
    public int score = 1234;
    public bool audio = true;
    public float clockTimer = 3432;
}

If you add the CreateAssetMenu attribute (line 3) you will be able to create your objects directly inside the editor. To do so simply right-click inside the ‘Assets’ panel and select your scriptable object from the menu. In this example it would be Create > SharedData > MySharedData.

Creating the scriptable object for data persistence

Once done, fill in your data in the ‘Inspector’ panel and later use it inside your scripts the following way:

sing UnityEngine;

public class SharedData: MonoBehaviour
{
    // An instance of the ScriptableObject
    public MySharedData sharedData;
    
    void Start() {
        Debug.Log(sharedData.score);
        sharedData.score += 100;
        Debug.Log("The current score is now: " + sharedData.score);
    }
}

Storing data in an external file

The last method is to use the C# file methods to serialize data to json or xml to save / load them to / from file. This solution requires a bit more work since you have to write wrapper functions for serialization and saving / loading data from hard drive. In addition, there is a known issue with BinaryFormatter on iOS that prevents file manipulation on native mobile device. Luckily, there is an easy solution to this and it only requires you to add a single line of code in either Awake() or Start() function:

System.Environment.SetEnvironmentVariable("MONO_REFLECTION_SERIALIZER","yes");

It is perfect for permanently storing a great amount of data on platform’s hard drive. Firstly, create a serializable class that will hold all the data your game needs.

[Serializable]
public class MySharedData
{
    public int score = 1234;
    public bool audio = true;
    public float clockTimer = 3432;
}

Please note the [Serializable] attribute above the class declaration. Secondly, write the static Save method that will create a file, serialize the data and store it on the hard drive. On lines 4-5 I highlighted the necessary imports.

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

public class DataSaver : MonoBehaviour
{
    ...

    static public void Save()
    {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Create(Application.persistentDataPath + "/MySharedData.dat");

        MySharedData sharedData = new MySharedData();
        sharedData.score = 100;
        sharedData.audio = true;
        sharedData.clockTimer = 12;

        bf.Serialize(file, sharedData);
        file.Close();
    }
}

Thirdly, in order to access the data we need to load it first. To that end I’ll add the analogous Load method. It will make an attempt to read from the previously saved file, deserialize the data and recreate the object from it.

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

public class DataSaver : MonoBehaviour
{
    ...

    static public void Save()
    {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream fs = File.Create(Application.persistentDataPath + "/MySharedData.dat");

        MySharedData sharedData = new MySharedData();
        sharedData.score = 100;
        sharedData.audio = true;
        sharedData.clockTimer = 12;

        bf.Serialize(fs, sharedData);
        fs.Close();
    }

    static public void Load()
    {
        if (File.Exists(Application.persistentDataPath + "/MySharedData.dat"))
        {
            BinaryFormatter bf = new BinaryFormatter();
            FileStream fs = File.Open(Application.persistentDataPath + "/MySharedData.dat", FileMode.Open);
            MySharedData sharedData = bf.Deserialize(fs) as MySharedData;
            fs.Close();

            if (sharedData != null)
            {
                Debug.Log("Score: " + sharedData.score);
                Debug.Log("Audio: " + sharedData.audio);
                Debug.Log("Clock Timer: " + sharedData.clockTimer);
            }
        }
    }
}

Conclusion

In this article I have presented few methods of preserving the data between scenes and game sessions in Unity. All solutions I showed here come with pros and cons and it is up to the developer to decide which one to use. It is usually dictated by the type of data we are dealing with and whether we want to preserve it only for the duration of a single game session or permanently save it on hard drive to access it between different game sessions.