Saturday 15 July 2017

Saving data

At some point in your application, you are very likely to need to save some data permanently. Whether it is the score value, the current game state, the number of unlockables unlocked, you probably need a way to retain this information in order to retrieve it later when the user returns to the app.

In Unity there are different ways to save data on the disk and I'm going to show a few in this post.

1. Player Preferences

This is by far the easiest way to store information permanently. If you have ever done any coding for the Android platform, Player Preferences in Unity works just like Shared Preferences.

It is a Unity built in feature, it automatically creates a file on the disk and store information one by one. Let's assume we have a class which stores some variables the we need to save, like so:

public class SaveData  {

 public string playerName = "";
 public int totalScore = 0;
 public int playerLevel = 0;
 public bool maxLevelReached = false;

 public void print()
 {

  string toPrint = "Player Name: " + playerName + "\n" +
                   "Total Score: " + totalScore + "\n" +
                   "Player Level" + playerLevel + "\n" +
                   "Max Level Reached: " + maxLevelReached;

  Debug.Log (toPrint);

 }

}

In order to save all the variables we need to do something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void SaveDataPlayerPrefs()
 {

  SaveData data = new SaveData ();

  data.playerName = "Jeffrey the mighty hero";
  data.playerLevel = 90;
  data.maxLevelReached = true;
  data.totalScore = 10;

  PlayerPrefs.SetInt ("PlayerLevel", data.playerLevel);
  PlayerPrefs.SetInt ("TotalScore", data.totalScore);
  PlayerPrefs.SetString ("PlayerName", data.playerName);
  PlayerPrefs.SetInt ("MaxLevelReached", data.maxLevelReached ? 1 : 0);

 }

As you can see, we need to write a line of code for each variable we want to save. Additionally, every parameter needs to be associated with a key string value, which is then used to retrieve the information. Finally, if you look at line 14 you will notice that something strange happens. That is because PlayerPrefs does not support boolean variables, therefore we need to find a workaround in order to store this type of variable. In this case, I simply turned the boolean into an integer.

If we want to load the information we would proceed as follow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void ReadPlayerPrefs()
 {
  SaveData data = new SaveData ();

  data.playerLevel = PlayerPrefs.GetInt ("PlayerLevel",-1);
  data.playerName = PlayerPrefs.GetString ("PlayerName", "_no_name_");
  data.totalScore = PlayerPrefs.GetInt ("TotalScore", -1);

  int maxLevelReachedInt = PlayerPrefs.GetInt ("MaxLevelReached", -1);

  if (maxLevelReachedInt == 1)
   data.maxLevelReached = true;
  else if (maxLevelReachedInt == 0)
   data.maxLevelReached = false;
  else
   Debug.Log ("Problem occured while loading PlayerPrefs");

  data.print ();

 }

We can retrieve all the data we saved previously using the keys we used to identify each variable. We also need to provide a default value in case the data we are trying to find is not available. Notice how I had to check that the integer was a a 1 or a 0  in order to set the boolean variable in our SaveData object. To be honest, we should perform a check on each variable to make sure that the information has been retrieved correctly.

The console should produce this output:


2. Binary formatter

Binary formatter is a "pure C#" method for serializing objects. Simply put, it turns objects data in a bunch of 1s and 0s.

Firstly, in order to use it, we need to mark the SaveData class as Serializable. We can do that very easily by simply adding [Serializable] before the class declaration.

This is the routine which uses the binary formatter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public void SaveDataBinary()
 {
  string path  = "Assets/Res/SaveFiles/SaveDataFile.data";

  SaveData data = new SaveData ();

  data.playerName = "Jeffrey the mighty hero";
  data.playerLevel = 90;
  data.maxLevelReached = true;
  data.totalScore = 10;

  BinaryFormatter bf = new BinaryFormatter ();
  FileStream fs = File.Open (path, FileMode.Create);
 
  bf.Serialize (fs, data);
  fs.Close ();
 }

After we define the path, which is the location where the file will be created at, we can get a BinaryFormatter object and a FileStream object, passing the path and a FileMode parameter. In this case, we want to create a file. We then simply proceed by telling the formatter to serialize our data object, which is going to be stored in the "SaveDataFile.data" file.

When it's time to get everything back, we can get the object as shown:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 public void LoadDataBinary()
 {
  string path  = "Assets/Res/SaveFiles/SaveDataFile.data";
  SaveData data = null;

  BinaryFormatter bs = new BinaryFormatter ();
  FileStream fs = File.Open (path, FileMode.Open);
  data = (SaveData)bs.Deserialize (fs);

  data.print ();
 }

Obviously, the file path and name must be the same as the ones we used when we saved the object. We can then create an empty SaveData object and deserialize our file. Notice how we need to cast to a SaveData type as the deserialize method returns a generic objecti type.

3. JSON

Unity has recently added a Json utility feature in order to pass data around using the now popolar JSON format. I believe there's no need to introduce JSON in this post, there's plenty of documentation on the web, but if you have never heard of it all you need to know is that is a particular type of format used to save objects, very easy to use and read.

Here's how you can save your object in JSON format in Unity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void SaveDataJson()
 {
  string path  = "Assets/Res/SaveFiles/SaveDataFile.json";

  SaveData data = new SaveData ();

  data.playerName = "Jeffrey the mighty hero";
  data.playerLevel = 90;
  data.maxLevelReached = true;
  data.totalScore = 10;

  string datajson = JsonUtility.ToJson (data);

  FileStream fs = new FileStream (path, FileMode.Create);

  StreamWriter sm = new StreamWriter (fs);

  sm.Write (datajson);

  sm.Close ();
  fs.Close ();

 }

In a very similar process followed when using the binary formatter, we indeed create a file, with the extension .json. The object is then converted to JSON format, which is really nothing but a string, which is then written into the file.

Once saved, the file should be preset on your device and you should be able to open it. It should look like this:

1
2
3
{"playerName":"Jeffrey the mighty hero saved in Json",
"totalScore":40,"playerLevel":40,
"maxLevelReached":false}

This is your object saved in JSON format.

Below, a routine that get the information back from this file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void LoadDataJson()
 {
  string path  = "Assets/Res/SaveFiles/SaveDataFile.json";

  StreamReader reader = new StreamReader (path);

  string saveDataString = "";

  while (!reader.EndOfStream) {

   saveDataString += reader.ReadLine ();
  
  }

  reader.Close ();

  SaveData saveData = JsonUtility.FromJson<SaveData> (saveDataString);

  saveData.print ();
 }

I used the StreamReader to read the .json file as if it was a simple text file and I stored the result into a string.

This variable is then passed into the JsonUtility method which parses the string type and converts it into the object specified in the <>, in our case, our SaveData type. It doesn't get any easier then that.

4. Scriptable Objects

Ok, perhaps this last method does not belong to this list. Yes you can save data permanently using Sciptable Objects, however, it is not recommended to do so during runtime. SOs as mostly used (and useful) in the editor. They are a very convenient way to create assets files which can be modified by simply setting their parameters.

All we have to do is to create a new class identical to the SaveData class we wrote at the beginning of this post, only thus tune we would inherit from Scriptable Object:


1
2
3
4
5
6
7
8
9
[CreateAssetMenu]
public class SaveDataSO : ScriptableObject {

 public string playerName = "";
 public int totalScore = 0;
 public int lastLevelPlayer = 0;
 public bool maxLevelReached = false;

}

Once we created the asset file, we can simply reference it anywhere else in the project and simply modify its parameters, and every change made is permanent.

A very easy method for sure, however, using SOs can sometimes create headaches: common issues when saving information this way are decoupling and the fact that SOs can only serialize certain types of parameters, so you should definitely read more about this should you intend to use scriptable objects.

Conclusion

As always, there are different ways to perform a task in programming. These examples only show the basic of data storage, and they are all used for local saves. Depending on what type of game you are developing, you might need to save data in the cloud, perhaps to allow the user to access your application (and the data saved) from different devices. In that case you should look into services offered by Google Play or Amazon Web Services for example.

Additionally, none of these examples showed any protection. Users could easily get to the data files and modify the content to their advantage, which may or may not affect the overall experience. Implementing safety is of course always a good idea.

Finally, if you find yourself with not enough time/resources/will to write your own save manager, a simple search on the asset store will allow you to get access to a wide variety of data saving scripts with all different levels of complexity and protection, choosing is always a matter of what is really needed in your project.