Saving Data in Unity3D Using PlayerPrefs
Rate this tutorial
(Part 1 of the Persistence Comparison Series)
Persisting data is an important part of most games. Unity offers only a limited set of solutions, which means we have to look around for other options as well.
In this tutorial series, we will explore the options given to us by Unity and third-party libraries. Each part will take a deeper look into one of them with the final part being a comparison:
- Part 1: PlayerPrefs (this tutorial)
- Part 2: Files
- Part 3: BinaryReader and BinaryWriter (coming soon)
- Part 4: SQL
- Part 5: Realm Unity SDK
- Part 6: Comparison of all these options
To make it easier to follow along, we have prepared an example repository for you. All those examples can be found within the same Unity project since they all use the same example game, so you can see the differences between those persistence approaches better.
The repository can be found at https://github.com/realm/unity-examples, with this tutorial being on the persistence-comparison branch next to other tutorials we have prepared for you.
Note that if you have worked through any of the other tutorials in this series, you can skip this section since we are using the same example for all parts of the series so that it is easier to see the differences between the approaches.
The goal of this tutorial series is to show you a quick and easy way to take some first steps in the various ways to persist data in your game.
Therefore, the example we will be using will be as simple as possible in the editor itself so that we can fully focus on the actual code we need to write.
A simple capsule in the scene will be used so that we can interact with a game object. We then register clicks on the capsule and persist the hit count.
When you open up a clean 3D template, all you need to do is choose
GameObject
-> 3D Object
-> Capsule
.You can then add scripts to the capsule by activating it in the hierarchy and using
Add Component
in the inspector.The scripts we will add to this capsule showcasing the different methods will all have the same basic structure that can be found in
HitCountExample.cs
.1 using UnityEngine; 2 3 /// <summary> 4 /// This script shows the basic structure of all other scripts. 5 /// </summary> 6 public class HitCountExample : MonoBehaviour 7 { 8 // Keep count of the clicks. 9 [private int hitCount; // 1 ] 10 11 private void Start() // 2 12 { 13 // Read the persisted data and set the initial hit count. 14 hitCount = 0; // 3 15 } 16 17 private void OnMouseDown() // 4 18 { 19 // Increment the hit count on each click and save the data. 20 hitCount++; // 5 21 } 22 }
The first thing we need to add is a counter for the clicks on the capsule (1). Add a
[SerializeField]
here so that you can observe it while clicking on the capsule in the Unity editor.Whenever the game starts (2), we want to read the current hit count from the persistence and initialize
hitCount
accordingly (3). This is done in the Start()
method that is called whenever a scene is loaded for each game object this script is attached to.The second part to this is saving changes, which we want to do whenever we register a mouse click. The Unity message for this is
OnMouseDown()
(4). This method gets called every time the GameObject
that this script is attached to is clicked (with a left mouse click). In this case, we increment the hitCount
(5) which will eventually be saved by the various options shown in this tutorials series.(See
PlayerPrefsExampleSimple.cs
in the repository for the finished version.)The easiest and probably most straightforward way to save data in Unity is using the built-in
PlayerPrefs
. The downside, however, is the limited usability since only three data types are supported:- string
- float
- integer
Another important fact about them is that they save data in plain text, which means a player can easily change their content.
PlayerPrefs
should therefore only be used for things like graphic settings, user names, and other data that could be changed in game anyway and therefore does not need to be safe.Depending on the operating system the game is running on, the
PlayerPrefs
get saved in different locations. They are all listed in the documentation. Windows, for example, uses the registry to save the data under HKCU\Software\ExampleCompanyName\ExampleProductName
.The usage of
PlayerPrefs
is basically the same as a dictionary. They get accessed as key
/value
pairs where the key
is of type string
. Each supported data type has its own function:- SetString(key, value)
- GetString(key)
- SetFloat(key, value)
- GetFloat(key)
- SetInt(key, value)
- GetInt(key)
1 using UnityEngine; 2 3 public class PlayerPrefsExampleSimple : MonoBehaviour 4 { 5 // Resources: 6 // https://docs.unity3d.com/ScriptReference/PlayerPrefs.html 7 8 [private int hitCount = 0; ] 9 10 private const string HitCountKey = "HitCountKey"; // 1 11 12 private void Start() 13 { 14 // Check if the key exists. If not, we never saved the hit count before. 15 if (PlayerPrefs.HasKey(HitCountKey)) // 2 16 { 17 // Read the hit count from the PlayerPrefs. 18 hitCount = PlayerPrefs.GetInt(HitCountKey); // 3 19 } 20 } 21 22 private void OnMouseDown() 23 { 24 hitCount++; 25 26 // Set and save the hit count before ending the game. 27 PlayerPrefs.SetInt(HitCountKey, hitCount); // 4 28 PlayerPrefs.Save(); // 5 29 } 30 31 }
For the
PlayerPrefs
example, we create a script named PlayerPrefsExampleSimple
based on the HitCountExample
shown earlier.In addition to the basic structure, we also need to define a key (1) that will be used to save the
hitCount
in the PlayerPrefs
. Let's call it "HitCountKey"
.When the game starts, we first want to check if there was already a hit count saved. The
PlayerPrefs
have a built-in function HasKey(hitCountKey)
(2) that let's us achieve exactly this. If the key exists, we read it using GetInt(hitCountKey)
(3) and save it in the counter.The second part is saving data whenever it changes. On each click after we incremented the
hitCount
, we have to call SetInt(key, value)
on PlayerPrefs
(4) to set the new data. Note that this does not save the data to disk. This only happens during OnApplicationQuit()
implicitly. We can explicitly write the data to disk at any time to avoid losing data in case the game crashes and OnApplicationQuit()
never gets called.
To write the data to disk, we call Save()
(5).(See
PlayerPrefsExampleExtended.cs
in the repository for the finished version.)In the second part of this tutorial, we will extend this very simple version to look at ways to save more complex data within
PlayerPrefs
.Instead of just detecting a mouse click, the extended script will detect
Shift+Click
and Ctrl+Click
as well.Again, to visualize this in the editor, we will add some more
[SerializeFields]
(1). Substitute the current one (hitCount
) with the following:1 // 1 2 [private int hitCountUnmodified = 0; ] 3 [private int hitCountShift = 0; ] 4 [private int hitCountControl = 0; ]
Each type of click will be shown in its own
Inspector
element.The same has to be done for the
PlayerPrefs
keys. Remove the HitCountKey
and add three new elements (2).1 // 2 2 private const string HitCountKeyUnmodified = "HitCountKeyUnmodified"; 3 private const string HitCountKeyShift = "HitCountKeyShift"; 4 private const string HitCountKeyControl = "HitCountKeyControl";
There are many different ways to save more complex data. Here we will be using three different entries in
PlayerPrefs
as a first step. Later, we will also look at how we can save structured data that belongs together in a different way.One more field we need to save is the
KeyCode
for the key that was pressed:1 // 3 2 private KeyCode modifier = default;
When starting the scene, loading the data looks similar to the previous example, just extended by two more calls:
1 private void Start() 2 { 3 // Check if the key exists. If not, we never saved the hit count before. 4 if (PlayerPrefs.HasKey(HitCountKeyUnmodified)) // 4 5 { 6 // Read the hit count from the PlayerPrefs. 7 hitCountUnmodified = PlayerPrefs.GetInt(HitCountKeyUnmodified); // 5 8 } 9 if (PlayerPrefs.HasKey(HitCountKeyShift)) // 4 10 { 11 // Read the hit count from the PlayerPrefs. 12 hitCountShift = PlayerPrefs.GetInt(HitCountKeyShift); // 5 13 } 14 if (PlayerPrefs.HasKey(HitCountKeyControl)) // 4 15 { 16 // Read the hit count from the PlayerPrefs. 17 hitCountControl = PlayerPrefs.GetInt(HitCountKeyControl); // 5 18 } 19 }
As before, we first check if the key exists in the
PlayerPrefs
(4) and if so, we set the corresponding counter (5) to its value. This is fine for a simple example but here, you can already see that saving more complex data will bring PlayerPrefs
very soon to its limits if you do not want to write a lot of boilerplate code.Unity offers a detection for keyboard clicks and other input like a controller or the mouse via a class called
Input
. Using GetKey
, we can check if a specific key was held down the moment we register a mouse click.The documentation tells us about one important fact though:
Note: Input flags are not reset until Update. You should make all the Input calls in the Update Loop.
Therefore, we also need to implement the
Update()
function (6) where we check for the key and save it in the previously defined modifier
.The keys can be addressed via their name as string but the type safe way to do this is to use the class
KeyCode
, which defines every key necessary. For our case, this would be KeyCode.LeftShift
and KeyCode.LeftControl
.Those checks use
Input.GetKey()
(7) and if one of the two was found, it will be saved as the modifier
(8). If neither of them was pressed (9), we just reset modifier
to the default
(10) which we will use as a marker for an unmodified mouse click.1 private void Update() // 6 2 { 3 // Check if a key was pressed. 4 if (Input.GetKey(KeyCode.LeftShift)) // 7 5 { 6 // Set the LeftShift key. 7 modifier = KeyCode.LeftShift; // 8 8 } 9 else if (Input.GetKey(KeyCode.LeftControl)) // 7 10 { 11 // Set the LeftControl key. 12 modifier = KeyCode.LeftControl; // 8 13 } 14 else // 9 15 { 16 // In any other case reset to default and consider it unmodified. 17 modifier = default; // 10 18 } 19 }
The same triplet can then also be found in the click detection:
1 private void OnMouseDown() 2 { 3 // Check if a key was pressed. 4 switch (modifier) 5 { 6 case KeyCode.LeftShift: // 11 7 // Increment the hit count and set it to PlayerPrefs. 8 hitCountShift++; // 12 9 PlayerPrefs.SetInt(HitCountKeyShift, hitCountShift); // 15 10 break; 11 case KeyCode.LeftControl: // 11 12 // Increment the hit count and set it to PlayerPrefs. 13 hitCountControl++; // 14 PlayerPrefs.SetInt(HitCountKeyControl, hitCountControl); // 15 15 break; 16 default: // 13 17 // Increment the hit count and set it to PlayerPrefs. 18 hitCountUnmodified++; // 14 19 PlayerPrefs.SetInt(HitCountKeyUnmodified, hitCountUnmodified); // 15 20 break; 21 } 22 23 // Persist the data to disk. 24 PlayerPrefs.Save(); // 16 25 }
First we check if one of those two was held down while the click happened (11) and if so, increment the corresponding hit counter (12). If not (13), the
unmodfied
counter has to be incremented (14).Finally, we need to set each of those three counters individually (15) via
PlayerPrefs.SetInt()
using the three keys we defined earlier.Like in the previous example, we also call
Save()
(16) at the end to make sure data does not get lost if the game does not end normally.When switching back to the Unity editor, the script on the capsule should now look like this:
(See
PlayerPrefsExampleJson.cs
in the repository for the finished version.)In the previous two sections, we saw how to handle two simple examples of persisting data in
PlayerPrefs
. What if they get more complex than that? What if you want to structure and group data together?One possible approach would be to use the fact that
PlayerPrefs
can hold a string
and save a JSON
in there.First we need to figure out how to actually transform our data into JSON. The .NET framework as well as the
UnityEngine
framework offer a JSON serializer and deserializer to do this job for us. Both behave very similarly, but we will use Unity's own JsonUtility
, which performs better in Unity than other similar JSON solutions.Internally, this method uses the Unity serializer. Therefore, the object you pass in must be supported by the serializer. It must be a MonoBehaviour, ScriptableObject, or plain class/struct with the Serializable attribute applied. The types of fields that you want to be included must be supported by the serializer; unsupported fields will be ignored, as will private fields, static fields, and fields with the NonSerialized attribute applied.
In our case, since we are only saving simple data types (int) for now, that's fine. We can define a new class (1) and call it
HitCount
:1 // 1 2 private class HitCount 3 { 4 public int Unmodified; 5 public int Shift; 6 public int Control; 7 }
We will keep the Unity editor outlets the same (2):
1 // 2 2 [private int hitCountUnmodified = 0; ] 3 [private int hitCountShift = 0; ] 4 [private int hitCountControl = 0; ]
All those will eventually be saved into the same
PlayerPrefs
field, which means we only need one key (3):1 // 3 2 private const string HitCountKey = "HitCountKeyJson";
As before, the
modifier
will indicate which modifier was used:1 // 4 2 private KeyCode modifier = default;
In
Start()
, we then need to read the JSON. As before, we check if the PlayerPrefs
key exists (5) and then read the data, this time using GetString()
(as opposed to GetInt()
before).Transforming this JSON into the actual object is then done using
JsonUtility.FromJson()
(6), which takes the string as an argument. It's a generic function and we need to provide the information about which object this JSON is supposed to be representing—in this case, HitCount
.If the JSON can be read and transformed successfully, we can set the hit count fields (7) to their three values.
1 private void Start() 2 { 3 // 5 4 // Check if the key exists. If not, we never saved to it. 5 if (PlayerPrefs.HasKey(HitCountKey)) 6 { 7 // 6 8 var jsonString = PlayerPrefs.GetString(HitCountKey); 9 var hitCount = JsonUtility.FromJson<HitCount>(jsonString); 10 11 // 7 12 if (hitCount != null) 13 { 14 hitCountUnmodified = hitCount.Unmodified; 15 hitCountShift = hitCount.Shift; 16 hitCountControl = hitCount.Control; 17 } 18 } 19 }
The detection for the key that was pressed is identical to the extended example since it does not involve loading or saving any data but is just a check for the key during
Update()
:1 private void Update() // 8 2 { 3 // Check if a key was pressed. 4 if (Input.GetKey(KeyCode.LeftShift)) // 9 5 { 6 // Set the LeftShift key. 7 modifier = KeyCode.LeftShift; // 10 8 } 9 else if (Input.GetKey(KeyCode.LeftControl)) // 9 10 { 11 // Set the LeftControl key. 12 modifier = KeyCode.LeftControl; // 10 13 } 14 else // 11 15 { 16 // In any other case reset to default and consider it unmodified. 17 modifier = default; // 12 18 } 19 }
In a very similar fashion,
OnMouseDown()
needs to save the data whenever it's changed.1 private void OnMouseDown() 2 { 3 // Check if a key was pressed. 4 switch (modifier) 5 { 6 case KeyCode.LeftShift: // 13 7 // Increment the hit count and set it to PlayerPrefs. 8 hitCountShift++; // 14 9 break; 10 case KeyCode.LeftControl: // 13 11 // Increment the hit count and set it to PlayerPrefs. 12 hitCountControl++; // 14 13 break; 14 default: // 15 15 // Increment the hit count and set it to PlayerPrefs. 16 hitCountUnmodified++; // 16 17 break; 18 } 19 20 // 17 21 var updatedCount = new HitCount 22 { 23 Unmodified = hitCountUnmodified, 24 Shift = hitCountShift, 25 Control = hitCountControl, 26 }; 27 28 // 18 29 var jsonString = JsonUtility.ToJson(updatedCount); 30 PlayerPrefs.SetString(HitCountKey, jsonString); 31 PlayerPrefs.Save(); 32 }
Compared to before, you see that checking the key and increasing the counter (13 - 16) is basically unchanged except for the save part that is now a bit different.
First, we need to create a new
HitCount
object (17) and assign the three counts. Using JsonUtility.ToJson()
, we can then (18) create a JSON string from this object and set it using the PlayerPrefs
.Remember to also call
Save()
here to make sure data cannot get lost in case the game crashes without being able to call OnApplicationQuit()
.Run the game, and after you've clicked the capsule a couple of times with or without Shift and Control, have a look at the result. The following screenshot shows the Windows registry which is where the
PlayerPrefs
get saved.The location when using our example project is
HKEY_CURRENT_USER\SOFTWARE\Unity\UnityEditor\MongoDB Inc.\UnityPersistenceExample
and as you can see, our JSON is right there, saved in plain text. This is also one of the big downsides to keep in mind when using PlayerPrefs
: Data is not safe and can easily be edited when saved in plain text. Watch out for our future tutorial on encryption, which is one option to improve the safety of your data.In this tutorial, we have seen how to save and load data using
PlayerPrefs
. They are very simple and easy to use and a great choice for some simple data points. If it gets a bit more complex, you can save data using multiple fields or wrapping them into an object which can then be serialized using JSON
.What happens if you want to persist multiple objects of the same class? Or multiple classes? Maybe with relationships between them? And what if the structure of those objects changes?
As you see,
PlayerPrefs
get to their limits really fast—as easy as they are to use as limited they are.In future tutorials, we will explore other options to persist data in Unity and how they can solve some or all of the above questions.