Saving games
Note
If you’re looking to save user configuration, you can use the class for this purpose.
Firstly, we should identify what objects we want to keep between game sessions and what information we want to keep from those objects. For this tutorial, we will use groups to mark and handle objects to be saved, but other methods are certainly possible.
We will start by adding objects we wish to save to the “Persist” group. We can do this through either the GUI or script. Let’s add the relevant nodes using the GUI:
GDScript C#
foreach (Node saveNode in saveNodes)
{
// Now, we can call our save function on each node.
}
The next step is to serialize the data. This makes it much easier to read from and store to disk. In this case, we’re assuming each member of group Persist is an instanced node and thus has a path. GDScript has helper functions for this, such as to_json() and , so we will use a dictionary. Our node needs to contain a save function that returns this data. The save function will look like this:
GDScript C#
public Godot.Collections.Dictionary<string, object> Save()
{
return new Godot.Collections.Dictionary<string, object>()
{
{ "Filename", GetFilename() },
{ "Parent", GetParent().GetPath() },
{ "PosX", Position.x }, // Vector2 is not supported by JSON
{ "PosY", Position.y },
{ "Attack", Attack },
{ "Defense", Defense },
{ "CurrentHealth", CurrentHealth },
{ "MaxHealth", MaxHealth },
{ "Damage", Damage },
{ "Regen", Regen },
{ "Experience", Experience },
{ "Tnl", Tnl },
{ "Level", Level },
{ "AttackGrowth", AttackGrowth },
{ "DefenseGrowth", DefenseGrowth },
{ "HealthGrowth", HealthGrowth },
{ "IsAlive", IsAlive },
{ "LastAttack", LastAttack }
};
}
This gives us a dictionary with the style { "variable_name":value_of_variable }
, which will be useful when loading.
As covered in the tutorial, we’ll need to open a file so we can write to it or read from it. Now that we have a way to call our groups and get their relevant data, let’s use to_json() to convert it into an easily stored string and store them in a file. Doing it this way ensures that each line is its own object, so we have an easy way to pull the data out of the file as well.
// Note: This can be called from anywhere inside the tree. This function is
// path independent.
// dict of relevant variables.
public void SaveGame()
{
saveGame.Open("user://savegame.save", (int)File.ModeFlags.Write);
var saveNodes = GetTree().GetNodesInGroup("Persist");
foreach (Node saveNode in saveNodes)
{
// Check the node is an instanced scene so it can be instanced again during load.
if (saveNode.Filename.Empty())
{
GD.Print(String.Format("persistent node '{0}' is not an instanced scene, skipped", saveNode.Name));
continue;
}
// Check the node has a save function.
if (!saveNode.HasMethod("Save"))
{
GD.Print(String.Format("persistent node '{0}' is missing a Save() function, skipped", saveNode.Name));
continue;
}
// Call the node's save function.
var nodeData = saveNode.Call("Save");
// Store the save dictionary as a new line in the save file.
saveGame.StoreLine(JSON.Print(nodeData));
}
saveGame.Close();
}
Game saved! Loading is fairly simple as well. For that, we’ll read each line, use parse_json() to read it back to a dict, and then iterate over the dict to read our values. But we’ll need to first create the object and we can use the filename and parent values to achieve that. Here is our load function:
GDScript C#
// Note: This can be called from anywhere inside the tree. This function is
// path independent.
public void LoadGame()
{
if (!saveGame.FileExists("user://savegame.save"))
return; // Error! We don't have a save to load.
// We need to revert the game state so we're not cloning objects during loading.
// This will vary wildly depending on the needs of a project, so take care with
// this step.
// For our example, we will accomplish this by deleting saveable objects.
var saveNodes = GetTree().GetNodesInGroup("Persist");
foreach (Node saveNode in saveNodes)
saveNode.QueueFree();
// Load the file line by line and process that dictionary to restore the object
// it represents.
saveGame.Open("user://savegame.save", (int)File.ModeFlags.Read);
while (saveGame.GetPosition() < saveGame.GetLen())
{
// Get the saved dictionary from the next line in the save file
var nodeData = new Godot.Collections.Dictionary<string, object>((Godot.Collections.Dictionary)JSON.Parse(saveGame.GetLine()).Result);
// Firstly, we need to create the object and add it to the tree and set its position.
var newObjectScene = (PackedScene)ResourceLoader.Load(nodeData["Filename"].ToString());
var newObject = (Node)newObjectScene.Instance();
GetNode(nodeData["Parent"].ToString()).AddChild(newObject);
newObject.Set("Position", new Vector2((float)nodeData["PosX"], (float)nodeData["PosY"]));
// Now we set the remaining variables.
foreach (KeyValuePair<string, object> entry in nodeData)
{
string key = entry.Key.ToString();
if (key == "Filename" || key == "Parent" || key == "PosX" || key == "PosY")
continue;
newObject.Set(key, entry.Value);
}
}
}
Now we can save and load an arbitrary number of objects laid out almost anywhere across the scene tree! Each object can store different data depending on what it needs to save.
We have glossed over setting up the game state for loading. It’s ultimately up to the project creator where much of this logic goes. This is often complicated and will need to be heavily customized based on the needs of the individual project.
Additionally, our implementation assumes no Persist objects are children of other Persist objects. Otherwise, invalid paths would be created. To accommodate nested Persist objects, consider saving objects in stages. Load parent objects first so they are available for the add_child() call when child objects are loaded. You will also need a way to link children to parents as the will likely be invalid.