Gamedev

Making a human readable save game system in Unity using JSON

published on
Human Readable Save Game File

When I wanted to make a save system for my game Ayre, I didn’t want to just serialize objects into binary files. It seemed like something that might bite me in the ass down the road if someone had a problem with their save and I had to debug it. I though, I want to open it like a text file and see what’s going on. Previously when I made games in Construct2, I would save what I wanted in a JSON file. You could open it, see what’s going on, edit it, and carry on. Now some of you might say “woah, then people could edit their save game and cheat” and I would say yes, yes they can, and you shouldn’t care.

Setup

So, first of all, I added JSON .NET For Unity from the Asset store. It’s free. Go get it now. I’ll wait. The main reason is better Dictionary support than the built in stuff. This lets your JSON file contain multiple layers of objects. You’ll see.

First we have to build the class that will serve as the blueprint for the save. Here you add all the variables you want to track in a save file. Here I have a couple of Dictionaries containing special classes and also a bunch of other variables:

[Serializable]
public class SaveList
{
public Dictionary<int, SaveItem> saveList;
public Dictionary<int, GemListItem> save_gemList;
public Dictionary<GameManagerScript.UpgradeType, int> save_Upgrades;
public float save_dragon_max_speed;
public float save_dragon_max_altitude;
public float save_dragon_boost_amount;
public float save_dragon_boost_timer;
public float save_dragon_glide_max;
public float save_dragon_climb_loss;
public float save_dragon_dive_gain;
public Vector3 player_Location;
public float player_rot_x;
public float player_rot_y;
public float player_rot_z;
public float player_rot_w;
//public Quaternion player_Rotation;
public Vector3 dragon_Location;
//public Quaternion dragon_Rotation;
public float dragon_rot_x;
public float dragon_rot_y;
public float dragon_rot_z;
public float dragon_rot_w;
public int activePlayer;
public bool isFlying;
}

Note: Quaterions don’t seem to like being saved like this, so I had to save the individual x,y,z,w floats.

Next you need to define those SaveItem and GemListItem classes. GameManagerScript.UpgradeType is just an enum, so nothing fancy there. GameManagerScript.InteractionType below is also an enum.

[Serializable]
public class SaveItem
{
public int ItemID;
public GameManagerScript.InteractionType InteractionType;
public bool completed;
public float bestTime;
public float timeLimit;
public float lastTime;
public SaveItem(int iD, GameManagerScript.InteractionType interactionType, float newTimeLimit)
{
ItemID = iD;
InteractionType = interactionType;
completed = false;
bestTime = 0f;
timeLimit = newTimeLimit;
lastTime = 0f;
}
public SaveItem(int iD, GameManagerScript.InteractionType interactionType)
{
ItemID = iD;
InteractionType = interactionType;
completed = false;
bestTime = 0f;
timeLimit = 0f;
lastTime = 0f;
}
public SaveItem(int iD, GameManagerScript.InteractionType interactionType, bool _completed)
{
ItemID = iD;
InteractionType = interactionType;
completed = _completed;
bestTime = 0f;
timeLimit = 0f;
lastTime = 0f;
}
[JsonConstructor]
public SaveItem(int _ID, GameManagerScript.InteractionType _interactionType, bool _completed, float _bestTime, float _timeLimit, float _lastTime)
{
ItemID = _ID;
InteractionType = _interactionType;
completed = _completed;
bestTime = _bestTime;
timeLimit = _timeLimit;
lastTime = _lastTime;
}
}
[Serializable]
public class GemListItem
{
public int GemID;
public bool collected;
public GemListItem(int _gemID, bool _collected)
{
GemID = _gemID;
collected = _collected;
}
}

Note: I had to add [JsonConstructor] to SaveItem so it knew which constructor to use.

Saving the Game

To save the game, you need to run something like this when either the player presses a save game button, or you auto save at certain times. I use save slots in the UI, so players can have multiple saves. Also, this saves to Application.persistentDataPath which I *think* is the best place for it.

public SaveGame()
{
var ql = new SaveList();
ql.saveList = SaveItemDictionary;
ql.save_gemList = GemDictionary;
ql.save_Upgrades = Upgrades;
ql.save_dragon_max_speed = DragonScript.maxSpeed;
ql.save_dragon_max_altitude = DragonScript.maxAltitude;
ql.save_dragon_boost_amount = DragonScript.boostAmount;
ql.save_dragon_boost_timer = DragonScript.boostTimer;
ql.save_dragon_glide_max = DragonScript.glideSpeedMax;
ql.save_dragon_climb_loss = DragonScript.climbLoss;
ql.save_dragon_dive_gain = DragonScript.diveGain;
ql.player_Location = Human.transform.position;
ql.player_rot_x = Human.transform.rotation.x;
ql.player_rot_y = Human.transform.rotation.y;
ql.player_rot_z = Human.transform.rotation.z;
ql.player_rot_w = Human.transform.rotation.w;
ql.dragon_Location = Dragon.transform.position;
ql.dragon_rot_x = Dragon.transform.rotation.x;
ql.dragon_rot_y = Dragon.transform.rotation.y;
ql.dragon_rot_z = Dragon.transform.rotation.z;
ql.dragon_rot_w = Dragon.transform.rotation.w;
ql.activePlayer = ActivePlayer;
ql.isFlying = DragonScript.IsFlying;
string result = JsonConvert.SerializeObject(ql, new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Serialize
});
string savePath = Application.persistentDataPath + "/savegames/";
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
Debug.Log("Saving as JSON: " + result);
string gameDataFileName = "Slot" + slotNumber;
string filePath = savePath + gameDataFileName + ".json";
File.WriteAllText(filePath, result);
StreamWriter newTask = new StreamWriter(savePath + gameDataFileName + ".txt", false);
newTask.WriteLine(System.DateTime.Now.ToString("HH:mm dd MMMM, yyyy"));
newTask.Close();
}

So, what does this do? We created a new SaveList object and then filled it with all the data we want to save.

Then we create a string containing the JSON data. The ReferenceLoopHandling is the magic that lets you put arrays (Dictionaries) within the object:

string result = JsonConvert.SerializeObject(ql, new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Serialize
});

Then you save it as a file:

string filePath = savePath + gameDataFileName + ".json";
File.WriteAllText(filePath, result);

And you’ve created a text file:

Human Readable Save Game File
Human Readable Save Game File

Loading the Save File

So when you want to load your game from a file, you basically do the same steps again.

Load your file into a string:

string dataAsJson = File.ReadAllText(Application.persistentDataPath + "/savegames/Slot" + slotNumber + ".json");

Parse the string into a SaveList object using the same ReferenceLoopHandling:

SaveList loadedData = JsonConvert.DeserializeObject<SaveList>(dataAsJson, new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Serialize
});

Populate your Dictionaries and variables:

DragonScript.maxSpeed = loadedData.save_dragon_max_speed;
DragonScript.maxAltitude = loadedData.save_dragon_max_altitude;
DragonScript.boostAmount = loadedData.save_dragon_boost_amount;
DragonScript.boostTimer = loadedData.save_dragon_boost_timer;
DragonScript.glideSpeedMax = loadedData.save_dragon_glide_max;
DragonScript.climbLoss = loadedData.save_dragon_climb_loss;
DragonScript.diveGain = loadedData.save_dragon_dive_gain;
SaveItemDictionary = loadedData.saveList;
GemDictionary = loadedData.save_gemList;
Upgrades = loadedData.save_Upgrades;
//Set Dragon Position
Dragon.transform.position = loadedData.dragon_Location;
Dragon.transform.rotation = new Quaternion(loadedData.dragon_rot_x, loadedData.dragon_rot_y, loadedData.dragon_rot_z, loadedData.dragon_rot_w);
//Set Human Position
Human.transform.position = loadedData.player_Location;
Human.transform.rotation = new Quaternion(loadedData.player_rot_x, loadedData.player_rot_y, loadedData.player_rot_z, loadedData.player_rot_w);

Congratulations, you now have a working save / load system using a human readable save file.

What’s Next?

Build on this by thinking about how to present your save system. Take a screenshot and save the date and time. Present the image along with the time when players load their game.

Save Image