Save Game

This guide explains how to save and persist data.

Goals

  • Allow persistance in the Game through Save Slots

How it works

The Save system works around two main "concepts":

  • A single "SaveDataHandler" resource
  • A common "Save Integration Interface" that any node or resource that needs to save their own data uses.

Overview

The SaveDataHandler.tres resource is responsible for serializing and saving the data in the user:://save_slots/ folder. For debug builds is will save as .json so that we can explore them and find problems/bugs/modify them. For release builds it will save as a binary file with a .save extension, but it's contents are formatted the same.

It is also responsible for loading save slots, updating any resources and existing nodes with the loaded data, and holding this data so that nodes that are instanced at any other time after slot loading can request their own data from it.

How it works and Save "Interface"

SaveDataHandler expects a minimum "interface" in Resources or Nodes that need to save data:

  • get_save_data_key() - returns a unique string id that will be used as key in the json to identify that node/resource
  • get_data_to_save() - returns a dictionary with all the data that node/resource needs save, in whatever structure fits best for that particular node/resource
  • load_data_from_save(saved_data: Dictionary) - sends the saved data back to the resource/node so that they can update themselves.

This way SaveDataHandler only deals with "Handling" the data from game to disk and from disk to game, not having to know about how they are serialized/deserialized, or which data in each resource/node needs saving or not. Each Resource/Node implements the interface as it's best for its own context.

There is a basic "snippet" for the "interface" that needs to be implemented in each node/resource that needs saving in the script SaveDataUtils.gd that you can copy and uncomment wherever needed

Save Snippet 01

It has some comments with instructions where needed and specially for the one function that is only needed in Nodes

Save Snippet 02 - Node only function

Adding Resources to SaveDataHandler

SaveDataHandler.tres has a property for an array of Resources, just drag and drop the resource that needs to be saved/loaded in it, and implement the "Save Integration Interface" in the script for that particular resource.

Adding Resources to SaveDataHandler

Adding Nodes to SaveDataHandler

After implementing the "Save Integration Interface" snippet on the node, make sure to call _setup_saved_data() on the node's _ready() function or somewhere else if it makes sense for this particular node. Avoid calling it on _init(), it will probably be too soon, unless you know this node will always be created after loading a slot. But better to avoid it anyway, _ready() is usually a much better place.

_setup_saved_data() will take are of adding the node to the correct group, so that if the player saves the game, this node will be called by SaveDataHandler when it grabs all the nodes in the group.

Quest Exceptions

The save for Quests work a bit differently. Instead of each QuestLine, Quest, or QuestStep having their own unique id and saving their data, SaveDataHandler only handles QuestManager, and QuestManager will call the interface on each QuestLine and in any active Quest.

This way QuestManager can take care of the details of loading questlines first, activating quests or resuming them, initializing whatever is needed.

Also Quests are responsible for doing the same for their own QuestSteps. This way responsibility is always closer to the best entity to know how to handle these decisions and each one can handle it in the best way.

We don't need this for things like inventories which are simpler systems with less moving parts. If we have any other complex system like quests in the future, this can be used as a starting point, or reference.

SaveDataUtils and Json

Saving to Json and loading back has it's problems. Json only has a few data types, and converts any int into float, it expects every dict key to be a string, which is not always true, and Vector2 that get converted with to_json() or JSON.print() are not converted back to Vector2 by JSON.parse(), they just become strings.

So SaveDataUtils.gd is there to be a helper in this cases. It has the following static functions that can be called from anywhere:

  • convert_value_to_json_save(value) - this uses duck typing intentionally, to be able to receive any type and convert the ones that are incompatible into something we can later convert back into the original value.
  • convert_json_save_to_value(json_value) - expects values after a json parse, and will convert back the incompatible types to their original types.
  • add_to_save_nodes_group(node: node) - what the save interface uses to add nodes to the correct group, this way if we need to refactor this further up, we can change it here.

Besides this all, it also holds the "save integration interface" snippet, as described above.

How to use it in the editor or for debugging

On the Game scene, there is now a new user_editor_save property:

Using saves on editor runs

By default it is off, turn it on whenever you want the game to save to an "editor_slot" and whenever you press F5 to run the game it will automatically load it and continue from there. When it is on it will also ignore the debug_initial_screen property right above it. So if you want to test things like the splashscreen or the Main menu by setting the initial screen, disable use_editor_save

You can choose which slot to use in SaveDataHandler.tres

Choosing save slots

I left a high number so that I wouldn't overwrite actual save slots whenever that screen is integrated. You can use whichever number you want, but DON'T USE 100

Slot 100 is reserved for unit tests, whatever is there will be overwritten and deleted by GUT runs.

Also, if you want to see the save itself, or open it on visual studio code to check out something, just go to "Project > Open User Data Folder"

Easily open used data folder

They will be in the "save_slots" folder in a human readable json file.

To Load a Slot

There are two Events for loading a save slot through code in the game:

  • save_slot_chosen - used by SaveSlotsScreen to choose a slot and load it.
  • load_current_slot_requested - used by PauseScreen to reload the current slot.

Emitting any of these Event signals will trigger Game to properly handle a transition to a "Loading" screen and continue the game property from the scene the save tells it to go to.