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 Resource
s or Node
s 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/resourceget_data_to_save()
- returns a dictionary with all the data that node/resource needs save, in whatever structure fits best for that particular node/resourceload_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
It has some comments with instructions where needed and specially for the one function that is only needed in Nodes
Adding Resource
s to SaveDataHandler
SaveDataHandler.tres
has a property for an array of Resource
s, 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 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:
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
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"
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 bySaveSlotsScreen
to choose a slot and load it.load_current_slot_requested
- used byPauseScreen
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.