Binary Serialization in Unity

Facebook
Twitter
LinkedIn

Introduction

This weekend I hosted the Game Dev Knights of University of Central Florida Unity introduction workshop. We weren’t able to complete the workshop however we did cover a lot of ground considering the audience and what we did cover was covered extensively. A topic I was really excited to cover was a saving and loading system that allows for more robust data saving. I decided I’d do a blog post walkthrough of how to go about binary serialization and saving in Unity.

Getting Started

In my last Blog post, “Dynamic Objects Saving and Loading“, I go through the process of doing everything but the core file saving and loading part. I thought I’d mention this post to show how you might be able to expand on what we’re covering here. Serialization is a way of converting an object into a stream of bytes, in order to save or read. There are different ways you can serialize. JSON and XML are common ways, for this example however we will be just saving it as bytes. First we want to create our Data Model(s). The data model is what we will be saving in our data file. It is going to contain any information that needs to be saved. It’s smart to have multiple data model types, for example one for GameOptions and PlayerProgress. GameOptions containing resolution selection, volume, quality options, etc. PlayerProgress containing the scene they left in, position, level, weapon, etc. The DataModel is simply a class in this example.
[System.Serializable]
public class DataClass
{
    public int variable;
    public float variableFloat;
}
This example class has two variables, and integer and float. Binary Serialization lets you serialize quite a number of data types. If you can see it in Unity’s inspector it can handle it, along with a few extra. From Lists, Dictionaries, to ints, to structs to other classes.

Saving and Loading

Next lets write the code to actually save and load bytes. This is using System.IO, so its quite simple.
public void Save(byte[] data, string file)
{
    string path = Application.dataPath + "/" + "OurGame_Data";
    string filePath = path + "/" + file + ".dat";

    if (!Directory.Exists(path))
    {
        Directory.CreateDirectory(path);
        FileStream newFile = File.Create(filePath);
        newFile.Close();
    }

    // Create the file.
    File.WriteAllBytes(filePath, data);
}
    
public byte[] Load(string file)
{
    string path = Application.dataPath + "/" + "OurGame_Data" ;
    string filePath = path + "/" + file + ".dat";

    if (!Directory.Exists(path))
    {
        return null;
    }

    return File.ReadAllBytes(filePath);
}
This code is creating a file at the Application.dataPath. This is the path to our game’s .exe location under the game data folder. Then we create another folder called OurGame_Data, this should be replaced to anything you want or nothing at all and save in that folder itself. Next we just do self-explanatory things such as checking if the path exists for saving, if it doesn’t then create the path and file. For loading if it doesn’t then it just returns null, nothing. Then using File.WriteAllBytes and File.ReadAllBytes to read and write.

What now?

Well we can now save and load bytes. But what bytes should I be saving and loading and how do I read it? This is where binary serialization comes in. It’s going to allow us to convert a class’s variable contents to an array of bytes. This array can be super big, imagine a file, it can reach gigabytes. Taking more time to save to the hard drive and load. So keep contents small and manageable.
// Serialization is converting a class or data structure into bytes or a data format which can be saved.
byte[] data = null;
// Create memory stream that the Binary Formater relies on
MemoryStream memorySteam = new MemoryStream();
// Binary formatter is used to serialize to Binary.
BinaryFormatter formatter = new BinaryFormatter();
// Serialize the data instance
formatter.Serialize(memorySteam, dataInstance);
data = memorySteam.GetBuffer();
memorySteam.Flush();
memorySteam.Position = 0;
memorySteam.Close();
Save(data, "Game");
I’ve added comments to help explain the code and what’s happening. You might’ve noticed data streams being closed and opened. Data streams are used to transfer data and need to be closed to avoid entering a data transfer that is happening. That’s why you cannot save and load at the same time. We then call our Save and Load methods from before.

Conclusion

Having control over data saving and loading is important in game development. Relying on PlayerPrefs for data saving can be risky if you encounter a platform that doesn’t allow PlayerPrefs or support it. For example when writing for the Nintendo Switch using Unity, you could not interact with PlayerPrefs OR System.IO, which resulted us in altering our serialization save methods to allow us to use their I/O library. This was easier to do since it was a single step compared to having to port completely to I/O from Player Prefs.

More to explore

Designing an Educational Game

A blog post covering my experience designing an educational game for 5th grade students in the US following common core standards.