260 lines
9.9 KiB
ReStructuredText
260 lines
9.9 KiB
ReStructuredText
.. _doc_saving_games:
|
|
|
|
Saving games
|
|
============
|
|
|
|
Introduction
|
|
------------
|
|
|
|
Save games can be complicated. It can be desired to store more
|
|
information than the current level or number of stars earned on a level.
|
|
More advanced save games may need to store additional information about
|
|
an arbitrary number of objects. This will allow the save function to
|
|
scale as the game grows more complex.
|
|
|
|
Identify persistent objects
|
|
---------------------------
|
|
|
|
First 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.
|
|
As in the :ref:`doc_scripting_continued` tutorial, we can do this through
|
|
the GUI or through script. Let's add the relevant nodes using the GUI:
|
|
|
|
.. image:: img/groups.png
|
|
|
|
Once this is done when we need to save the game we can get all objects
|
|
to save them and then tell them all to save with this script:
|
|
|
|
.. tabs::
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
var save_nodes = get_tree().get_nodes_in_group("Persist")
|
|
for i in save_nodes:
|
|
# Now we can call our save function on each node.
|
|
|
|
.. code-tab:: csharp
|
|
|
|
var saveNodes = GetTree().GetNodesInGroup("Persist");
|
|
foreach (Node saveNode in saveNodes)
|
|
{
|
|
// Now we can call our save function on each node.
|
|
}
|
|
|
|
|
|
Serializing
|
|
-----------
|
|
|
|
The next step is to serialize the data. This makes it much easier to
|
|
read 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 :ref:`to_json()
|
|
<class_@GDScript_to_json>` and :ref:`parse_json()
|
|
<class_@GDScript_parse_json>`, 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:
|
|
|
|
.. tabs::
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
func save():
|
|
var save_dict = {
|
|
"filename" : get_filename(),
|
|
"parent" : get_parent().get_path(),
|
|
"pos_x" : position.x, # Vector2 is not supported by JSON
|
|
"pos_y" : position.y,
|
|
"attack" : attack,
|
|
"defense" : defense,
|
|
"current_health" : current_health,
|
|
"max_health" : max_health,
|
|
"damage" : damage,
|
|
"regen" : regen,
|
|
"experience" : experience,
|
|
"tnl" : tnl,
|
|
"level" : level,
|
|
"attack_growth" : attack_growth,
|
|
"defense_growth" : defense_growth,
|
|
"health_growth" : health_growth,
|
|
"is_alive" : is_alive,
|
|
"last_attack" : last_attack
|
|
}
|
|
return save_dict
|
|
|
|
.. code-tab:: csharp
|
|
|
|
public Dictionary<object, object> Save()
|
|
{
|
|
return new Dictionary<object, 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":that_variables_value }`` which will be useful when
|
|
loading.
|
|
|
|
Saving and reading data
|
|
-----------------------
|
|
|
|
As covered in the :ref:`doc_filesystem` tutorial, we'll need to open a file
|
|
and write to it and then later 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.
|
|
|
|
.. tabs::
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
# Note: This can be called from anywhere inside the tree. This function is path independent.
|
|
# Go through everything in the persist category and ask them to return a dict of relevant variables
|
|
func save_game():
|
|
var save_game = File.new()
|
|
save_game.open("user://savegame.save", File.WRITE)
|
|
var save_nodes = get_tree().get_nodes_in_group("Persist")
|
|
for i in save_nodes:
|
|
var node_data = i.call("save");
|
|
save_game.store_line(to_json(node_data))
|
|
save_game.close()
|
|
|
|
.. code-tab:: csharp
|
|
|
|
// Note: This can be called from anywhere inside the tree. This function is path independent.
|
|
// Go through everything in the persist category and ask them to return a dict of relevant variables
|
|
public void SaveGame()
|
|
{
|
|
var saveGame = new File();
|
|
saveGame.Open("user://savegame.save", (int)File.ModeFlags.Write);
|
|
|
|
var saveNodes = GetTree().GetNodesInGroup("Persist");
|
|
foreach (Node saveNode in saveNodes)
|
|
{
|
|
var nodeData = saveNode.Call("Save");
|
|
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:
|
|
|
|
.. tabs::
|
|
.. code-tab:: gdscript GDScript
|
|
|
|
# Note: This can be called from anywhere inside the tree. This function is path independent.
|
|
func load_game():
|
|
var save_game = File.new()
|
|
if not save_game.file_exists("user://save_game.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 savable objects.
|
|
var save_nodes = get_tree().get_nodes_in_group("Persist")
|
|
for i in save_nodes:
|
|
i.queue_free()
|
|
|
|
# Load the file line by line and process that dictionary to restore the object it represents
|
|
save_game.open("user://savegame.save", File.READ)
|
|
while not save_game.eof_reached():
|
|
var current_line = parse_json(save_game.get_line())
|
|
# First we need to create the object and add it to the tree and set its position.
|
|
var new_object = load(current_line["filename"]).instance()
|
|
get_node(current_line["parent"]).add_child(new_object)
|
|
new_object.position = Vector2(current_line["pos_x"], current_line["pos_y"]))
|
|
# Now we set the remaining variables.
|
|
for i in current_line.keys():
|
|
if i == "filename" or i == "parent" or i == "pos_x" or i == "pos_y":
|
|
continue
|
|
new_object.set(i, current_line[i])
|
|
save_game.close()
|
|
|
|
.. code-tab:: csharp
|
|
|
|
// Note: This can be called from anywhere inside the tree. This function is path independent.
|
|
public void LoadGame()
|
|
{
|
|
var saveGame = new File();
|
|
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 savable 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.EofReached())
|
|
{
|
|
var currentLine = (Dictionary<object, object>)JSON.Parse(saveGame.GetLine()).Result;
|
|
if (currentLine == null)
|
|
continue;
|
|
|
|
// First we need to create the object and add it to the tree and set its position.
|
|
var newObjectScene = (PackedScene)ResourceLoader.Load(currentLine["Filename"].ToString());
|
|
var newObject = (Node)newObjectScene.Instance();
|
|
GetNode(currentLine["Parent"].ToString()).AddChild(newObject);
|
|
newObject.Set("Position", new Vector2((float)currentLine["PosX"], (float)currentLine["PosY"]));
|
|
|
|
// Now we set the remaining variables.
|
|
foreach (KeyValuePair<object, object> entry in currentLine)
|
|
{
|
|
string key = entry.Key.ToString();
|
|
if (key == "Filename" || key == "Parent" || key == "PosX" || key == "PosY")
|
|
continue;
|
|
newObject.Set(key, entry.Value);
|
|
}
|
|
}
|
|
|
|
saveGame.Close();
|
|
}
|
|
|
|
|
|
And 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.
|
|
|
|
Some notes
|
|
----------
|
|
|
|
We may have glossed over a step, but setting the game state to one fit
|
|
to start loading data can be complicated. This step will need to be
|
|
heavily customized based on the needs of an individual project.
|
|
|
|
This implementation assumes no Persist objects are children of other
|
|
Persist objects. Doing so would create invalid paths. If this is one of
|
|
the needs of a project this needs to be considered. Saving objects in
|
|
stages (parent objects first) so they are available when child objects
|
|
are loaded will make sure they're available for the add_child() call.
|
|
There will also need to be some way to link children to parents as the
|
|
NodePath will likely be invalid.
|