123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- .. _doc_saving_games:
- Saving games
- ============
- Introduction
- ------------
- Save games can be complicated. For example, it may be desirable
- to store information from multiple objects across multiple levels.
- Advanced save game systems should allow for additional information about
- an arbitrary number of objects. This will allow the save function to
- scale as the game grows more complex.
- .. note::
- If you're looking to save user configuration, you can use the
- :ref:`class_ConfigFile` class for this purpose.
- .. seealso::
- You can see how saving and loading works in action using the
- `Saving and Loading (Serialization) demo project <https://github.com/godotengine/godot-demo-projects/blob/master/loading/serialization>`__.
- Identify persistent objects
- ---------------------------
- 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:
- .. 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 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 class :ref:`JSON<class_json>` to convert between dictionary and string,
- 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_scene_file_path(),
- "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 Godot.Collections.Dictionary<string, Variant> Save()
- {
- return new Godot.Collections.Dictionary<string, Variant>()
- {
- { "Filename", SceneFilePath },
- { "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.
- Saving and reading data
- -----------------------
- As covered in the :ref:`doc_filesystem` 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 the class :ref:`JSON<class_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 = FileAccess.open("user://savegame.save", FileAccess.WRITE)
- var save_nodes = get_tree().get_nodes_in_group("Persist")
- for node in save_nodes:
- # Check the node is an instanced scene so it can be instanced again during load.
- if node.scene_file_path.is_empty():
- print("persistent node '%s' is not an instanced scene, skipped" % node.name)
- continue
- # Check the node has a save function.
- if !node.has_method("save"):
- print("persistent node '%s' is missing a save() function, skipped" % node.name)
- continue
- # Call the node's save function.
- var node_data = node.call("save")
- # JSON provides a static method to serialized JSON string.
- var json_string = JSON.stringify(node_data)
- # Store the save dictionary as a new line in the save file.
- save_game.store_line(json_string)
- .. 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()
- {
- using var saveGame = FileAccess.Open("user://savegame.save", FileAccess.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 (string.IsNullOrEmpty(saveNode.SceneFilePath))
- {
- GD.Print($"persistent node '{saveNode.Name}' is not an instanced scene, skipped");
- continue;
- }
- // Check the node has a save function.
- if (!saveNode.HasMethod("Save"))
- {
- GD.Print($"persistent node '{saveNode.Name}' is missing a Save() function, skipped");
- continue;
- }
- // Call the node's save function.
- var nodeData = saveNode.Call("Save");
- // Json provides a static method to serialized JSON string.
- var jsonString = Json.Stringify(nodeData);
- // Store the save dictionary as a new line in the save file.
- saveGame.StoreLine(jsonString);
- }
- }
- Game saved! Now, to load, we'll read each
- line. Use the :ref:`parse<class_JSON_method_parse>` method to read the
- JSON string back to a dictionary, 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():
- if not FileAccess.file_exists("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 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.
- var save_game = FileAccess.open("user://savegame.save", FileAccess.READ)
- while save_game.get_position() < save_game.get_length():
- var json_string = save_game.get_line()
- # Creates the helper class to interact with JSON
- var json = JSON.new()
- # Check if there is any error while parsing the JSON string, skip in case of failure
- var parse_result = json.parse(json_string)
- if not parse_result == OK:
- print("JSON Parse Error: ", json.get_error_message(), " in ", json_string, " at line ", json.get_error_line())
- continue
- # Get the data from the JSON object
- var node_data = json.get_data()
- # Firstly, we need to create the object and add it to the tree and set its position.
- var new_object = load(node_data["filename"]).instantiate()
- get_node(node_data["parent"]).add_child(new_object)
- new_object.position = Vector2(node_data["pos_x"], node_data["pos_y"])
- # Now we set the remaining variables.
- for i in node_data.keys():
- if i == "filename" or i == "parent" or i == "pos_x" or i == "pos_y":
- continue
- new_object.set(i, node_data[i])
- .. code-tab:: csharp
- // Note: This can be called from anywhere inside the tree. This function is
- // path independent.
- public void LoadGame()
- {
- if (!FileAccess.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.
- using var saveGame = FileAccess.Open("user://savegame.save", FileAccess.ModeFlags.Read);
- while (saveGame.GetPosition() < saveGame.GetLength())
- {
- var jsonString = saveGame.GetLine();
- // Creates the helper class to interact with JSON
- var json = new Json();
- var parseResult = json.Parse(jsonString);
- if (parseResult != Error.Ok)
- {
- GD.Print($"JSON Parse Error: {json.GetErrorMessage()} in {jsonString} at line {json.GetErrorLine()}");
- continue;
- }
- // Get the data from the JSON object
- var nodeData = new Godot.Collections.Dictionary<string, Variant>((Godot.Collections.Dictionary)json.Data);
- // Firstly, we need to create the object and add it to the tree and set its position.
- var newObjectScene = GD.Load<PackedScene>(nodeData["Filename"].ToString());
- var newObject = newObjectScene.Instantiate<Node>();
- GetNode(nodeData["Parent"].ToString()).AddChild(newObject);
- newObject.Set(Node2D.PropertyName.Position, new Vector2((float)nodeData["PosX"], (float)nodeData["PosY"]));
- // Now we set the remaining variables.
- foreach (var (key, value) in nodeData)
- {
- if (key == "Filename" || key == "Parent" || key == "PosX" || key == "PosY")
- {
- continue;
- }
- newObject.Set(key, 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.
- Some notes
- ----------
- 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 :ref:`add_child()
- <class_node_method_add_child>`
- call when child objects are loaded. You will also need a way to link
- children to parents as the :ref:`NodePath
- <class_nodepath>` will likely be invalid.
- JSON vs binary serialization
- ----------------------------
- For simple game state, JSON may work and it generates human-readable files that are easy to debug.
- But JSON has many limitations. If you need to store more complex game state or
- a lot of it, :ref:`binary serialization<doc_binary_serialization_api>`
- may be a better approach.
- JSON limitations
- ~~~~~~~~~~~~~~~~
- Here are some important gotchas to know about when using JSON.
- * **Filesize:**
- JSON stores data in text format, which is much larger than binary formats.
- * **Data types:**
- JSON only offers a limited set of data types. If you have data types
- that JSON doesn't have, you will need to translate your data to and
- from types that JSON can handle. For example, some important types that JSON
- can't parse are: ``Vector2``, ``Vector3``, ``Color``, ``Rect2``, and ``Quaternion``.
- * **Custom logic needed for encoding/decoding:**
- If you have any custom classes that you want to store with JSON, you will
- need to write your own logic for encoding and decoding those classes.
- Binary serialization
- ~~~~~~~~~~~~~~~~~~~~
- :ref:`Binary serialization<doc_binary_serialization_api>` is an alternative
- approach for storing game state, and you can use it with the functions
- ``get_var`` and ``store_var`` of :ref:`class_FileAccess`.
- * Binary serialization should produce smaller files than JSON.
- * Binary serialization can handle most common data types.
- * Binary serialization requires less custom logic for encoding and decoding
- custom classes.
- Note that not all properties are included. Only properties that are configured
- with the :ref:`PROPERTY_USAGE_STORAGE<class_@GlobalScope_constant_PROPERTY_USAGE_STORAGE>`
- flag set will be serialized. You can add a new usage flag to a property by overriding the
- :ref:`_get_property_list<class_Object_method__get_property_list>`
- method in your class. You can also check how property usage is configured by
- calling ``Object._get_property_list``.
- See :ref:`PropertyUsageFlags<enum_@GlobalScope_PropertyUsageFlags>` for the
- possible usage flags.
|