Ho to save in files var of type "Node"

Godot Version

Godot 4.7-stable

Question

I cant find a straight answer to a relative simple question: how can i save (in a file) node references that are contained in a var of type Node?

From what i understood (please tell me if i’m right) the problem is that the Node var mostly contains an ID that links to the node it refers to: this ID is created randomly at node instantiation, therefore even if i save it, loading a new scene will generete different IDs, making the saved ones useless.

Approach 1:
if it was possible to set custom IDs to node, then maybe the original IDs in the var would work even after loading a new game from file. Problem is, i cant find the method to set IDs, not sure if it even exist

Approach 2:
get rid of the IDs and node references, and work with node paths. Problem is that the code became more complicated: either every node reference became a get_node(path_node), or i double the var (one of type Node for the reference and one of type String for the path), but then i need to connect them after the tree scene is completely instatiated.

Approach 2.1:
It would be great if i could make a var of type string that contains the path of the node (this way it can be easily saved), and connecting it to the node using setter and getter (this way i can refer to the node directly). Something like:

var node_to_be_saved : String:
set(value):
node_to_be_saved=value.get_path()
get():
get_node(node_to_be_saved)

The problem here is that value is of type Node, while the editor is expecting a type String, so it doesnt work.

So, how do pro coders handle this problem?

Node types may use pointers under the hood, these point to specific memory address in your computer that will always be different. Saving and loading pointers is not useful, the memory stored at that address will be deleted.

You can save paths if that’s useful to you, instead of keeping your var node_to_be_saved as a path forever, only convert it for your save and load function. Usually saving node paths isn’t useful, or only in very specific scenarios, so it’s better to marshal/serialize the nodes data and save that separately.

Sounds kind of XY problemy, can you explain your specific problem? The root issue?

The problem of making the conversion (node=>path) just for the save, is that it must be converted back to node reference (path=>node) during loading, and this doesnt work because sometimes the referenced node is not even loaded yet.

To better understand the request:

I’m working on a game where there are a lot of nodes rapresenting different in-game objects (like one node for each city, one node for each nation, one node for each unit, etc). This nodes are grouped as child of a “container” that is the parent of all nodes of the same type (like, NationContainer node contains all of the nations).
The container are “structural” and are present at game launch (even if the game is empty), while the child nodes are loaded after the player click “load” or “start game”.

Each node is deeply interconnected with the others, and i keep track of those connection using var of type Node (or, arrays of type Node).
EG: the city node has a “nation” var that contains the reference to the nation it belongs, the nation has a “city_list[]” var that contains all the city of the nation. This way i can easily point to different linked object using a simple code like city.nation.capital.building_list[0] (this will point to the first building in the capital of the nation the city belongs to)

I’m currently building the saving structure: i decided to save the var i need (stated in a string array) in a dictionary, the code is the following:

func save_to_dictionary(node : Node, list_of_var : Array[String] ):
var save_dict = Dictionary()
save_dict[“name”]=node.name
save_dict[“parent_path”]=node.get_parent().get_path() #the parent is also technically a reference, but since it's a container it will always be there during loading, so no problem here
save_dict[“scenetype”] = node.get_scene_file_path()
for variable in list_of_var:
if variable in node:
save_dict[variable]=node.get(variable)
return save_dict

and when i need to load

func load_single_object_from_dictionary(obj_dict : Dictionary):
var new_obj=load(obj_dict.scenetype).instantiate()
for variable in obj_dict.keys():
if variable in new_obj:
new_obj.set(variable, obj_dict[variable])
get_node(obj_dict.parent_path).add_child(new_obj)

the saved dictionary is stored in a file using FileAccess and the store_var method, and loaded using get_var().

This works wonderfully with all var types, but not for the Node type (although i dont think it’s a question of saving structure, but the problem is related to the aformentioned ID issue)

Hope it’s clear!

This is a question of the saving structure, saving and loading is fairly complex, some parts of it cannot be automatically serialized. A node is one of those unique cases you must account for, and you sort of do for "parent_path" and even recognize one of the problems that appears as nodes don’t always exist at their path as things get moved around and instantiated.

You should prepare to de-serialize these unique cases instead of blindly using set which will limit you to only the most basic types.

I dont have experience with serialization, and from what I read online i’m not understanding how it could help with my case.
Could you explain how would you implement the saving of a variable of type Node?

It depends on what I need to replicate the Node. I believe you know what is needed, which city is under which nation. It’s more a matter of actually saving and loading it, your current system only serializes with store_var/get_var defines, but you need more unique serialization.

If your nations are well-defined before loading then I’d say you could store the owning nation from each city. You could load nations first if they are ill-defined, or you could scale an Array as you load cities until the nations are properly defined post-load and remap the data.

It depends on what I need to replicate the Node.

I dont understand this point, there is no need to replicate anything, i just need to pass the node’s reference to a savefile so it can be restored when i launch the game again.

Loading in order won’t help, as there is no priority to follow (nations have variant of type node that reference to cities, and cities have variant of type node that reference to nation, depending which goes first will generate error).

Waiting for the loading to be finished and then remap is basically what i wrote on the original post on “Approach 2”: i work with node path (i guess it could be done in different ways), the point is storing the information about the referenced node using “something easy to be saved”, and only after loading is completed there is a phase of re-connection that uses this “something” to map the Node var to the intended node.

But I’m still looking for a more elegant way as i dont like it very much

This is the most elegant it’s going to get, Godot supports saving paths (as it’s a string), that’s what you’ll have to use. Maybe you could store the extra variable as a setter, much like described in your first post, but leverage both the node and it’s save path?

var nation: Node:
    set(value):
        nation = value
        nation_save_path = value.get_path() if value else ""
var nation_save_path: NodePath

func post_load() -> void:
    nation = get_node(nation_save_path)

While writing this topic i came up with another approach that i’d like to share in case other people might find it useful.

Basically the var of type Node only become a “helper class” that point to the path, which is actually the real source of the data and is much easier to store in a save file.
Also, when the save file is read and the scene is loaded again, you’ll only need to fill in the path_to_node string: the node reference will come automatically

var path_to_node : String
var node_reference_you_need_to_save : Node :
set(value):
path_to_node =value.get_path()
get():
return get_node(path_to_node )

(post deleted by author)

Yeah i guess this kind of approach is the cleaniest, i came up with a slighlty different solution but the concept is basically the same.
Now i only have to extend it to the Arrays of type Node (which i’m not sure will work as straightforward with setter and getters, but i’ll find a way), thank you!

It’s a pity thought that Godot does not support custom IDs setting for the instatiated object, it would simplify this process a lot

You may see a performance hit with get(): return get_node(...), each use of this variable incurring a node lookup will slow you down if you use this variable multiple times per-frame.

If the cities know their nations then the nations can populate such arrays as part of the post_load or setter. Take care when doubly-linking items like this, especially with setters and getters that will not transfer functions just because the reference is the same.

It’s not obvious what store_var(my_node) should/would do, IDs might not work in your case either as the ID only holds useful data if the node already exists, which you’ve stated they’re loaded later. Many games save objects that are wholly dynamically created, so the ID would mean nothing to such a system. The ID doesn’t store changes to properties or variables, so it’s useless where you’d want to save the progress of the node. All in all at best this acts as a path to the node, just like we’ve resolved on; but this is a niche use case that should be done explicitly so you understand what is happening and why.

I dont think the ID holds usefull data only when the node exists, i believe the IDs work in a similar way to how groups work.
In fact, you can call the method instance_from_id(any_random_ID_number) and, if the ID exists in the scene, it will return the node with that ID.
I might be wrong, but I suppose that a var of type Node is just a container for an ID with instance_from_id(ID) as getter

If my assumption is correct, with an “advanced” instantiate method (like scene.instantiate_with_custom_ID(ID)), you could set the ID of each object during scene loading, in order to re-create the scene exactly how it was in the previous session, up to the ID of each instance.
By doing so, saving with file.store_var(my_node) and then loading with my_node=file.get_var() will generate a Node var that contains an ID that is (or will be) assigned to a node of the newly loaded game, and you wont need a re-mapping phase as the connection will be instantly made (as soon as both nodes are loaded)

BUT, this is just speculation :stuck_out_tongue: i might be completely wrong