Saving the Game
If you read my last correspondence, you know I handed off a pretty big task to future-me: build a save system.
Well, hi. It’s future-me. And… I did it! We have a save system, baby!
Up until now I’ve only ever saved data using Unity’s PlayerPrefs. Great for simple things like an INT here or a String there. But we’re talking about a deck building roguelike… you know? Building a deck. Which in the game I’m building is a list of Scriptable Objects. How the hell do I save that?
Luckily, I had some foresight into eventually needing to save this stuff. I basically gave everything an ID number from the get-go. Yes, cards, but also characters, enemies, nodes, encounters. Everything was primed to be stored and retrieved as an INT.
But PlayerPrefs still isn’t built to handle lists of data and the files they end up in are not secure. In fact, they can be easily editable. Even if no one ever cheats, encrypting saves means fewer headaches down the road if I expand features like leaderboards, etc... So… JSON it is.
By the way, Unity has its own JSON utility. Which is wild. There are assets in the Unity Asset Store that offer save systems. But… I have to imagine they are using this built-in utility to write and read these files. What else would you need? And it’s actually pretty simple.
csharp
File.WriteAllText(MetaPath, json);
return JsonUtility.FromJson<MetaSave>(json);
I started by creating a SaveSystem.cs file. This one file has the save system logic along with a few classes I need: A KeySave class, a MetaSave class, and a CharacterSave class. These classes hold the data I need for each one: MetaSave has BOOLS and INTS for meta progression (permanent unlocks and such), CharacterSave has what you would expect (player level, your deck, stats, current location and last completed node, etc…), and the KeySave has the keys needed to encrypt and decrypt the JSON files.
So here’s how it flows.
Loading a MetaSave from JSON:
csharp
MetaSave metaLoad = SaveSystem.LoadMeta();
Once I’ve loaded the meta data, I map it back into GameManager like this:
csharp
public void LoadMetaData()
{
MetaSave metaLoad = SaveSystem.LoadMeta();
// Daichi Tengus
daichiMizuhoTenguDefeated = metaLoad.daichiMizuhoTenguDefeated;
daichiKageroTenguDefeated = metaLoad.daichiKageroTenguDefeated;
daichiMujinTenguDefeated = metaLoad.daichiMujinTenguDefeated;
daichiYamanamiTenguDefeated = metaLoad.daichiYamanamiTenguDefeated;
daichiYukiTenguDefeated = metaLoad.daichiYukiTenguDefeated;
daichiKamihikariTenguDefeated = metaLoad.daichiKamihikariTenguDefeated;
// Yumeko Tengus
yumekoMizuhoTenguDefeated = metaLoad.yumekoMizuhoTenguDefeated;
yumekoKageroTenguDefeated = metaLoad.yumekoKageroTenguDefeated;
yumekoMujinTenguDefeated = metaLoad.yumekoMujinTenguDefeated;
yumekoYamanamiTenguDefeated = metaLoad.yumekoYamanamiTenguDefeated;
yumekoYukiTenguDefeated = metaLoad.yumekoYukiTenguDefeated;
yumekoKamihikariTenguDefeated = metaLoad.yumekoKamihikariTenguDefeated;
soundOn = metaLoad.soundOn;
}
The nuance here is knowing when and what I need to load at what time. When selecting a character, I don’t need to know which node they completed last but I do need to know which level they were on and how many levels they completed. I load the CharacterSave and apply only the properties I need:
csharp
CharacterSave charSave = SaveSystem.LoadCharacter(GameManager.instance.characterSelected);
GameManager.instance.levelSelected = charSave.levelSelected;
GameManager.instance.currentIsland = charSave.currentIsland;
GameManager.instance.scalesGathered = charSave.scalesGathered;
Figuring this stuff out was and continues to be a process. What happens when I load into a map and how does the map determine if this is the player’s first time here or are they a few nodes in? I ended up checking if the save file exists to make a lot of these determinations. On death, the file is just deleted entirely. By building the file path and checking if it exists, I can decide what to do from there. Here, I check if it exists and then check if the node list is populated. This allows me to determine which way to go. I found that using File.Exists to be super helpful:
csharp
if (!File.Exists(path))
{
AssignEncountersToNodes();
}
else
{
CharacterSave charSave = SaveSystem.LoadCharacter(GameManager.instance.characterSelected);
if(charSave.idsToNodes.Count > 0)
{
LoadInSavedEncounters();
}
else
{
AssignEncountersToNodes();
}
}
And finally, for those that really want to know, here is the save system itself:
As you can see, it’s a lot of passing data in and out of the written file. I have an encryption process in place that requires passing in two keys in order to encrypt and decrypt the string as it’s written and read from file. You can see those classes here:
So what’s next?
QA! And I’m not being facetious here. I actually enjoy playing a game over and over again, noting bugs, and then squashing them. Once the game is in a good place, I’ll move toward beta: one character, two islands, and hopefully enough to show whether this whole thing is actually fun.
Thanks so much for reading. Talk to you all soon.
Andrew