Persistent data: How to save your game states and settings
Saving data is critical for any game. Whether you need to save high scores, preferences, or a game state, Unity offers a variety of methods – from PlayerPrefs to serializing data, encrypting it, and writing to a file.
As part of Unite Now 2020, I created a session with tips on data persistence in Unity. It covers some of the common ways to save and load data in your Unity project, but it’s by no means an exhaustive list. That is to say, there are more ways to serialize data than you’ll ever need, and each approach solves a particular problem and comes with its own set of strengths and weaknesses. This blog post will cover the same common methods that I discussed in the Unite Now session.
PlayerPrefs are not made to save game states. However, they’re useful, so we’ll discuss them. You can use PlayerPrefs to store a player’s preferences between sessions, such as quality settings, audio volume or other non-essential data. PlayerPrefs are stored somewhere on your device, separate from your project. The exact location varies depending on your operating system, but it’s usually somewhere that’s globally accessible and managed by your OS. The stored data is in simple key-value pairs. Because of their ease of access, they aren’t safe from users who wish to open and modify them, and they can be deleted by accident since they’re saved outside of the project and managed by your OS.
PlayerPrefs are relatively easy to implement and require only a few lines of code, but they only support Float, Int and String-type values, making it challenging to serialize large, complex objects. A determined user can overcome this limitation by converting their saved data into some format represented by one of these basic types, but I don’t recommend it since that there are better tools to store your data.
public void SavePrefs()
public void LoadPrefs()
int volume = PlayerPrefs.GetInt("Volume", 0);
Simple PlayerPrefs implementation
Finally, since each Unity application stores all its PlayerPrefs in a single file, it’s not well-suited for handling multiple save files or cloud saves, both of which require you to store and receive save data from a different location.
JSON is a human-readable data format. That is, it’s easily understood by people and machines alike – which has both advantages and disadvantages. It’s much easier to debug your saved data or create new save data for testing purposes when you can read and understand it, but, on the other hand, it’s easy for players to read and modify the data as well. The ability to read and change data is useful if you support modding but detrimental if you want to prevent cheating. Every use case is different, and it’s these kinds of tradeoffs that lead developers to create many other data formats.
JSON is standardized and widely used in many different applications. As a result, all platforms support it strongly, which is helpful when building cross-platform games. JSON was developed as a communication protocol for web browsers, making it inherently good for sending data over a network. Because of this, JSON is excellent for sending and receiving data from a server backend.
JsonUtility is Unity’s built-in API for serializing and deserializing JSON data. Similar to PlayerPrefs, it’s also relatively easy to implement. However, unlike PlayerPrefs, you must save the JSON data yourself, either in a file or over a network. Handling the data storage yourself makes it easy to manage multiple save files because you can store each file in a different location. To make this easier, I wrote a basic file manager, which is available in this example repository.
It’s important to mention that JsonUtility isn’t a fully featured JSON implementation. If you’re used to working with JSON data, you may notice the lack of support for specific features. Holding back certain features was a deliberate design choice, and JsonUtility is faster and more efficient than other .NET JSON solutions as a result.
The same limitations constrain JsonUtility as the internal Unity serializer – that is to say, if you can’t serialize a field in the Inspector, you won’t be able to serialize it to JSON. To work around these limitations, you could create Plain Old Data types (or PODS) to hold all your save data. When it comes time to save, transfer your data from their runtime types into a POD, and save that to a disk. If needed, you can also create custom serialization callbacks to support types that Unity’s serializer doesn’t support by default.
On the topic of JsonUtility, EditorJsonUtility is another useful tool. Whereas JsonUtility works for any MonoBehaviour or ScriptableObject-based object, EditorJsonUtility will work for any Unity engine type. So you could create a JSON representation of any object in the Unity Editor – or go in the other direction and create an asset from a JSON file.
Aside from the built-in serialization options, there are other external libraries that you could use as well:
- EasySave is a well-supported and popular plug-in available on the Unity Asset Store. It allows you to save all your data without writing any code, which is excellent for beginners. It also has a powerful and flexible API that makes it ideal for advanced users as well. It isn’t free, but it’s worth the price if you’re looking for a fully featured out-of-the-box solution.
- JSON.Net is a free and open source JSON implementation for all DotNet platforms. Unlike the built-in JsonUtility, it’s fully featured. The standard version doesn’t support all of Unity’s platforms, but there is a modified version available in the Unity Asset Store that adds support.
- XML is an alternative data format. Like JSON, it’s relatively human-readable and has some features that may be useful for your specific application, such as namespaces. DotNet has built-in support for XML.
- BinaryFormatter is a DotNet library to store your objects in a binary format directly. However, BinaryFormatter has dangerous security vulnerabilities and should be avoided. I repeat, do not use BinaryFormatter. Learn more information about the security risks here.
When security comes up, most people think of encryption first. However, when it comes to storing data locally on a player’s device, encryption is relatively easy to overcome. Even without breaking the encryption, users can manipulate the data directly in memory with freely available tools. In other words, it’s safe to assume that anything that’s stored locally is untrustworthy.
If you need real security, your best option is to keep your data on a server where users can’t modify it. For this to work, the application shouldn’t send any data directly to the server because users could still manipulate it. Instead, the application can only send commands to the server, let the server change the data, and then send the results back to the application. So if data security is vital for you, it’s best to know as soon as possible because it will affect your project’s architecture.