Hello everyone! I am learning how to use GDExtension, and wrote a custom class derived from Node3D to be used as a ‘container’ to build and control part of a scene. I originally wanted to make some of the objects member variables of this custom class.
In _enter_tree() I create the pointer, give the node a name, add it as a child to the ‘container’ node, and assign its ownership to the scene’s root. In _ready(), I give the objects their initial properties. And in _process(), I make any changes that would happen over time.
I have noticed that the pointers won’t be consistent between runs. If I add a new ‘container’ node to the scene (from the + in the scene tree), everything works as expected. But when I reload the project, that is when the problems arise. If I dynamic cast these member variable pointers to the classes that they should be, it returns a null pointer.
The way I have been getting around this is by using a dynamic cast on find_child(name_of_instance) in _ready() and _process() instead of having them as member variables. I would like to know if anyone has another way that they have been working around this, in the interest of improvement and curiosity.
Hey,
Could ysu provide a code snippet of your container class. And did I understand correctly that you want to pass your pointers back to gdscript and there the nullpointer problem araises oder does that happen in c++?
I think there was a misunderstanding; I only want to use C++, but during the game, the pointers stored as members are lost between _enter_tree and _ready. I am not sure if there are other points where they are lost as well.
(Based on the below testing and upon reflection, one could probably save the member variables in _ready for use in _process. )
I have removed some irrelevant parts to create a minimal example to share. Please let me know if I have made any potential mistakes.
NOTE (if you decide to test): To be able to best see what I am trying to point out, please add the Container node to the scene. Note that the “Ready” prints without the “lost” prints. Run the game, and notice that the pointer is lost between _enter_tree and _ready.
NOTE #2: You might need to give it a name other than Container, since that class already exists. I have a specific name for mine that is relevant to each use, but I wanted the code below to be more general than mine is.
#include "container.h"
#include <godot_cpp/core/memory.hpp> // for memnew
using namespace godot;
void Container::_bind_methods() {}
Container::Container() : Node3D() {
time_passed = 0.0;
}
Container::~Container() {}
void Container::_enter_tree ( ){
if(DEBUG) godot::UtilityFunctions::print("Enter Tree - Container.");
if(get_child_count() < 1){
member = memnew(MeshInstance3D); // create member variable with memnew
member->set_name("Member");
// add it to the node tree in _enter_tree, or else afaik it won't show up there in the editor
add_child(member);
member->set_owner(get_tree()->get_edited_scene_root());
}
}
void Container::_ready ( ){
if(DEBUG) godot::UtilityFunctions::print("Ready - Container.");
bool lost = false;
// I wrote this line to show when the member has been lost between _enter_tree and _ready (except when adding it to the node tree in the editor)
if(member == nullptr){godot::UtilityFunctions::print("We lost the pointer to Member in _ready."); lost = true; }
if(lost) member = dynamic_cast<MeshInstance3D*>(this->find_child("Member"));
if(member != nullptr && lost){godot::UtilityFunctions::print("We have retrieved the pointer in _ready.");}
}
// called every frame (as often as possible)
void Container::_process(double delta) {
// without this line, the pointer seems to be lost in the editor, but not during the game. That isn't terribly relevant though.
if (Engine::get_singleton()->is_editor_hint()) return; // Early return if we are in editor
time_passed += delta;
bool lost = false;
// it is maintained between _ready and _process; the lack of printing here proves that
if(member == nullptr){godot::UtilityFunctions::print("We lost the pointer to Member in _process."); lost = true; }
if(lost) member = dynamic_cast<MeshInstance3D*>(this->find_child("Member"));
if(member != nullptr && lost){godot::UtilityFunctions::print("We have retrieved the pointer in _process.");}
}
When I added the Container to my godot sceen it automatically appended a child of name Member
Thus the following code snippet will never be true, unless you delete that child. Once I deleted the child I no longer got the debug messages about the lost pointer. And it created a new mesh member. So maybe rather construct an else where you load the member from the existing child node if the get_child_count is bigger than 0.
I originally did the if-statement there so that it wouldn’t add copies of the child node, but didn’t think to add an else to “pick up” the children again. (Which, in hindsight, I do feel quite silly for. )
This would be the resulting change:
if(get_child_count() < 1){
member = memnew(MeshInstance3D); // create member variable with memnew
member->set_name("Member");
// add it to the node tree in _enter_tree, or else afaik it won't show up there in the editor
add_child(member);
member->set_owner(get_tree()->get_edited_scene_root());
}
else{
member = dynamic_cast<MeshInstance3D*>(this->find_child("Member"));
}
I have tested it, and it resolves all of the debug messages!
Thank you very much for your help and time! Let me know if you have any other thoughts.
Thank you for the guidance! I appreciate your time.
May I ask if you know what makes Object::cast_to better than dynamic_cast? I trust that it is the better choice, otherwise they wouldn’t have implemented it, but would love to know more!
I see that the code for cast_to uses dynamic_cast, but unfortunately I don’t really understand the importance of the rest of the function body.
Apologies if my follow-up question was answered in this sentence; I might be missing something simple.
I thought I understood why you should use it, but I did not.
internal::gdextension_interface_object_cast_to() checks if the _owner of p_object is castable to T by checking owner’s class tag ancestry and then returns _owner or nullptr as casted.
internal::get_object_instance_binding() in practice takes _owner and returns the instance binding: which is p_object, the object we passed in to cast.
Checking the owner’s class tag ancestry seems to imply that this cast can be done with RTTI disabled. But, dynamic_cast is used. Looking further, it appears that Godot v3 supported no RTTI and now Godot v4 RTTI is required.
AFAICT, using dynamic_cast directly will work as expected. For your custom gdextension classes and engine classes.
Side notes
Class Tag: The address of a static int unique for each class.
_owner: Owner is the Godot Engine object. For example, on the GDExtension side, when you memnew(Sprite2D) the owner of this gdext object is an instance of Godot’s Sprite2D.
The owner and instance binding have a bidirectional relationship. You can get the instance bindings from the owner and you can get the owner from the instance bindings.
Object::cast_to<T>() has had a history of difficulties.
Looks like an advantage of Object::cast_to<T>() is that Variants holding an Object will auto-unwrap to Object?
I wonder how this works when communicating through Godot between classes defined in multiple GDExtensions?
Fin
I am also always looking to learn more about Godot and GDExtension. If you have any discoveries, Ah-ha’s, or even find flaws in my posts, please do share!
I truly appreciate the level of effort and detail that went into your answer! Thank you so much for helping to break this down with/for me. It makes a lot more sense now.
I am going to go forward with this wealth of knowledge, apply it, and will let you know if I find anything else out!
I hope you have a really great day! Can’t tell you how much I appreciate your help!