saving_games.rst 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. .. _doc_saving_games:
  2. Saving games
  3. ============
  4. Introduction
  5. ------------
  6. Save games can be complicated. For example, it may be desirable
  7. to store information from multiple objects across multiple levels.
  8. Advanced save game systems should allow for additional information about
  9. an arbitrary number of objects. This will allow the save function to
  10. scale as the game grows more complex.
  11. .. note::
  12. If you're looking to save user configuration, you can use the
  13. :ref:`class_ConfigFile` class for this purpose.
  14. .. seealso::
  15. You can see how saving and loading works in action using the
  16. `Saving and Loading (Serialization) demo project <https://github.com/godotengine/godot-demo-projects/blob/master/loading/serialization>`__.
  17. Identify persistent objects
  18. ---------------------------
  19. Firstly, we should identify what objects we want to keep between game
  20. sessions and what information we want to keep from those objects. For
  21. this tutorial, we will use groups to mark and handle objects to be saved,
  22. but other methods are certainly possible.
  23. We will start by adding objects we wish to save to the "Persist" group. We can
  24. do this through either the GUI or script. Let's add the relevant nodes using the
  25. GUI:
  26. .. image:: img/groups.webp
  27. Once this is done, when we need to save the game, we can get all objects
  28. to save them and then tell them all to save with this script:
  29. .. tabs::
  30. .. code-tab:: gdscript GDScript
  31. var save_nodes = get_tree().get_nodes_in_group("Persist")
  32. for node in save_nodes:
  33. # Now, we can call our save function on each node.
  34. .. code-tab:: csharp
  35. var saveNodes = GetTree().GetNodesInGroup("Persist");
  36. foreach (Node saveNode in saveNodes)
  37. {
  38. // Now, we can call our save function on each node.
  39. }
  40. Serializing
  41. -----------
  42. The next step is to serialize the data. This makes it much easier to
  43. read from and store to disk. In this case, we're assuming each member of
  44. group Persist is an instanced node and thus has a path. GDScript
  45. has helper class :ref:`JSON<class_json>` to convert between dictionary and string,
  46. Our node needs to contain a save function that returns this data.
  47. The save function will look like this:
  48. .. tabs::
  49. .. code-tab:: gdscript GDScript
  50. func save():
  51. var save_dict = {
  52. "filename" : get_scene_file_path(),
  53. "parent" : get_parent().get_path(),
  54. "pos_x" : position.x, # Vector2 is not supported by JSON
  55. "pos_y" : position.y,
  56. "attack" : attack,
  57. "defense" : defense,
  58. "current_health" : current_health,
  59. "max_health" : max_health,
  60. "damage" : damage,
  61. "regen" : regen,
  62. "experience" : experience,
  63. "tnl" : tnl,
  64. "level" : level,
  65. "attack_growth" : attack_growth,
  66. "defense_growth" : defense_growth,
  67. "health_growth" : health_growth,
  68. "is_alive" : is_alive,
  69. "last_attack" : last_attack
  70. }
  71. return save_dict
  72. .. code-tab:: csharp
  73. public Godot.Collections.Dictionary<string, Variant> Save()
  74. {
  75. return new Godot.Collections.Dictionary<string, Variant>()
  76. {
  77. { "Filename", SceneFilePath },
  78. { "Parent", GetParent().GetPath() },
  79. { "PosX", Position.X }, // Vector2 is not supported by JSON
  80. { "PosY", Position.Y },
  81. { "Attack", Attack },
  82. { "Defense", Defense },
  83. { "CurrentHealth", CurrentHealth },
  84. { "MaxHealth", MaxHealth },
  85. { "Damage", Damage },
  86. { "Regen", Regen },
  87. { "Experience", Experience },
  88. { "Tnl", Tnl },
  89. { "Level", Level },
  90. { "AttackGrowth", AttackGrowth },
  91. { "DefenseGrowth", DefenseGrowth },
  92. { "HealthGrowth", HealthGrowth },
  93. { "IsAlive", IsAlive },
  94. { "LastAttack", LastAttack }
  95. };
  96. }
  97. This gives us a dictionary with the style
  98. ``{ "variable_name":value_of_variable }``, which will be useful when
  99. loading.
  100. Saving and reading data
  101. -----------------------
  102. As covered in the :ref:`doc_filesystem` tutorial, we'll need to open a file
  103. so we can write to it or read from it. Now that we have a way to
  104. call our groups and get their relevant data, let's use the class :ref:`JSON<class_json>` to
  105. convert it into an easily stored string and store them in a file. Doing
  106. it this way ensures that each line is its own object, so we have an easy
  107. way to pull the data out of the file as well.
  108. .. tabs::
  109. .. code-tab:: gdscript GDScript
  110. # Note: This can be called from anywhere inside the tree. This function is
  111. # path independent.
  112. # Go through everything in the persist category and ask them to return a
  113. # dict of relevant variables.
  114. func save_game():
  115. var save_file = FileAccess.open("user://savegame.save", FileAccess.WRITE)
  116. var save_nodes = get_tree().get_nodes_in_group("Persist")
  117. for node in save_nodes:
  118. # Check the node is an instanced scene so it can be instanced again during load.
  119. if node.scene_file_path.is_empty():
  120. print("persistent node '%s' is not an instanced scene, skipped" % node.name)
  121. continue
  122. # Check the node has a save function.
  123. if !node.has_method("save"):
  124. print("persistent node '%s' is missing a save() function, skipped" % node.name)
  125. continue
  126. # Call the node's save function.
  127. var node_data = node.call("save")
  128. # JSON provides a static method to serialized JSON string.
  129. var json_string = JSON.stringify(node_data)
  130. # Store the save dictionary as a new line in the save file.
  131. save_file.store_line(json_string)
  132. .. code-tab:: csharp
  133. // Note: This can be called from anywhere inside the tree. This function is
  134. // path independent.
  135. // Go through everything in the persist category and ask them to return a
  136. // dict of relevant variables.
  137. public void SaveGame()
  138. {
  139. using var saveFile = FileAccess.Open("user://savegame.save", FileAccess.ModeFlags.Write);
  140. var saveNodes = GetTree().GetNodesInGroup("Persist");
  141. foreach (Node saveNode in saveNodes)
  142. {
  143. // Check the node is an instanced scene so it can be instanced again during load.
  144. if (string.IsNullOrEmpty(saveNode.SceneFilePath))
  145. {
  146. GD.Print($"persistent node '{saveNode.Name}' is not an instanced scene, skipped");
  147. continue;
  148. }
  149. // Check the node has a save function.
  150. if (!saveNode.HasMethod("Save"))
  151. {
  152. GD.Print($"persistent node '{saveNode.Name}' is missing a Save() function, skipped");
  153. continue;
  154. }
  155. // Call the node's save function.
  156. var nodeData = saveNode.Call("Save");
  157. // Json provides a static method to serialized JSON string.
  158. var jsonString = Json.Stringify(nodeData);
  159. // Store the save dictionary as a new line in the save file.
  160. saveFile.StoreLine(jsonString);
  161. }
  162. }
  163. Game saved! Now, to load, we'll read each
  164. line. Use the :ref:`parse<class_JSON_method_parse>` method to read the
  165. JSON string back to a dictionary, and then iterate over
  166. the dict to read our values. But we'll need to first create the object
  167. and we can use the filename and parent values to achieve that. Here is our
  168. load function:
  169. .. tabs::
  170. .. code-tab:: gdscript GDScript
  171. # Note: This can be called from anywhere inside the tree. This function
  172. # is path independent.
  173. func load_game():
  174. if not FileAccess.file_exists("user://savegame.save"):
  175. return # Error! We don't have a save to load.
  176. # We need to revert the game state so we're not cloning objects
  177. # during loading. This will vary wildly depending on the needs of a
  178. # project, so take care with this step.
  179. # For our example, we will accomplish this by deleting saveable objects.
  180. var save_nodes = get_tree().get_nodes_in_group("Persist")
  181. for i in save_nodes:
  182. i.queue_free()
  183. # Load the file line by line and process that dictionary to restore
  184. # the object it represents.
  185. var save_file = FileAccess.open("user://savegame.save", FileAccess.READ)
  186. while save_file.get_position() < save_file.get_length():
  187. var json_string = save_file.get_line()
  188. # Creates the helper class to interact with JSON.
  189. var json = JSON.new()
  190. # Check if there is any error while parsing the JSON string, skip in case of failure.
  191. var parse_result = json.parse(json_string)
  192. if not parse_result == OK:
  193. print("JSON Parse Error: ", json.get_error_message(), " in ", json_string, " at line ", json.get_error_line())
  194. continue
  195. # Get the data from the JSON object.
  196. var node_data = json.data
  197. # Firstly, we need to create the object and add it to the tree and set its position.
  198. var new_object = load(node_data["filename"]).instantiate()
  199. get_node(node_data["parent"]).add_child(new_object)
  200. new_object.position = Vector2(node_data["pos_x"], node_data["pos_y"])
  201. # Now we set the remaining variables.
  202. for i in node_data.keys():
  203. if i == "filename" or i == "parent" or i == "pos_x" or i == "pos_y":
  204. continue
  205. new_object.set(i, node_data[i])
  206. .. code-tab:: csharp
  207. // Note: This can be called from anywhere inside the tree. This function is
  208. // path independent.
  209. public void LoadGame()
  210. {
  211. if (!FileAccess.FileExists("user://savegame.save"))
  212. {
  213. return; // Error! We don't have a save to load.
  214. }
  215. // We need to revert the game state so we're not cloning objects during loading.
  216. // This will vary wildly depending on the needs of a project, so take care with
  217. // this step.
  218. // For our example, we will accomplish this by deleting saveable objects.
  219. var saveNodes = GetTree().GetNodesInGroup("Persist");
  220. foreach (Node saveNode in saveNodes)
  221. {
  222. saveNode.QueueFree();
  223. }
  224. // Load the file line by line and process that dictionary to restore the object
  225. // it represents.
  226. using var saveFile = FileAccess.Open("user://savegame.save", FileAccess.ModeFlags.Read);
  227. while (saveFile.GetPosition() < saveFile.GetLength())
  228. {
  229. var jsonString = saveFile.GetLine();
  230. // Creates the helper class to interact with JSON.
  231. var json = new Json();
  232. var parseResult = json.Parse(jsonString);
  233. if (parseResult != Error.Ok)
  234. {
  235. GD.Print($"JSON Parse Error: {json.GetErrorMessage()} in {jsonString} at line {json.GetErrorLine()}");
  236. continue;
  237. }
  238. // Get the data from the JSON object.
  239. var nodeData = new Godot.Collections.Dictionary<string, Variant>((Godot.Collections.Dictionary)json.Data);
  240. // Firstly, we need to create the object and add it to the tree and set its position.
  241. var newObjectScene = GD.Load<PackedScene>(nodeData["Filename"].ToString());
  242. var newObject = newObjectScene.Instantiate<Node>();
  243. GetNode(nodeData["Parent"].ToString()).AddChild(newObject);
  244. newObject.Set(Node2D.PropertyName.Position, new Vector2((float)nodeData["PosX"], (float)nodeData["PosY"]));
  245. // Now we set the remaining variables.
  246. foreach (var (key, value) in nodeData)
  247. {
  248. if (key == "Filename" || key == "Parent" || key == "PosX" || key == "PosY")
  249. {
  250. continue;
  251. }
  252. newObject.Set(key, value);
  253. }
  254. }
  255. }
  256. Now we can save and load an arbitrary number of objects laid out
  257. almost anywhere across the scene tree! Each object can store different
  258. data depending on what it needs to save.
  259. Some notes
  260. ----------
  261. We have glossed over setting up the game state for loading. It's ultimately up
  262. to the project creator where much of this logic goes.
  263. This is often complicated and will need to be heavily
  264. customized based on the needs of the individual project.
  265. Additionally, our implementation assumes no Persist objects are children of other
  266. Persist objects. Otherwise, invalid paths would be created. To
  267. accommodate nested Persist objects, consider saving objects in stages.
  268. Load parent objects first so they are available for the :ref:`add_child()
  269. <class_node_method_add_child>`
  270. call when child objects are loaded. You will also need a way to link
  271. children to parents as the :ref:`NodePath
  272. <class_nodepath>` will likely be invalid.
  273. JSON vs binary serialization
  274. ----------------------------
  275. For simple game state, JSON may work and it generates human-readable files that are easy to debug.
  276. But JSON has many limitations. If you need to store more complex game state or
  277. a lot of it, :ref:`binary serialization<doc_binary_serialization_api>`
  278. may be a better approach.
  279. JSON limitations
  280. ~~~~~~~~~~~~~~~~
  281. Here are some important gotchas to know about when using JSON.
  282. * **Filesize:**
  283. JSON stores data in text format, which is much larger than binary formats.
  284. * **Data types:**
  285. JSON only offers a limited set of data types. If you have data types
  286. that JSON doesn't have, you will need to translate your data to and
  287. from types that JSON can handle. For example, some important types that JSON
  288. can't parse are: ``Vector2``, ``Vector3``, ``Color``, ``Rect2``, and ``Quaternion``.
  289. * **Custom logic needed for encoding/decoding:**
  290. If you have any custom classes that you want to store with JSON, you will
  291. need to write your own logic for encoding and decoding those classes.
  292. Binary serialization
  293. ~~~~~~~~~~~~~~~~~~~~
  294. :ref:`Binary serialization<doc_binary_serialization_api>` is an alternative
  295. approach for storing game state, and you can use it with the functions
  296. ``get_var`` and ``store_var`` of :ref:`class_FileAccess`.
  297. * Binary serialization should produce smaller files than JSON.
  298. * Binary serialization can handle most common data types.
  299. * Binary serialization requires less custom logic for encoding and decoding
  300. custom classes.
  301. Note that not all properties are included. Only properties that are configured
  302. with the :ref:`PROPERTY_USAGE_STORAGE<class_@GlobalScope_constant_PROPERTY_USAGE_STORAGE>`
  303. flag set will be serialized. You can add a new usage flag to a property by overriding the
  304. :ref:`_get_property_list<class_Object_private_method__get_property_list>`
  305. method in your class. You can also check how property usage is configured by
  306. calling ``Object._get_property_list``.
  307. See :ref:`PropertyUsageFlags<enum_@GlobalScope_PropertyUsageFlags>` for the
  308. possible usage flags.