[C++] How to make custom nodes compatible to save?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By MegaGeneral

I have custom module which contains some class. Class contains some data, i.e. pointers to other nodes. When i try to save it, the editor crushes.

What i am missing there?

:bust_in_silhouette: Reply From: Zylann

Nodes get saved and loaded by their properties.
In addition, there is no way to save a “pointer” as is. Pointers are memory addresses, and these can change every time the game runs.

To have a persistent property in your custom node, It needs to be one of the following types:

bool, int, float, Vector2, Vector3, Color, Transform, Transform2D, Basis, String, Dictionary (non-cyclic), Array (non-cyclic), PoolXXXArray.

It can also be a resource, with Ref<T>, where T inherits Resource, but reference cycles are not allowed.

Then, you need a getter function and a setter function, here example with Sprite.offset:

ClassDB::bind_method(D_METHOD("set_offset", "offset"), &Sprite::set_offset);
ClassDB::bind_method(D_METHOD("get_offset"), &Sprite::get_offset);

And you need a property declaration, like so:

ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset"), "set_offset", "get_offset");

If you want to save relations between objects, things get a bit more complicated. References are usually saved using specific IDs, or node paths. Sometimes they can even be avoided entirely (using the node hierarchy itself for example). You may have to explain what kind of links you want to save otherwise.

Note: I’m assuming you are creating an engine module, not GDNative.

The ‘native’ types i already use and it works fine, but i need to save some relation between objects, right.
There are class player. I want to save it inside scene.

Yeah, now i work on module.

hpp:
class Player : public Spatial
{
	GDCLASS(Player, Spatial)
private:
	Camera* _camera;
	State* _state;
public:
	static void _bind_methods();

	Player();

	Camera* get_camera() const;
	void set_camera(Object* p_camera);

	State* get_state() const;
	void set_state(Object* p_state);
};
cpp:
Player::Player()  {	}
void Player::_bind_methods() 
{
	ClassDB::bind_method(D_METHOD("get_camera"), &Player::get_camera);
	ClassDB::bind_method(D_METHOD("set_camera", "camera"), 	&Player::set_camera);
	ADD_PROPERTY(PropertyInfo(Variant::Type::OBJECT, "camera"), "set_camera", "get_camera");

	ClassDB::bind_method(D_METHOD("get_state"), &Player::get_state);
	ClassDB::bind_method(D_METHOD("set_state", "state"), &Player::set_state);
	ADD_PROPERTY(PropertyInfo(Variant::Type::OBJECT, "state"), "set_state", 			"get_state");
}

Camera* Player::get_camera() const { return _camera; }
void Player::set_camera(Object* p_camera) { _camera = p_camera ? 	cast_to<Camera>(p_camera) : nullptr; }

State* Player::get_state() const { return _state; }
void Player::set_state(Object* p_state) { _state = p_state ? cast_to<State>(p_state) : 	nullptr; }

State - Reference class (empty now). Propably i have to write some saver?
So, there is class with integers which works fine and can be saved in scene.

P.S. I have other class that named Class which saves successfully with object properties to State*. Now i add into Player CTOR nullptr assignment to camera and state, and it is working, but i can not assign some node by property in inspector anyway.

MegaGeneral | 2020-03-17 06:23

You can’t save node types inside properties, because nodes are supposed to be saved along with the rest of the scene through their parent-child relationships, not inside another object.

To get back references to the camera (or any other node which you don’t know its path in advance), you have to proceed indirectly, by exposing its NodePath, so that you can obtain it through get_node later (when _ready). Raw pointers won’t work. Note that if you have only one camera you can get the active camera using get_viewport().get_camera(), so no need to save a reference to it.

I have no idea what State is, especially if it’s empty…
If it only holds data, you can surely save it as several properties, or as a dictionary of values (with a property exposing it as such).
I had to save an entire data graph in my own module, that’s how I did it: https://github.com/Zylann/godot_voxel/blob/4176894383a0caad96b147b2da4ab946ac709069/generators/graph/voxel_generator_graph.cpp#L680

Note that the same principles apply to GDScript, so if you have problems doing it in C++, you may want to try make it work in GDScript first.

Zylann | 2020-03-17 19:24

I will use this system not only for camera.
Okay, i undrestood about NodePath.
Then other question: how to organize the work with NodePath’es as properties?
Make property for Object*? Or for NodePath*?


The main problem of godot, i think - low-documented engine code and deficit of tutorials for C++

MegaGeneral | 2020-03-18 08:53

Then other question: how to organize the work with NodePath’es as properties?

Once you have a node path, you can get the node using get_node(), and store it in a variable if you wish.
In GDScript, the pattern of obtaining a node for which the path is determined from inspector would be this:

export(NodePath) external_node_path
onready var _external_node = get_node(external_node_path)

func _process(delta)L
    print("Hello ", _external_node.name)

There is also a very common pattern for non-changing nodes that you actually know the path, which I use a lot because there is no point wiring nodes in the inspector that are children of the same scene anyways, with properly chosen names:

onready var _owned_node = get_node("Path/To/MyNode")

(as you can also see a larger example here: https://github.com/Zylann/saga_toolkit_app/blob/a417eb35f33166e9e12bda019397b6482b92171c/main.gd#L23

For the case of node paths, if you were to do the same in C++:

// .h

NodePath _external_node_path;
ExternalNodeType *_external_node;

// .cpp

NodePath MyClass::get_external_node_path() const {
	return _external_node_path;
}

void MyClass::set_external_node_path(NodePath path) {
	_external_node_path = path;
}

void _notification(int p_what) {
	if (p_what == NOTIFICATION_READY) {
		_external_node = Object::cast_to<ExternalNodeType>(get_node(_external_node_path));
	}
}

void MyClass::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_external_node_path", "data"), &MyClass::set_external_node_path);
	ClassDB::bind_method(D_METHOD("get_external_node_path"), &MyClass::get_external_node_path);

	ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "external_node_path"), "set_external_node_path", "get_external_node_path");
}

The difference is much more verbose to do the same thing. Also, it can crash if something is wrong (you may use a debugger to have more insight), while the script implementation won’t crash, and will tell you what’s wrong in more details. So it’s a good idea to prototype in script first, and then translate.

The main problem of godot, i think - low-documented engine code and deficit of tutorials for C++

While this is true, I’ll reiterate my last point:
the same principles apply to GDScript, so if you have problems doing it in C++, you may want to try make it work in GDScript first. I’ll also add that the latter is much more documented, because it’s how the engine is primarily meant to be used. This is why there aren’t specifically as many tutorials for C++, it’s not a common entry level. There are still syntax specificities, but when you know how to script the engine, it often just takes looking at existing code to learn how the same things are done.

Note about your original question, to solve a crash, you need to use a C++ debugger, to confirm what’s really happening. It’s almost mandatory when you do C++, as it will point you exactly what call stack failed, and tell you the state of variables.

Zylann | 2020-03-18 22:39