If you've been using Unity for a while, you've probably already got to the point where you've wanted some data to stay after a player closes your game, and then load this data back when the player runs the game again.

Typically, such data concerns game settings and progress, items in inventory, high scores, etc. Nobody wants to change settings over and over again and imagine a game that requires dozens of hours to complete without saving the player's progress...  apart from hardcore mode 😎.

If you've already looked up some solutions, there's a pretty high chance the first you stumbled upon used PlayerPrefs class. I've used this class several times before. However, today I'd like to share with you a different solution.

The gist lies in having a serializable class to hold game data, and another class that holds an instance of it and is responsible for saving and loading data from a file in a process called serialization (and deserialization).

In the following implementation of this idea, I'm going to use BinaryWriter and BinaryReader from System.IO namespace to serialize data into a binary format. However, there's a couple more options, for example, you can serialize into JSON or XML format using a different serialization utility.

The choice really depends on what you plan to do with such serialized data. You might want to just save them in a file on disk, or maybe store them in a database and possibly also send them over the network.

But let's focus now on an example. To keep things simple, let's say we'd like to store just a player's position when he quits the game, so he appears at the same position when he runs the game next time.

I assume you have some basic understanding of the Unity engine, so I'm not going to waste your time here with details on how to setup a project with a player that you can move around and so. There's a link to a fully working example with all that at the end of this post.

Instead, I'll start by showing you our core PersistanceManager class and then I'm going to explain how it works and how to use it.

using System.IO;
using UnityEngine;

public class PersistenceManager : MonoBehaviour
{
    private class GameData
    {
        public Vector3 PlayerPosition;
    }

    private GameData gameData;

    public Vector3 PlayerPosition
    {
        get => gameData.PlayerPosition;
        set => gameData.PlayerPosition = value;
    }

    private string gameDataFilename = "GameData.dat";

    public void LoadGameData()
    {
        Debug.Log("Application.persistentDataPath=" + Application.persistentDataPath);

        string gameDataPath = Path.Combine(Application.persistentDataPath, gameDataFilename);

        if (File.Exists(gameDataPath))
        {
            using (BinaryReader reader = new BinaryReader(File.Open(gameDataPath, FileMode.Open))) { 
                GameData gameData = new GameData();
                gameData.PlayerPosition.x = reader.ReadSingle();
                gameData.PlayerPosition.y = reader.ReadSingle();
                gameData.PlayerPosition.z = reader.ReadSingle();
                this.gameData = gameData;
            }
        }
        else
        {
            gameData = new GameData();
        }
    }

    public void SaveGameData()
    {
        string gameDataPath = Path.Combine(Application.persistentDataPath, gameDataFilename);

        using (BinaryWriter writer = new BinaryWriter(File.Open(gameDataPath, FileMode.Create)))
        {
            writer.Write(gameData.PlayerPosition.x);
            writer.Write(gameData.PlayerPosition.y);
            writer.Write(gameData.PlayerPosition.z);
        };
    }
}
Think of PersistenceManager as a little service other classes can use to store and get data that should be persistent and how the persistency is achieved is solely a responsibility of the PersistenceManager.

First, notice the definition of GameData class is private and inside PersistenceManager, because no one else needs it. Instance of this class is also private and it's what we want to be serialized and eventually deserialized.

Getter and setter of public property PlayerPosition point to gameData.PlayerPosition, think of this property as of a "middleman" between private instance of GameData which is belongs only to PersistenceManager and the rest of the project.

Private string gameDataPath is assigned in LoadGameData method combining the filename of GameData.dat with Application.persistenceDataPath. The resulting path is different on every platform, so Debug.Log call comes handy.

In the LoadGameData method, if the GameData.dat file exists, our game loads its content and deserializes it using BinaryReader. Otherwise, a fresh instance of GameData is assigned to gameData instead.

Finally, in the SaveGameData method, BinaryWriter is used to serialize gameData into a binary file.

To achieve our final goal, persist player position across different game sessions, all we need to do now is use PersistenceManager like this:

using System;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    [SerializeField]
    private PersistenceManager persistenceManager;

    [SerializeField]
    private GameObject player;

    private void Awake()
    {
        persistenceManager.LoadGameData();
    }

    void Start()
    {
        player.transform.position = persistenceManager.PlayerPosition;
    }

    void OnApplicationQuit()
    {
        persistenceManager.PlayerPosition = player.transform.position;
        persistenceManager.SaveGameData();
    }
}

As you can see, I'm using PersistanceManager class directly in a very simple GameManager class, to keep this example easy to follow. In a project with bigger scope, you'd probably don't want such coupling.

And that's basically it. Thank you for reading this post 🙏. The complete Unity project is available here on GitHub.

I used similar approach to store some data in my educational game Hiragana vs. Katakana, and it's source code is also available on GitHub.