godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.01k stars 21.17k forks source link

Singleton created in GDExtension don't recieve any events: _input, _physics_process, _process, _ready, etc don't work #83516

Open KirillGrigoriev opened 1 year ago

KirillGrigoriev commented 1 year ago

Godot version

v4.1.2.stable.official [399c9dc39]

System information

Godot v4.1.2.stable - Windows 10.0.19045 - Vulkan (Mobile) - dedicated NVIDIA GeForce GTX 650 (NVIDIA; 30.0.14.7430) - 13th Gen Intel(R) Core(TM) i5-13400 (16 Threads)

Issue description

Singleton created by memnew in GDExtension and registered by Engine::get_singleton()->register_singleton don't receive any events, any of virtual methods won't called, also don't shown in Node Tree Viewer, near "root" node, but all other functions seems work, singleton available from any GDScript.

Steps to reproduce

  1. Setup "godot-cpp" module follow official docs: https://docs.godotengine.org/en/stable/tutorials/scripting/gdextension/gdextension_cpp_example.html
  2. Create Foo.cpp use given source code below, compile and assemble extension use scons.
  3. Create simple project that use given extension.
  4. Check console.

Minimal reproduction project

Foo.cpp:

#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/godot.hpp>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/input_event.hpp>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

using namespace godot;

class Foo : public Node {
    GDCLASS(Foo , Node );

protected:
    static void _bind_methods();

public:
    virtual void _enter_tree() override { UtilityFunctions::print("_enter_tree"); }
    virtual void _exit_tree() override { UtilityFunctions::print("_exit_tree"); }
    virtual void _input(const Ref<InputEvent> &event) override { UtilityFunctions::print("_input"); }
    virtual void _physics_process(double delta) override { UtilityFunctions::print("_physics_process"); }
    virtual void _process(double delta) override { UtilityFunctions::print("_process"); }
    virtual void _ready() override { UtilityFunctions::print("_ready"); }
    void say_hello() { UtilityFunctions::print("Hello from Foo"); }

    virtual ~Foo() override {}
    Foo() : Node() {}
};

Foo* foo = nullptr;

void initialize_example_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; }

    ClassDB::register_class<Foo>();
    foo = memnew(Foo);
    Engine::get_singleton()->register_singleton("GlobalFoo", foo);
}
void uninitialize_example_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { return; }

    Engine::get_singleton()->unregister_singleton("GlobalFoo");
    memdelete(foo);
}
void Foo::_bind_methods() {
    ClassDB::bind_method(D_METHOD("say_hello"), &Foo::say_hello);
}

extern "C" {
    GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
        godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);

        init_obj.register_initializer(initialize_example_module);
        init_obj.register_terminator(uninitialize_example_module);
        init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);

        return init_obj.init();
    }
}

test_scene.gd:

extends Node3D

func _ready():
    GlobalFoo.say_hello()

Output:

Godot Engine v4.1.2.stable.official.399c9dc39 - https://godotengine.org
Vulkan API 1.2.175 - Forward Mobile - Using Vulkan Device #0: NVIDIA - NVIDIA GeForce GTX 650

Hello from Foo
dsnopek commented 1 year ago

Singleton's in the engine and GDExtension aren't like "autoload singletons", they are something totally different.

Some examples of engine singletons include Time, Input, ResourceLoader, etc. Since GDExtensions are meant to work like engine modules, that means GDExtension singletons are the same kind of thing.

Presently, if you want to make an autoload singleton using a GDExtension class, you'll need to make a scene where your GDExtension node is the root of the scene. Then add that scene as an autoload singleton in the editor, same as you would add any scene.

I hope that helps!

KirillGrigoriev commented 1 year ago

Thanks for reply. But, I need something that can hold data and interact with game world regardless of loaded scene. In other words: I need to create GDScript autoload singleton in GDExtension. It's like to lose inventory when load another level, for example. Now, I'm use autoload GDScript as wrapper, wrapper act exactly like I need, but I'm sure it shouldn't work like this. There are should be a way to create something like this in GDExtension without wrappers, if not in current version, but in future version of engine.

dsnopek commented 1 year ago

Now, I'm use autoload GDScript as wrapper, wrapper act exactly like I need, but I'm sure it shouldn't work like this.

You don't need to use GDScript. You can make scenes into autoload singletons too, not just GDScripts. So, if you make a scene that has your GDExtension node as the root of the scene, and then make that scene into an autoload singleton (via the usual dialog in the editor), it should work without needing to involve GDScript at all.

KirillGrigoriev commented 1 year ago

Oh, I got your idea. Create scene that extends from my singleton GDExtension class, save it, and add file to Autoload section. But, now somehow methods from my extension that depends on this class called before singleton created. Methods like (for example):

Bar::~Bar() { foo->m_bars.erase(std::remove(vec.begin(), vec.end(), this), vec.end()); }
Bar::Bar() { foo->m_bars.push_back(this); }

For some reason called both constructors and destructors at engine start. Singleton in extension referred by pointer that I set from singleton's constructor. Like:

Foo::Foo() : Node() { foo = this; }

Engine just crushed on start with null pointer exception. To sure about it, I just comment places like this, and engine started, but obviously extension initialized wrongly. Maybe, autoloads loads after creating scene and all nodes, or maybe autoloads don't loads at editor time, this I'm not sure about it. Also, seems like now scene is wrapper, with two scene loaded at same time engine starts and closes slower, I can see this lags, it's not important for me, but seems better use GDScript as wrapper and we still have a wrapper. I'm actually happy with GDScript wrapper, but I would happy engine I like become better in this part.

dsnopek commented 1 year ago

If you're still doing Engine::get_singleton()->register_singleton("GlobalFoo", foo); I think you should probably remove that. I don't think you don't want an engine-level singleton at all, you just want the project-level autoload singleton.

However, regarding this in particular:

For some reason called both constructors and destructors at engine start.

All engine classes (which includes GDExtension classes but also the ones built into the engine) are created and destroyed once during startup, so that the ClassDB can read some information from them. For this reason, you shouldn't do anything that creates important resources in a class constructor.

Usually, doing something like:

Foo::Foo() : Node() { foo = this; }

... is fine. I do this in the engine-level singletons in my GDExtensions. :-)

But, I don't think you really want an engine-level singleton, so I wouldn't do this in your case. Your singleton is an autoload singleton, which means you access it by grabbing it from the scene tree (ie get_tree()->get_node("/root/MyAutoloadName")).

KirillGrigoriev commented 1 year ago

Oh, I got it. Seems like engine-level singleton have better performance, I think, it's important for games, but I need more testing. Anyway, big thanks for advice and spended time. )