Brian Drake Brian is the owner of UpRoom Games, a small independent game studio focused on creating unique experiences and sharing game dev knowledge in Unity.

Why you should (or shouldn’t) save data with Unity’s PlayerPrefs

8 min read 2403

Why You Should Or Shouldn't Save Data With Unity's PlayerPrefs

Whether you’re relatively new to developing in Unity or you’ve got a few projects under your belt, you’ve most likely realized how useful it would be for players to save data.

If you’re like most of us, your first instinct might be to do a quick search on how to save player data in Unity. From this search, you may be surprised that there are quite a few different answers, all varying in complexity.

The simplest solution is a Unity class called PlayerPrefs. In this article, we’ll go over what PlayerPrefs is, how, when, and when not to use it. We’ll also discuss reasonable alternatives to using PlayerPrefs.

Using PlayerPrefs in Unity

With PlayerPrefs, you can have an easy-to-use interface that allows you to start persisting some data!

Let’s say you’ve made a simple game with some number of levels. You’ve decided you want to save the level your player is on when they quit the game so that the correct level loads back up when they start again.

Well, this looks pretty easy in PlayerPrefs. All you need to do to save the level is the following:

<pre>
    // currentLevel is an integer variable representing the level the player is on
PlayerPrefs.SetInt("Level", currentLevel);
PlayerPrefs.Save();
</pre>

When you’re ready to load, getting the saved value is just as easy:

<pre>
// You can set a default return value if you haven't saved a "Level" yet with the second parameter
    currentLevel = PlayerPrefs.GetInt("Level", 0);
</pre>

This certainly seems to be about as easy as it gets, so it’s time to save some data, right?

Well, sort of. The short answer is that saving a game state like this is generally a bad idea.

The longer answer is that saving and loading data in Unity is actually a very deep topic without a clear best solution.



Drawbacks to using PlayerPrefs

Let’s start by taking a closer look at PlayerPrefs to see if it’s something we want to use to save our game data. A good first step in this process is to check the docs for PlayerPrefs. Right away, some things should raise some eyebrows.

Method of saving data

First of all, PlayerPrefs saves data on a per-application basis. This means that if you want to have more than one save file for your application, you need to get creative. For example, you could choose to append a save file ID to each entry in PlayerPrefs and only load and save based on that ID.

Of course, then you also need to find a way to save a list of player IDs, which brings us to the next issue with using PlayerPrefs: you can only save data as a string, float, or integer.

Specific data types

You probably know that there are more data types than just string, float, and integer. You can probably envision a scenario where you would want to use a data type other than these three. This should be a big red flag for us when trying to figure out if this class is suitable for saving game data.

If we need to construct some sort of complicated string parsing system to mimic the behavior of a simple list, we might be barking up the wrong tree.

Location of saved data

Another important tidbit we can extract from the docs is where the location of the data from PlayerPrefs is actually saved. This may be something you haven’t given much thought to if you’re relatively new to programming, but saved data is just data that exists somewhere in a file.

That file might be on a different computer across the country in the case of an online application, or it might just be in a sub folder called data. The file could be structured like a database or could be a simple text file, it just depends on what the application developers felt like doing.


More great articles from LogRocket:


In this case, if we are on a Windows computer, we can see from the docs that PlayerPrefs entries are saved to the registry. This should be another red flag when considering saving game data with PlayerPrefs. The registry is really not used for that sort of thing and mainly saves system and application configuration data.

With all these flags and eyebrows popping up, it’s pretty natural to wonder what exactly PlayerPrefs is used for if not for saving player progress data. Well, the first clue (and point of confusion) comes from the name.

When to use Unity’s PlayerPrefs

If you thought that the name “PlayerPrefs” alludes to the preferences of the player, you’re certainly not alone, but you are also incorrect.

The “Player” in “PlayerPrefs” actually refers to the Unity Player, the name of the standalone Unity Engine application that gets built when you turn your game into an application of its own. You may have come across this naming convention when changing settings for your project in Unity, as the runtime options are under Player Settings.

With this in mind, we can start to find some reasonable use cases for PlayerPrefs.

Most projects made with Unity will probably want to launch with a specific resolution or quality level. While we could choose to save settings related to these things with the rest of our game data, it is also reasonable to assume that a comfortable resolution isn’t really relevant for each save, but is more relevant to the application with respect to the computer it runs on.

In this case, saving resolution with PlayerPrefs could make perfect sense. With each new save file your users create, the application resolution remains at whatever they initially set. This is more optimal than defaulting to a value that may not even work on each user’s machine each time they start a new save.

The PlayerPrefs class is not an ideal way to save game or per user data for a Unity project. The data types are very restrictive and data cannot be easily separated from each user. However, it is an excellent interface for saving configuration and settings information relevant to each specific application, such as resolution, quality level, default audio device, and more.

So if we aren’t going to save our game data using PlayerPrefs, what are we going to do? The first step is to really think about the specifics of your application and how user data is going to be handled.

For example, it’s a good idea to think about what would happen if the user could access the data saved by your application. Realistically, any local saving system (including using PlayerPrefs) will lead to this possibility.

If the answer is that it would be catastrophically bad, then you probably already know that you need to be saving user data off-site on a separate server (as is the case with almost all online applications). If you are making an offline application, however, the answers can vary pretty wildly.

In the case of a game, having your users access their own save data is probably not the worst thing, but it could lead to manipulation of the game state or even accidental corruption of their progress. There isn’t a lot you can do about the latter, since no matter what format your data is stored in, it will likely always be possible to find a way to randomly remove a chunk of it. For the former, there are a couple of options that range from not worrying about it to encrypting your save data to make it more difficult, though not impossible, to manipulate.

Storing data without PlayerPrefs

To keep things simple, let’s focus on saving data for a local application. In essence, all we need to do to save player data is write information to a file. In Unity, the simplest way to do this is with a StreamWriter and JsonUtility.

StreamWriter will let us actually write data to a file, and JsonUtility will handle converting the data we want to save into JavaScript Object Notation (JSON).

While we aren’t actually using any JavaScript, JSON has become one of the standards for transmitting data due to its convenience for web applications. It is very easily read by humans and maps very simply to JavaScript objects.

For our purposes, we will let Unity handle the actual translation to and from JSON via JsonUtility. JsonUtility is sort of a blessing and a curse, since it is very fast but has several limitations.

Before implementing this in your projects, I encourage you to read the docs. It’s very beneficial to understand some of the limitations of JsonUtility before going all-in for a project.

Let’s take a look at an example before the weeds get too thick.

<pre>
using System.IO;
using UnityEngine;

public class DataManager
{
    private Data gameData;
    private static string dataFilePath = Path.Combine(Application.persistentDataPath, "GameData.json");

    public DataManager(int level = 0)
    {
        gameData = new Data();
        gameData.level = level;
    }

    // Here we set our level with some sort of GameManager
    public void SetLevel(int level)
    {
        if (gameData == null)
        {
            gameData = new Data();
        }

        gameData.level = level;
    }

    // The method to return the loaded game data when needed
    public Data GetGameData()
    {
        return gameData;
    }

    public void Save()
    {
        // This creates a new StreamWriter to write to a specific file path
        using (StreamWriter writer = new StreamWriter(dataFilePath))
        {
            // This will convert our Data object into a string of JSON
            string dataToWrite = JsonUtility.ToJson(gameData);

            // This is where we actually write to the file
            writer.Write(dataToWrite);
        }
    }

    public void Load()
    {
        // This creates a StreamReader, which allows us to read the data from the specified file path
        using (StreamReader reader = new StreamReader(dataFilePath))
        {
            // We read in the file as a string
            string dataToLoad = reader.ReadToEnd();

            // Here we convert the JSON formatted string into an actual Object in memory
            gameData = JsonUtility.FromJson<Data>(dataToLoad);
        }
    }

    [System.Serializable]
    public class Data
    {
        // The actual data we want to save goes here, for this example we'll only use an integer to represent the level
        public int level = 0;
    }
}
</pre>

This example should be enough to start saving and loading data in your local Unity projects. Web-based projects are complex enough that we won’t be covering data management practices for them here.

Obviously, there is a bit more overhead here than with the PlayerPrefs class. However, once you have a simple save and load set up, it’s almost as easy to use in practice. One thing to note here is that the data saved in GameData.json is going to be easily readable and, therefore, modifiable.

This means that in its current state, it wouldn’t be too difficult for an end user to modify their save data. To make this more difficult, we would need to look into different methods of encryption for our data.

Encrypting save data is a bit beyond the scope of this article, but there are many thorough resources available on the topic if you are so inclined to search. For now, as an alternative to PlayerPrefs for local, nonsensitive data, the simple file operations outlined here are a great way to get started saving game data.

Example of implementing PlayerPrefs

So what about PlayerPrefs? Since it’s an excellent system for saving computer-specific information, we can modify our example to also save some settings data in a way that doesn’t interfere with our file operations and will persist across users on the same computer.

<pre>
using System.IO;
using UnityEngine;

public class DataManager
{
    private Data gameData;
    private SettingsData settingsData;
    private static string dataFilePath = Path.Combine(Application.persistentDataPath, "GameData.json");

    public DataManager(int level = 0)
    {
        gameData = new Data();
        gameData.level = level;

        settingsData = new SettingsData(0, 1920, 1080);
    }

    // Here we set our level with some sort of GameManager
    public void SetLevel(int level)
    {
        if (gameData == null)
        {
            gameData = new Data();
        }

        gameData.level = level;
    }

    // The method to return the loaded game data when needed
    public Data GetGameData()
    {
        return gameData;
    }

    // This can be used when we change settings values
    public void SetSettings(int quality, int horizontalResolution, int verticalResolution)
    {
        settingsData = new SettingsData(quality, horizontalResolution, verticalResolution);
    }

    public void SaveSettings()
    {
        PlayerPrefs.SetInt("QualityLevel", settingsData.qualityLevel);
        PlayerPrefs.SetInt("HorizontalResolution", settingsData.horizontalResolution);
        PlayerPrefs.SetInt("VerticalResolution", settingsData.verticalResolution);
        PlayerPrefs.Save();
    }

    public void LoadSettings()
    {
        // We can get a default value if there is no settings information, which is what the second parameter
        // in the PlayerPrefs.GetInt(p1, p2) is for
        settingsData = new SettingsData(
                            PlayerPrefs.GetInt("QualityLevel", 0),
                            PlayerPrefs.GetInt("HorizontalResolution", 1920),
                            PlayerPrefs.GetInt("VerticalResolution", 1080)
                        );
    }

    public void Save()
    {
        // This creates a new StreamWriter to write to a specific file path
        using (StreamWriter writer = new StreamWriter(dataFilePath))
        {
            // This will convert our Data object into a string of JSON
            string dataToWrite = JsonUtility.ToJson(gameData);

            // This is where we actually write to the file
            writer.Write(dataToWrite);
        }

        // This doesn't need to be coupled to the Save function, and probably shouldn't be,
        // but for demonstration purposes we'll put this here
        SaveSettings();
    }

    public void Load()
    {
        // This creates a StreamReader, which allows us to read the data from the specified file path
        using (StreamReader reader = new StreamReader(dataFilePath))
        {
            // We read in the file as a string
            string dataToLoad = reader.ReadToEnd();

            // Here we convert the JSON formatted string into an actual Object in memory
            gameData = JsonUtility.FromJson<Data>(dataToLoad);
        }

        LoadSettings();
    }

    public class SettingsData
    {
        public int qualityLevel = 0;
        public int horizontalResolution = 1920;
        public int verticalResolution = 1080;

        public SettingsData(int quality, int hr, int vr)
        {
            this.qualityLevel = quality;
            this.horizontalResolution = hr;
            this.verticalResolution = vr;
        }
    }

    [System.Serializable]
    public class Data
    {
        // The actual data we want to save goes here, for this example we'll only use an integer to represent the level
        public int level = 0;
    }
}

</pre>

While the code presented here is not exactly game ready, hopefully the concepts explained through it will be useful in your Unity project development.

Conclusion

That’s it! I hope that the uses of PlayerPrefs are a little more clear.

This is a topic that I found somewhat confusing when I started working in Unity. However, at the end of the day, it’s really up to you for how to handle the data in your project. Every problem comes with a wide range of potential solutions.

I hope you make something awesome!

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page web and mobile apps.

.
Brian Drake Brian is the owner of UpRoom Games, a small independent game studio focused on creating unique experiences and sharing game dev knowledge in Unity.

Leave a Reply