Saving and Loading Data in Unity
One way to save and load relatively complex sets of data, in the unity game engine.
Motivation
So if you are working on a relatively large or complicated game, you will want to save a lot of data about the player: where in the world they are, how many lives they have, what items they have equipped, what quests they’ve accepted, etc. What is more, you will want some form of “secure” saving method, so that the user cannot easily tamper with their save. For these reasons, the “standard” Unity technique, of saving things out to PlayerPrefs
, will not suffice; it is human-readable (and thus insecure for our purposes) and is unwieldy to deal with, especially when handling List
s.
Therefore, we turn to binary serialisation, which allows us to store a lot of data compactly and easily, and (by our definition) securely.
Saving
Before we actually save any data, there are a few things we need to sort out. First, we create a SaveData
class. This represents all the data we want to save out. An example is shown below:
/* This stores the info to be saved.*/ [Serializable()] public class SaveData : ISerializable { /* The values, which will be edited during gameplay.*/ public List<int> m_inventoryItems = new List<int>(); //the integers are the item IDs public int[] m_equippedItems = new int[4]; //id's for the items currently equipped /* Called automatically. Required for the ISerializable class to be serialized properly.*/ public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("inventory", (m_inventoryItems)); info.AddValue("equipped", (m_equippedItems)); } }
So what do we have here? At the top, we have the Serializable()
tag. This tells the compiler that this class should be able to be serialised out to disc. We then see that the class implements the ISerializable
interface, which provides the necessaries for us to control how we serialise the class (rather than having some default behaviour). We then have the data we wish to save. Finally, we have a method which is called automatically when the class is serialised; in the method, we push our data as, essentially, a key-value pair into the SerializationInfo
instance (which stores information about how to serialise/deserialise the class, shockingly. Never would have guessed, would you).
Next, we have a class which actually handles the saving and loading itself:
/* We access this class from other scripts.*/ public static class SaveLoad { public static void Save(string savePath) { SaveData data = new SaveData(); /* Getting the info from the player's script.*/ for(int i = 0; i < 4; i++) { if(player.equipped[i] != null) { data.equippedItems[i] = player.equipped[i]._data; } } for(int j = 0; j < player.inventory.Count; j++) { data.inventoryItems.Add(player.inventory[j].id); } Stream strem = File.Open(savePath, FileMode.Create); //FileMode.Create creates a file if it doesn't exist, or overwrites the existing file of the same name. BinaryFormatter form = new BinaryFormatter(); form.Binder = new VersionDeserializationBinder(); form.Serialize(strem, data); strem.Close(); } }
This might look quite scary, so let’s break it down. We make the class static just for convenience; it is not a strict necessity. The key is the Save()
method. Firstly, this creates a new SaveData
instance to store the data. Next, it puts all the necessary data into the SaveData
instance. Then comes the more thorny part. We create a Stream
, using the provided filename. This basically opens the desired file in “Create” mode. We then make a BinaryFormatter
, which handles turning our data into binary form, so we don’t have to do it manually (which would be a right faff).
The next line bears close inspection. The BinaryFormatter
requires a “Binder”, which tells the serialiser basically how to convert a given type into serialised data, so that when we load this again, it will be able to understand what things were int
s, which were float
s, etc. The issue is that the “identifier”, if you will, for each type changes when the assembly changes, and the assembly changes each time Unity builds your project. That’s no good. So we create this class here:
/** * As Unity assigns a new assembly name on each compile, we use this to make sure we get a constant assembly name. * * DO NOT CHANGE THIS. **/ public sealed class VersionDeserializationBinder : SerializationBinder { public override Type BindToType(string assemblyName, string typeName) { if(!string.IsNullOrEmpty(assemblyName) && !string.IsNullOrEmpty(typeName)) { Type typeToDeserialize = null; assemblyName = Assembly.GetExecutingAssembly().FullName; typeToDeserialize = Type.GetType(String.Format("{0}, {1}", typeName, assemblyName)); return typeToDeserialize; } return null; } }
It is not super-important that you understand the ins and outs of this, but suffice it to say that this takes into account the changing assembly name, allowing different builds to use the same saved data.
Scooting on back up to our Save()
method, we see that after setting the binder, we do the actual seialisation, and then close the file stream. And that is it. Our data is now saved to that file, in binary format. Excellent! But how do we get the data back out again?
Loading
To handle loading, let us add some things to our SaveLoad
class:
[Serializable()] public class SaveData : ISerializable { /* The values, which will be edited during gameplay.*/ public List<int> m_inventoryItems = new List<int>(); //the integers are the item IDs public int[] m_equippedItems = new int[4]; //id's for the items currently equipped /* Standard constructor.*/ public SaveData() { } /* This constructor is called by ISerializable (automatically)*/ public SaveData(SerializationInfo info, StreamingContext context) { /* Take the values from "info" and pop them into the appropriate fields.*/ m_inventoryItems = (List<int>)info.GetValue("inventory", typeof(List<int>)); m_equippedItems = (int[])info.GetValue("equipped", typeof(int[])); } /* Called automatically. Required for the ISerializable class to be serialized properly.*/ public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("inventory", (m_inventoryItems)); info.AddValue("equipped", (m_equippedItems)); } }
So we have added a couple of constructors. First we have a default constructor, which is pretty boring. But next we have a more interesting contructor. This is called automatically when deserialising our data out FROM the disc, TO an instance of this class. The program will, in the background, look for something called “inventory”, and grab the data from the SerializationInfo
(remember that we put the data in there earlier, when saving), before plopping that data into the member variable m_inventoryItems
. The same is done for the equipped items.
We now move on back to our SaveLoad
class, and add a Load()
method, which looks like this:
public static void Load(string filePath) { SaveData data = new SaveData(); Stream strem = File.Open(filePath, FileMode.Open); BinaryFormatter form = new BinaryFormatter(); form.Binder = new VersionDeserializationBinder(); data = (SaveData)form.Deserialize(strem); strem.Close(); /* We then use "data" to access our values.*/ }
Here, we create a new SaveData
instance, and essentially reverse what we did when saving. We open the save file, set up a BinaryFormatter
with the appropriate binder, and deserialise the data. And that is it.
Wrap-Up
There you have it: a way to save out your data to a file simply, effectively, and securely. Takes a bit of set-up, but once it’s done, you can add in new types of data to save relatively easily (just edit the SaveData
class). My advice in general is to keep using PlayerPrefs
for saving stuff like key bindings, which are not exactly things which the player can use to cheat with. Meanwhile, you should use this method (or another like it) to save data which is in large quantities, or which you do not want the player to tamper with.
Hope you found this interesting, and I’ll see you next time!
https://cow-co.gitlab.io/BlogPosts/SavingLoadingUnity.html
http://stackoverflow.com/questions/32903751/deserialization-slow-performance-in-unity
Assets, Objects and serialization
Checked with version: 5.4
–
Difficulty: Advanced
This is the second chapter in a series of articles covering Assets, Resources and resource management in Unity 5.
This chapter covers the deep internals of Unity’s serialization system and how Unity maintains robust references between different Objects, both in the Unity Editor and at runtime. It also discusses the technical distinctions between Objects and Assets. The topics covered here are fundamental to understanding how to efficiently load and unload Assets in Unity. Proper Asset management is crucial to keeping loading times short and memory usage low.
Save Compute Buffer at any point in time.
Reload – with Pause on – with Settings.
Edit and Scale.
Then Get to Polish for Demo on Tuesday
Demonstrate UI – Natural Flow
http://kerbyrosanes.com/