[Unity] A beginner’s guide to Save, Load and Protect your Game Data.

Preface.

Game data is an important part of our game thus “creating, storing and loading it” are crucial. Moreover, online games allow players to visit each other home which requires exchange and side-load game data between players.

Imaging you are sailing in middle of the ocean, taking a wrong turn mean your crew is doomed. The same rule is true when dealing with game data, a poor designed one often required a rework which may break in future update.

In 2017, I work on a Tower Defence game call Galaxy Gunner. Our team make a lot of changes, one of them causes not-compatible data when upgrade to newest game version. This bug slips out into our production release and cost us a lot of money. Our active users and D1 retention drop by 30%. After that, it takes one month to recover our game stats from this incident. The root of the evil is “BinaryFormatter” which is used to save and load our data. When our data structure changes, the old data becomes invalid thus reset our player progress. Our hot fix requires implementing a helper class to parse the old data and create new one from it. It works well but this helper class needs baby-sitting each time we updated our game. Such a tire-some errand.

This tutorial should provide you with basic knowledge about common technique to handle game data and serve as foundation for further improvements. Use it as a beginner guide to avoid trap card when design your game data.

Where should I store my data?

Your data must save in Application.persistentDataPath (A special folder contains all generated data from your game) or using PlayerPrefs (A special config file which is stored differently by platforms). In Android Platform, your data will be automatic backup with your account. In iOS, your game data will lose up on uninstall. Beware that automatic back up might store your old data which lose your player’s newest progress. To fully support backup game data, a storing service is required (Firebase Filestore, Firebase Database, Unity Cloud Save …).

Common ways to work with Game Data in Unity.

Sample of a simple game data.

[System.Serializable]
public class GameData
{
    public string playerName;
    public int score;//current player score
    public int[] levelStates;
    //0 mean level have not pass
    // 1 mean player gain 1 star
    // 2 mean player gain 2 stars
    // 3 mean player gain 3 stars
}

We will use the sample above as reference data for our save-load system.

PlayerPrefs.

PlayerPrefs is a class that stores Player preferences between game sessions. It can store string, float and integer values into the user’s platform registry in key-value pair format.

It is very handy to store immediate data such as timestamp (open game timestamp, unlock timestamp), reward progress and settings data (audio volume, lighting…).

Due to its limitations, PlayerPrefs isn’t suit for saving complex game data which requires backup and cloud saving from multi-source.

Here is the sample code to save and load our Game Data using PlayerPrefs. This block of code demonstrates the complexity of PlayerPrefs when work with a simple data set.

public class DataController : MonoBehaviour
{
    private GameData gameData;
    public void LoadDataPlayerPrefs()
    {
        gameData = new GameData();
        gameData.playerName = PlayerPrefs.GetString("player_name", "player");
        gameData.score = PlayerPrefs.GetInt("score", 0);
        gameData.levelStates = new int[0];
        var data = PlayerPrefs.GetString("level_states", "0_0_0");
        var levelStates = data.Split('_');
        gameData.levelStates = new int[levelStates.Length];
        for (int i = 0; i < levelStates.Length; i++)
            gameData.levelStates[i] = int.Parse(levelStates[i]);
    }
    public void SaveDataPlayerPrefs()
    {
        PlayerPrefs.SetString("player_name", gameData.playerName);
        PlayerPrefs.SetInt("score", gameData.score);
        //this step generate a lot "Garbage"
        System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder(gameData.levelStates.Length);
        for (int i = 0; i < gameData.levelStates.Length - 1; i++)
            stringBuilder.Append(gameData.levelStates[i] + "_");
        stringBuilder.Append(gameData.levelStates[gameData.levelStates.Length - 1]);
        PlayerPrefs.SetString("level_states", stringBuilder.ToString());
    }
}

Here is the saved data using PlayerPrefs (Captured in register).

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Unity\UnityEditor\DefaultCompany\textmeshpro_utils]
"player_name_h2442731886"=hex:50,6c,61,79,65,72,00
"score_h195786797"=dword:00000000
"level_states_h931865480"=hex:30,5f,30,5f,30,00

As you can see, PlayerPrefs is not born to handle complex data structure. It takes a shit load of code to parse a simple integer array. As your data grows, the parsing logic will soon become unmaintainable.

BinaryFormatter.

BinaryFormatter is a DotNet library to store your objects in a binary format directly.

It is very fast and can save, load almost everything in your game. But it comes with a heavy price. The data and your code are strictly entangled, sometimes add, remove or change the order of a class properties might break your data. Recently, BinaryFormatter is marked deprecated and should be avoid in new project due to its security risks.

Here is sample code to save and load game data in binary format. It is tidier than PlayerPrefs with the same data set.

public class DataController : MonoBehaviour
{
    private GameData gameData;
    public void LoadDataBinary()
    {
        using (System.IO.FileStream fileStream = System.IO.File.Open(GetDataPath(), System.IO.FileMode.OpenOrCreate))
        {
            System.Runtime.Serialization.Formatters.Binary.BinaryFormatter binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            gameData = (GameData)binaryFormatter.Deserialize(fileStream);
        }
    }
    public void SaveDataBinary()
    {
        using (System.IO.FileStream fileStream = System.IO.File.Open(GetDataPath(), System.IO.FileMode.OpenOrCreate))
        {
            System.Runtime.Serialization.Formatters.Binary.BinaryFormatter binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            binaryFormatter.Serialize(fileStream, gameData);
        }
    }
    public static string GetDataPath()
    {
        return Application.persistentDataPath + "/data.dat";
    }
}

Here is the saved data using Binary Format. (Not actual binary data, some bytes are lost when they are converted to texts).

    ÿÿÿÿ          FAssembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null   GameData   
playerNamescorelevelStates       Player    	                     

XML.

XML stands for Extensible Markup Language. It defines a set of rules for encoding data in a format that is human-readable and machine-readable.

XML is very versatile, It can represent almost everything in your game and provide access to data via “node” and “path”.

The main problem with XML is its storage size (heavy used of tag) and parse speed which put a heavy pressure into our game resources.

Here is sample code to handle game data using XML.

public class DataController : MonoBehaviour
{
    private GameData gameData;
    public void SaveDataXML()
    {
        using (System.IO.FileStream stream = System.IO.File.Open(GetDataPath(), System.IO.FileMode.OpenOrCreate))
        {
            System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(GameData));
            serializer.Serialize(stream, gameData);
        }
    }
    public void LoadDataXML()
    {
        using (System.IO.FileStream stream = System.IO.File.Open(GetDataPath(), System.IO.FileMode.OpenOrCreate))
        {
            System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(GameData));
            gameData = (GameData)serializer.Deserialize(stream);
        }
    }
    public static string GetDataPath()
    {
        return Application.persistentDataPath + "/data.dat";
    }
}

Here is the saved data using XML Format.

<?xml version="1.0"?>
<GameData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <playerName>Player</playerName>
  <score>0</score>
  <levelStates>
    <int>0</int>
    <int>0</int>
    <int>0</int>
  </levelStates>
</GameData>

Unlike Binary Formatter save file, our XML one is human-readable which is very useful in development phase.

Json.

Json stands for JavaScript Object Notation. It is human-readable data format which is very handy for debug your save data in early development stage. Unlike XML, Json use key-value pairs to represent data instead of tags. With the same data set, a Json data file is lighter than an XML data file.

Unity has JsonUtility as built-in Json Parser. JsonUtility is fast and garbage-minimum with the tradeoff of flexibility. If you can’t serialize a field in the inspector, JsonUtility can’t serialize it too.

Here is sample Json Parsing code to handle game data.

public class DataController : MonoBehaviour
{
    private GameData gameData;
    public void LoadDataJson()
    {
        if (System.IO.File.Exists(GetDataPath()))
        {
            string data = System.IO.File.ReadAllText(GetDataPath());
            gameData = JsonUtility.FromJson<GameData>(data);
        }
        else
            gameData = new GameData();
    }
    public void SaveDataJson()
    {
        string origin = JsonUtility.ToJson(gameData);
        System.IO.File.WriteAllText(GetDataPath(), origin);
    }
    public static string GetDataPath()
    {
        return Application.persistentDataPath + "/data.dat";
    }
}

Here is the saved data using Json Format. (data.dat)

{
  "playerName": "Player",
  "score": 0,
  "levelStates": [
    0,
    0,
    0
  ]
}

As you can see, Json data file is more packed than XML one which greatly reduce storage size and parsing power. Due to various reasons, I prefer Json to XML when working with my game data.

Encrypt your data.

Encrypt data don’t make your game more secure. With enough resources, attacker can revert-engine (IL2CppDumper) to get the encrypt-decrypt key in your code (you still need to store them somewhere in your game in order to save and load your encrypted data). Even without decrypt your data, attacker could use memory tools (Cheat Engine) to directly modify your runtime variables thus by-pass our security layer.

I only use simple encryption to create a soft boundary between data and normal user. If real security is involved, you don’t let the game client touch the data. Important logics should be moved into your backend, the clients will send “command” and wait for the results from the server. Using this workflow, you can add several security layers inside your backend for data-tampering checking, speed hack, physic cheating…

Data encryption using XOR Cipher.

XOR Cipher is a symmetric encryption algorithm that operates using XOR operator. A string of text can be encrypted by applying the bitwise XOR operator to every character using a given key. To decrypt the output, reapply the XOR operator with the key will remove the cipher.

Here is sample code which apply XOR Cipher to your data before storing it into player devices.

public class DataController : MonoBehaviour
{
    private GameData gameData;
    public void LoadDataJsonEncrypted()
    {
        if (System.IO.File.Exists(GetDataPath()))
        {
            string encrypted = System.IO.File.ReadAllText(GetDataPath());
            string origin = XOROperator(encrypted, "12345678");
            gameData = JsonUtility.FromJson<GameData>(origin);
        }
        else
            gameData = new GameData();
    }
    public void SaveDataJsonEncrypted()
    {
        string origin = JsonUtility.ToJson(gameData);
        string encrypted = XOROperator(origin, "12345678");
        System.IO.File.WriteAllText(GetDataPath(), origin);
    }
    public static string XOROperator(string input, string key)
    {
        char[] output = new char[input.Length];
        for (int i = 0; i < input.Length; i++)
            output[i] = (char)(input[i] ^ key[i % key.Length]);
        return new string(output);
    }

Encrypted JSON data using XOR Cipher. Some characters are missed due to WordPress limitations.

JCXTORJS^Qh]SJQGBQ\FP
_QCS[kESGQF
ckJ

Variables Protection using XOR Cipher.

We will use XOR operator to obfuscate player score in memory in order to prevent memory-cheating. The SafeInt struct changes its memory address and refreshes encrypt key each time “score” changes its value thus attacker will have a hard time when use memory tool to modify our protected variables.

[System.Serializable]
public class GameData
{
    public string playerName;
    public SafeInt score;//safe guard current player score
    public int[] levelStates;
}
[System.Serializable]
public struct SafeInt
{
    private int key;
    private int encryptedValue;
    public SafeInt(int originValue)
    {
        key = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
        encryptedValue = originValue ^ key;
    }
    public int Value()
    {
        return encryptedValue ^ key;
    }
    public override string ToString()
    {
        return Value().ToString();
    }
    public static SafeInt operator +(SafeInt i1, SafeInt i2)
    {
        return new SafeInt(i1.Value() + i2.Value());
    }
    public static SafeInt operator -(SafeInt i1, SafeInt i2)
    {
        return new SafeInt(i1.Value() - i2.Value());
    }
    public static SafeInt operator *(SafeInt i1, SafeInt i2)
    {
        return new SafeInt(i1.Value() * i2.Value());
    }
    public static SafeInt operator /(SafeInt i1, SafeInt i2)
    {
        return new SafeInt(i1.Value() / i2.Value());
    }
}

Useful resources.

Persistent data: How to save your game states and settings. This is a very good blog about persistent game data in Unity.

Script serialization. This document provides knowledge about Unity Serialization process and some common technique to optimize them. You can use this document as a starting point to serialize un-support fields (such as multi-dimensions array, dictionary…) and save them with your game data.

A practical tutorial to hack (and protect) Unity games. This is an old blog (since Mono era) which describes various way to hijack a game. I use his SafeFloat as reference to create my SafeInt (XOR operator instead of value shift).

Save Data with BinaryWriter and BinaryReader. A useful article explains pro and cons of BinaryWriter and BinaryReader.

Conclusion.

This is the end of my short introduction about working with Game Data in Unity. Keep in mind that big thing tends to break, keep your data smaller lower the chance of backwards compatibility later on. Don’t take your game data lightly, overlook it will put a hefty price to your final product.

Have a great day and let’s not fall into death trap like I did with my previous game.

If you think this tutorial is helpful and would like to thank me.

Buy Me A Coffee

or via PayPal:

https://www.paypal.com/paypalme/kampertee

One thought on “[Unity] A beginner’s guide to Save, Load and Protect your Game Data.

Leave a comment