Saving game progress in Unity – PlayerPrefs and Serialization


Can you imagine that every time you wake up you live the same day over and over again? This can be fun if it’s a Groundhog’s day, but not particularly for your players.
Whenever you want to save your game progress, user settings, or objects modified during the gameplay you have to think about your saving system. In this series of tutorials I will try to show you some examples of saving data in Unity.

It will consist of following topics:

  • Using PlayerPrefs
  • Basics of serialization
  • Performance test for different types of serialization

Project with the source code can be found on Github.

 


 

PlayerPrefs

This method was inspired by “Shared Preferences” on Android platform. It is very easy to use, but limited as well. The only types you are allowed to save are: float, int, string.
The example usage could look like this:

 

PlayerPrefs may appear to be fairly limited because you can store just basic types. Another problem is not the best Read/write time. Let me show you one trick that can make it more performant.

Enum flags
Let’s imagine that in your level, player is supposed to collect some items. ItemType will be an enum that defines type of this collectible item:

As you probably noticed I’ve assigned an integer value to every position in enum and it’s always power of 2. Now if you get binary value from it, you will get:

Decimal Binary
1 0000 0001
2 0000 0010
4 0000 0100
8 0000 1000
16 0001 0000
32 0010 0000
64 0100 0000
128 1000 0000

This is simply setting a bool flat on consecutive bits of an integer.
If you know that in C# integer is 32 bits long (1 bit for sign), then the least significant bit is 1 and the most significant bit is 1,073,741,824. That means you can store 31 elements using just one integer!

Let’s write a method that will add an item to your collection:

 


 

Serialization

Serialization is the process of converting an object into a stream of bytes in order to store it on your drive, send it somewhere etc. Object stored in this way can be recreated when you need it (deserialization). If you use serialization you are no longer limited by basic types like string, float, int. You can store and persist object of any class you want!

There are 3 different types of serialization available in Unity:
– Binary – data is being serialized by BinaryFormatter. Data is saved in a binary format (to read it you first need to deserialize it)
– XML – saved in popular XML format
– JSON – saved by JSON – file is saved as a text so it’s readable without deserialising, good performance

Here is the code we used for serialization/deserialization:

Let me also show you the difference in appearance between XML and JSON:

JSON serialization output

JSON serialization output

XML serialization output

XML serialization output

After your data is saved, you can open generated files at any moment and check their content or even modify them. If you modify your data file, the next time you will load the data it will of course include your modifications. How cool is that!


Performance tests

In this paragraph we will try to show you my performance test with saving an object holding array of monsters. For the tests we used following classes:

For checking the execution time a System.Diagnostics.Stopwatch was used. I created a list of 10, 1 000, 100 000 and 1 000 000 objects and then serialized it and saved to a file. You can see all of my results in tables below:

10 elements XML Binary JSON
Size 1,274 bytes 805 bytes 462 bytes
Write 13 ms 24 ms 2 ms
Read 4 ms 8 ms 0.4 ms

 

1000 XML Binary JSON
Size 113 KB 32 KB 51 KB
Write 36 ms 27 ms 4 ms
Read 4 ms 17 ms 2 ms

 

100 000 XML Binary JSON
Size 11.9 MB 4 MB 5.7 MB
Write 2016 ms 591 ms 277 ms
Read 3986 ms 50 518 ms 297 ms

 

1 000 000 XML Binary JSON
Size 121.7 MB 40 MB 59 MB
Write 18 964 ms 7 827 ms 3 399 ms
Read 38 152 ms way too long 3 090 ms

 

I didn’t put the read time chart, because it took too long for BinaryFormatter to finish deserializing in a reasonable time.

As you can see JSON had the fastest read/write time. Binary had the smallest file size but also the longest read time. XML did quite OK, but still much worse than JSON.
It’s also to worth mention other serialization techniques. Two honorable mentions are FlatBuffers and ProtoBuffers (Protocol buffers). They give very promising results beating even JSON, but since they are not part of the Unity engine and you have to include additional libraries to use them I will not describe them here.


Summary

That’s it! Thank you very much for reading our tutorial. I understand that we barely scratched the surface of this topic but we hope you enjoyed it. If you have any questions please leave us a  comment 🙂

Entire project can be found on Github

Share:

2 Comments

Add yours
  1. 1
    FWCorey

    BinaryFormatter is not really an ideal method for binary serialization. Custom binary serialization where each stored class has a GetBytes() method and a public MyClass(byte[] bytes) constructor can outperform all of these by orders of magnitude with extremely small file sizes. It’s a more advanced technique and requires a bit more planning, but it has the advantage of providing data that can be used with any C# Stream.

    In one project I’ve worked on a scene with 12000 logical tiles, that each had several attribute components and 8000 objects, also with varied attributes, placed on those tiles was serialized and uploaded to a server in less than 300ms or downloaded and deserialized in less than 600ms.

    Maybe expand the article to include a brief overview and tips for new programmers trying their hand at those techniques. They are no harder than using PlayerPrefs after all and only limited for a newbie to the types supported by BitConverter.

  2. 2
    Vasyl

    Thank you very much for this great atricle! I walked through this when I was creating save system for my current project. I wish I read it before 🙂 But I want to notice that you have a little of redundant code in here. You shouldn’t call Close() and Dispose() methods manually. At the end of the using block the Dispose method is automatically called which will take care to free unmanaged resources. As for the Close() method it calls Dispose().

+ Leave a Comment