Unity: How to save and load data using json and Base64
Saving and loading game data is an essential part of almost any game. These two processes should be fast, efficient and error-prone since nothing is more painful for a gamer than losing progress he has been working on so hard.
In this tutorial I will show you an easy way to save and load your data applying a technique that makes it unreadable by the naked eye — Base64 encoding. The good part is that you will need to implement it only once, afterwards you can simply copy\paste it to all new projects changing underlying models.
Prerequisites
For this tutorial I’ve partially reused the swipe game mechanics from my latest game. In order to destroy a ball, player should swipe in the correct direction — white side of the ball. Each wrong swipe will reduce remaining life. Occasionally collectible stars will be added to stars count.
Then we will save incorrect swipes total count into a results file and collected stars total count into stars file.
I’ll skip the setup of the game itself and move forward to the save and load part directly but if you are curious how spawn and user input were implemented, please find source code at GitHub, the link is at the end of this article.
Part 1. Saving Data
Whenever player reaches zero life count a level fails and results should be saved. Let’s start by creating models which will represent the data we want to save.
In ResultData
we will store incorrect swipes count:
[Serializable]
public class ResultData
{
public int incorrectSwipes;
}
StarsData
holds how many stars were collected:
[Serializable]
public class StarsData
{
public int starsCollected;
}
Now let’s create a script that will work with those models — DataHandler
with one public method to save the data.
public class DataHandler: MonoBehaviour
{
public void SaveData(int incorrectSwipes, int stars)
{
}
}
This script is used in the Controller
which will call the SaveData
method whenever level fails:
private void LevelFailed()
{
_handler.SaveData(_incorrectSwipes, _stars);
}
Let’s now move on and extend SaveData
method to actually save the data. We will start with the result file first.
public class DataHandler: MonoBehaviour
{
private const string ResultsFile = "res.dat";private string _resultsFilePath;private void Awake()
{
_resultsFilePath = Path.Combine(Application.persistentDataPath, ResultsFile);if (!File.Exists(_resultsFilePath))
{
CreateNewResultsFile();
}
}private void CreateNewResultsFile(){
SaveResultsFile(new ResultData());
}public void SaveData(int incorrectSwipes)
{
SaveResultsFile(new ResultData {incorrectSwipes = incorrectSwipes});
}private void SaveResultsFile(ResultData data)
{
SaveFile(_resultsFilePath, data);
}private static void SaveFile(string filePath, ResultData data)
{
File.WriteAllText(filePath, JsonUtility.ToJson(data));
}
}
The current version is relatively simple, but let me break it down into parts:
- First of all, the script checks if the file is not yet created and creates it on
Awake
. - Whenever there is a save event the
SaveData
method is triggered which internally usesJsonUtility.ToJson
method.
If you run the game now, swipe it to a level failed state and open _resultsFilePath
file you will see res.dat like
{“incorrectSwipes”:3}
Note! You can simply
Debug.Log(_resultsFilePath)
if you are not sure where the file is located on your machine.
So far so good, now let’s extend the script to save also the star data.
public class DataHandler: MonoBehaviour
{
private const string ResultsFile = "res.dat";
private const string StarsFile = "stars.dat";private string _resultsFilePath;
private string _starsFilePath;private void Awake()
{
_resultsFilePath = Path.Combine(Application.persistentDataPath, ResultsFile);
_starsFilePath = Path.Combine(Application.persistentDataPath, StarsFile);if (!File.Exists(_resultsFilePath))
{
CreateNewResultsFile();
}if (!File.Exists(_starsFilePath))
{
CreateNewStarsFile();
}
}private void CreateNewResultsFile(){
SaveFile(new ResultData(), _resultsFilePath);
}private void CreateNewStarsFile(){
SaveFile(new StarsData(), _starsFilePath);
}public void SaveData(int incorrectSwipes, int stars)
{
SaveFile(new ResultData {incorrectSwipes = incorrectSwipes}, _resultsFilePath);
SaveFile(new StarsData {starsCollected = stars}, _starsFilePath);
}private void SaveFile(object data, string path)
{
SaveFile(path, data);
}private static void SaveFile(string filePath, object data)
{
File.WriteAllText(filePath, JsonUtility.ToJson(data));
}
}
There are a couple of changes:
- Check on
Awake
if stars file exists has been added SaveResultsFile
has been changed toSaveFile
that accepts object instead ofResultData
and path where to save a fileSaveData
method now saves newStarsData
whenever it is called
If you run the game now, swipe it to a level failed state with some stars collected and open _starsFilePath
file you will see stars.dat
{“starsCollected”:2}
Awesome, now our little game can save the game data in a plain json. You have probably already spotted a flaw in this initial implementation. It doesn’t check existing count and always override it! We will fix it in the next part once we are done with loading of those files.
Part 2. Loading Data
Let’s extend our DataHandler
with two methods to load stars data and result data. To read the data we will create another helper method with Generic Type Parameter to be able to load both types easily:
private ResultData LoadResultsData()
{
return LoadData<ResultData>(_resultsFilePath);
}private StarsData LoadStarsData()
{
return LoadData<StarsData>(_starsFilePath);
}private static T LoadData<T>(string filePath)
{
return JsonUtility.FromJson<T>(File.ReadAllText(filePath));
}
Here we are using JsonUtility.FromJson
method to convert json files into model. Now we can fix SaveData
method to take into consideration the previous data:
public void SaveData(int incorrectSwipes, int stars)
{
var resultData = LoadResultsData();
resultData.incorrectSwipes += incorrectSwipes;
SaveFile(resultData, _resultsFilePath);
var starsData = LoadStarsData();
starsData.starsCollected += stars;
SaveFile(starsData, _starsFilePath);
}
Run the game multiple times and you will see that the new count is added to the old one. Well done!
Let’s move on and encode the data in a non-human readable way using Base64.
Part 3. Base64 Encoding
It is crucial to understand that Base64 is not encryption, it’s an encoding. It’s a way of representing binary data using only printable (text) characters. Even though saving your game data in Base64 does make it a little bit harder to see internal data structure there are plenty of online tools to convert Base64 into human readable string with a simple click. Don’t use it to store sensitive data like usernames and passwords. But this method will definitely help you to prevent human modifications of saved files on an actual device.
Unity support Base64 out-of-the-box, there are just two lines to change in our implementation. But first remove the saved files since the data format will change and DataHandler
will not be able to read it anymore.
private static T LoadData<T>(string filePath)
{
return JsonUtility.FromJson<T>(
Encoding.UTF8.GetString(Convert.FromBase64String(File.ReadAllText(filePath))));
}private static void SaveFile(string filePath, object data)
{
File.WriteAllText(filePath,
Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonUtility.ToJson(data))));
}
We are using Convert.FromBase64String
and Convert.ToBase64String
methods to load and save data.
As always, run the game and check the files you will something that looks like
eyJpbmNvcnJlY3RTd2lwZXMiOjN9
instead of human readable json.
Afterwards
Well done finishing the tutorial! Should you have any questions please leave them in the comments section below.
The project source files can be found at this GitHub repository
The assets I used are taken from this game Mooon
Support
If you like the content you read and want to support the author — thank you very much!
Here is my Ethereum wallet for tips:
0xB34C2BcE674104a7ca1ECEbF76d21fE1099132F0