Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

GDExtension C++ example

Introduction

The C++ bindings for GDExtension are built on top of the C GDExtension API and provide a nicer way to "extend" nodes and other built-in classes in Godot using C++. This new system allows the extension of Godot to nearly the same level as statically linked C++ modules.

You can download the included example in the test folder of the godot-cpp repository on GitHub.

Mise en place du projet

Il y a quelques prérequis dont vous aurez besoin :

  • a Godot 4 executable,

  • un compilateur C++,

  • SCons comme outil de construction,

  • une copie du dépôt godot-cpp.

Voir aussi Compiling car les outils de build sont identiques à ceux dont vous avez besoin pour compiler Godot à partir de la source.

You can download the godot-cpp repository from GitHub or let Git do the work for you. Note that this repository has different branches for different versions of Godot. GDExtensions will not work in older versions of Godot (only Godot 4 and up) and vice versa, so make sure you download the correct branch.

Note

To use GDExtension you need to use the godot-cpp branch that matches the version of Godot that you are targeting. For example, if you're targeting Godot 4.1, use the 4.1 branch, which is what is shown through out this tutorial.

The master branch is the development branch which is updated regularly to work with Godot's master branch.

Avertissement

Our long-term goal is that GDExtensions targeting an earlier version of Godot will work in later minor versions, but not vice-versa. For example, a GDExtension targeting Godot 4.2 should work just fine in Godot 4.3, but one targeting Godot 4.3 won't work in Godot 4.2.

However, GDExtension is currently experimental, which means that we may break compatibility in order to fix major bugs or include critical features. For example, GDExtensions created for Godot 4.0 aren't compatible with Godot 4.1 (see Updating your GDExtension for 4.1).

If you are versioning your project using Git, it is recommended to add it as a Git submodule:

mkdir gdextension_cpp_example
cd gdextension_cpp_example
git init
git submodule add -b 4.1 https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init

Alternatively, you can also clone it to the project folder:

mkdir gdextension_cpp_example
cd gdextension_cpp_example
git clone -b 4.1 https://github.com/godotengine/godot-cpp

Note

If you decide to download the repository or clone it into your folder, make sure to keep the folder layout the same as we've setup here. Much of the code we'll be showcasing here assumes the project has this layout.

Si vous avez cloné l'exemple à partir du lien indiqué dans l'introduction, les sous-modules ne sont pas automatiquement initialisés. Vous devrez exécuter les commandes suivantes :

cd gdextension_cpp_example
git submodule update --init

This will initialize the repository in your project folder.

Construire(Building) des liaisons(bindings) C ++

Maintenant que nous avons téléchargé nos prérequis, il est temps de build les liaisons(bindings) C++.

Le dépôt contient une copie des métadonnées pour la version actuelle de Godot, mais si vous devez construire ces liens pour une version plus récente de Godot, il suffit d'appeler l'exécutable Godot :

godot --dump-extension-api

The resulting extension_api.json file will be created in the executable's directory. Copy it to the project folder and add custom_api_file=<PATH_TO_FILE> to the scons command below.

To generate and compile the bindings, use this command (replacing <platform> with windows, linux or macos depending on your OS):

The build process automatically detects the number of CPU threads to use for parallel builds. To specify a number of CPU threads to use, add -jN at the end of the SCons command line where N is the number of CPU threads to use.

cd godot-cpp
scons platform=<platform> custom_api_file=<PATH_TO_FILE>
cd ..

Cette étape prendra un certain temps. Lorsqu'elle sera terminée, vous devriez disposer de bibliothèques statiques pouvant être compilées dans votre projet et stockées dans godot-cpp/bin/.

Note

Vous devrez peut-être ajouter bits=64 à la commande sous Windows ou Linux.

Créer un plugin simple

Il est maintenant temps de construire un véritable plugin. Nous commencerons par créer un projet Godot vide dans lequel nous placerons quelques fichiers.

Open Godot and create a new project. For this example, we will place it in a folder called demo inside our GDExtension's folder structure.

Dans notre projet de démo, nous allons créer une scène contenant un nœud appelé "Main" et nous l'enregistrerons sous le nom de main.tscn. Nous y reviendrons plus tard.

Back in the top-level GDExtension module folder, we're also going to create a subfolder called src in which we'll place our source files.

You should now have demo, godot-cpp, and src directories in your GDExtension module.

Your folder structure should now look like this:

gdextension_cpp_example/
|
+--demo/                  # game example/demo to test the extension
|
+--godot-cpp/             # C++ bindings
|
+--src/                   # source code of the extension we are building

In the src folder, we'll start with creating our header file for the GDExtension node we'll be creating. We will name it gdexample.h:

#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H

#include <godot_cpp/classes/sprite2d.hpp>

namespace godot {

class GDExample : public Sprite2D {
    GDCLASS(GDExample, Sprite2D)

private:
    double time_passed;

protected:
    static void _bind_methods();

public:
    GDExample();
    ~GDExample();

    void _process(double delta) override;
};

}

#endif

There are a few things of note to the above. We include sprite2d.hpp which contains bindings to the Sprite2D class. We'll be extending this class in our module.

We're using the namespace godot, since everything in GDExtension is defined within this namespace.

Then we have our class definition, which inherits from our Sprite2D through a container class. We'll see a few side effects of this later on. The GDCLASS macro sets up a few internal things for us.

Après cela, nous déclarons une unique variable membre appelée time_passed.

In the next block we're defining our methods, we have our constructor and destructor defined, but there are two other functions that will likely look familiar to some, and one new method.

The first is _bind_methods, which is a static function that Godot will call to find out which methods can be called and which properties it exposes. The second is our _process function, which will work exactly the same as the _process function you're used to in GDScript.

Implémentons nos fonctions en créant notre fichier gdexample.cpp :

#include "gdexample.h"
#include <godot_cpp/core/class_db.hpp>

using namespace godot;

void GDExample::_bind_methods() {
}

GDExample::GDExample() {
    // Initialize any variables here.
    time_passed = 0.0;
}

GDExample::~GDExample() {
    // Add your cleanup here.
}

void GDExample::_process(double delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(10.0 + (10.0 * sin(time_passed * 2.0)), 10.0 + (10.0 * cos(time_passed * 1.5)));

    set_position(new_position);
}

This one should be straightforward. We're implementing each method of our class that we defined in our header file.

Note our _process function, which keeps track of how much time has passed and calculates a new position for our sprite using a sine and cosine function.

There is one more C++ file we need; we'll name it register_types.cpp. Our GDExtension plugin can contain multiple classes, each with their own header and source file like we've implemented GDExample up above. What we need now is a small bit of code that tells Godot about all the classes in our GDExtension plugin.

#include "register_types.h"

#include "gdexample.h"

#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>

using namespace godot;

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

    ClassDB::register_class<GDExample>();
}

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

extern "C" {
// Initialization.
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();
}
}

The initialize_example_module and uninitialize_example_module functions get called respectively when Godot loads our plugin and when it unloads it. All we're doing here is parse through the functions in our bindings module to initialize them, but you might have to set up more things depending on your needs. We call the function register_class for each of our classes in our library.

The important function is the third function called example_library_init. We first call a function in our bindings library that creates an initialization object. This object registers the initialization and termination functions of the GDExtension. Furthermore, it sets the level of initialization (core, servers, scene, editor, level).

At last, we need the header file for the register_types.cpp named register_types.h.

#ifndef GDEXAMPLE_REGISTER_TYPES_H
#define GDEXAMPLE_REGISTER_TYPES_H

#include <godot_cpp/core/class_db.hpp>

using namespace godot;

void initialize_example_module(ModuleInitializationLevel p_level);
void uninitialize_example_module(ModuleInitializationLevel p_level);

#endif // GDEXAMPLE_REGISTER_TYPES_H

Compiler le plugin

Nous ne pouvons pas facilement écrire à la main un fichier SConstruct que SCons utiliserait pour construire(building). Pour cet exemple, il suffit d'utiliser ce fichier SConstruct codé en dur que nous avons préparé. Un exemple plus détaillé et personnalisable sur la façon d'utiliser ces fichiers de construction(build) sera présenté dans un tutoriel ultérieur.

Note

This SConstruct file was written to be used with the latest godot-cpp master, you may need to make small changes using it with older versions or refer to the SConstruct file in the Godot 4.0 documentation.

Once you've downloaded the SConstruct file, place it in your GDExtension folder structure alongside godot-cpp, src and demo, then run:

scons platform=<platform>

Vous devriez maintenant être en mesure de trouver le module dans demo/bin/<platform>.

Note

Here, we've compiled both godot-cpp and our gdexample library as debug builds. For optimized builds, you should compile them using the target=template_release switch.

Using the GDExtension module

Before we jump back into Godot, we need to create one more file in demo/bin/.

This file lets Godot know what dynamic libraries should be loaded for each platform and the entry function for the module. It is called gdexample.gdextension.

[configuration]

entry_symbol = "example_library_init"
compatibility_minimum = "4.1"

[libraries]

macos.debug = "res://bin/libgdexample.macos.template_debug.framework"
macos.release = "res://bin/libgdexample.macos.template_release.framework"
windows.debug.x86_32 = "res://bin/libgdexample.windows.template_debug.x86_32.dll"
windows.release.x86_32 = "res://bin/libgdexample.windows.template_release.x86_32.dll"
windows.debug.x86_64 = "res://bin/libgdexample.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://bin/libgdexample.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "res://bin/libgdexample.linux.template_debug.x86_64.so"
linux.release.x86_64 = "res://bin/libgdexample.linux.template_release.x86_64.so"
linux.debug.arm64 = "res://bin/libgdexample.linux.template_debug.arm64.so"
linux.release.arm64 = "res://bin/libgdexample.linux.template_release.arm64.so"
linux.debug.rv64 = "res://bin/libgdexample.linux.template_debug.rv64.so"
linux.release.rv64 = "res://bin/libgdexample.linux.template_release.rv64.so"
android.debug.x86_64 = "res://bin/libgdexample.android.template_debug.x86_64.so"
android.release.x86_64 = "res://bin/libgdexample.android.template_release.x86_64.so"
android.debug.arm64 = "res://bin/libgdexample.android.template_debug.arm64.so"
android.release.arm64 = "res://bin/libgdexample.android.template_release.arm64.so"

This file contains a configuration section that controls the entry function of the module. You should also set the minimum compatible Godot version with compatability_minimum, which prevents older version of Godot from trying to load your extension.

The libraries section is the important bit: it tells Godot the location of the dynamic library in the project's filesystem for each supported platform. It will also result in just that file being exported when you export the project, which means the data pack won't contain libraries that are incompatible with the target platform.

Finally, the dependencies section allows you to name additional dynamic libraries that should be included as well. This is important when your GDExtension plugin implements someone else's library and requires you to supply a third-party dynamic library with your project.

Here is another overview to check the correct file structure:

gdextension_cpp_example/
|
+--demo/                  # game example/demo to test the extension
|   |
|   +--main.tscn
|   |
|   +--bin/
|       |
|       +--gdexample.gdextension
|
+--godot-cpp/             # C++ bindings
|
+--src/                   # source code of the extension we are building
|   |
|   +--register_types.cpp
|   +--register_types.h
|   +--gdexample.cpp
|   +--gdexample.h

Time to jump back into Godot. We load up the main scene we created way back in the beginning and now add a newly available GDExample node to the scene:

../../../_images/gdextension_cpp_nodes.webp

We're going to assign the Godot logo to this node as our texture, disable the centered property:

../../../_images/gdextension_cpp_sprite.webp

Nous sommes prêts à lancer le projet :

../../../_images/gdextension_cpp_animated.gif

Custom editor icon

By default, Godot uses the node icon in the scene dock for GDExtension nodes. The custom icon can be added via the gdextension file. The node's icon is set by reference to its name and resource path of an SVG file.

Par exemple :

[icons]

GDExample = "res://icons/gd_example.svg"

The path should point to a 16 by 16 pixel SVG image. Read the guide for creating icons for more information.

Ajouter des propriétés

GDScript allows you to add properties to your script using the export keyword. In GDExtension you have to register the properties with a getter and setter function or directly implement the _get_property_list, _get and _set methods of an object (but that goes far beyond the scope of this tutorial).

Lets add a property that allows us to control the amplitude of our wave.

In our gdexample.h file we need to add a member variable and getter and setter functions:

...
private:
    double time_passed;
    double amplitude;

public:
    void set_amplitude(const double p_amplitude);
    double get_amplitude() const;
...

Dans notre fichier ``gdexample.cpp`, il faut faire un certain nombre de changements . Nous ne montrerons que les méthodes que nous changerons donc ne supprimez pas les lignes qui n’apparaissent pas ici :

void GDExample::_bind_methods() {
    ClassDB::bind_method(D_METHOD("get_amplitude"), &GDExample::get_amplitude);
    ClassDB::bind_method(D_METHOD("set_amplitude", "p_amplitude"), &GDExample::set_amplitude);
    ClassDB::add_property("GDExample", PropertyInfo(Variant::FLOAT, "amplitude"), "set_amplitude", "get_amplitude");
}

GDExample::GDExample() {
    // Initialize any variables here.
    time_passed = 0.0;
    amplitude = 10.0;
}

void GDExample::_process(double delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);
}

void GDExample::set_amplitude(const double p_amplitude) {
    amplitude = p_amplitude;
}

double GDExample::get_amplitude() const {
    return amplitude;
}

Lorsque vous compilerez le module avec ces changements, vous verrez qu’une propriété a été ajoutée à notre interface. Vous pouvez maintenant changer cette propriété et en lançant le projet, vous verrez que notre icône Godot se déplace sur une plus grande zone.

Faisons la même chose mais pour la vitesse de notre animation et utilisons une fonction setter et getter. Notre fichier d'en-tête gdexample.h ne nécessite à nouveau que quelques lignes de code supplémentaires :

...
    double amplitude;
    double speed;
...
    void _process(double delta) override;
    void set_speed(const double p_speed);
    double get_speed() const;
...

Il faut quelques modifications supplémentaires dans notre fichier gdexample.cpp. Là encore nous ne montrerons que les méthodes qui ont changé, donc ne pas supprimer ce que nous ne montrons pas ici :

void GDExample::_bind_methods() {
    ...
    ClassDB::bind_method(D_METHOD("get_speed"), &GDExample::get_speed);
    ClassDB::bind_method(D_METHOD("set_speed", "p_speed"), &GDExample::set_speed);
    ClassDB::add_property("GDExample", PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,20,0.01"), "set_speed", "get_speed");
}

GDExample::GDExample() {
    time_passed = 0.0;
    amplitude = 10.0;
    speed = 1.0;
}

void GDExample::_process(double delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);
}

...

void GDExample::set_speed(const double p_speed) {
    speed = p_speed;
}

double GDExample::get_speed() const {
    return speed;
}

Now when the project is compiled, we'll see another property called speed. Changing its value will make the animation go faster or slower. Furthermore, we added a property range which describes in which range the value can be. The first two arguments are the minimum and maximum value and the third is the step size.

Note

For simplicity, we've only used the hint_range of the property method. There are a lot more options to choose from. These can be used to further configure how properties are displayed and set on the Godot side.

Signaux

Last but not least, signals fully work in GDExtension as well. Having your extension react to a signal given out by another object requires you to call connect on that object. We can't think of a good example for our wobbling Godot icon, we would need to showcase a far more complete example.

Voici la syntaxe requise :

some_other_node->connect("the_signal", Callable(this, "my_method"));

To connect our signal the_signal from some other node with our method my_method, we need to provide the connect method with the name of the signal and a Callable. The Callable holds information about an object on which a method can be called. In our case, it associates our current object instance this with the method my_method of the object. Then the connect method will add this to the observers of the_signal. Whenever the_signal is now emitted, Godot knows which method of which object it needs to call.

Note that you can only call my_method if you've previously registered it in your _bind_methods method. Otherwise Godot will not know about the existence of my_method.

To learn more about Callable, check out the class reference here: Callable.

Il est plus courant que votre objet envoie des signaux. Pour notre icône Godot vacillante, nous allons faire quelque chose de stupide juste pour montrer comment cela fonctionne. Nous allons émettre un signal chaque fois qu'une seconde s'est écoulée et transmettre la nouvelle position.

Dans notre fichier d'en-tête gdexample.h, nous devons définir un nouveau membre time_emit :

...
    double time_passed;
    double time_emit;
    double amplitude;
...

Cette fois, les changements dans gdexample.cpp sont plus élaborés. Tout d'abord, vous devrez définir time_emit = 0.0; dans notre méthode _init ou dans notre constructeur. Nous allons examiner les deux autres modifications nécessaires une par une.

In our _bind_methods method, we need to declare our signal. This is done as follows:

void GDExample::_bind_methods() {
    ...
    ClassDB::add_property("GDExample", PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,20,0.01"), "set_speed", "get_speed");

    ADD_SIGNAL(MethodInfo("position_changed", PropertyInfo(Variant::OBJECT, "node"), PropertyInfo(Variant::VECTOR2, "new_pos")));
}

Here, our ADD_SIGNAL macro can be a single call with a MethodInfo argument. MethodInfo's first parameter will be the signal's name, and its remaining parameters are PropertyInfo types which describe the essentials of each of the method's parameters. PropertyInfo parameters are defined with the data type of the parameter, and then the name that the parameter will have by default.

So here, we add a signal, with a MethodInfo which names the signal "position_changed". The PropertyInfo parameters describe two essential arguments, one of type Object, the other of type Vector2, respectively named "node" and "new_pos".

Ensuite, nous devons modifier notre méthode _process :

void GDExample::_process(double delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);

    time_emit += delta;
    if (time_emit > 1.0) {
        emit_signal("position_changed", this, new_position);

        time_emit = 0.0;
    }
}

Après qu'une seconde se soit écoulée, nous émettons notre signal et remettons notre compteur à zéro. Nous pouvons ajouter nos valeurs de paramètres directement dans emit_signal.

Once the GDExtension library is compiled, we can go into Godot and select our sprite node. In the Node dock, we can find our new signal and link it up by pressing the Connect button or double-clicking the signal. We've added a script on our main node and implemented our signal like this:

extends Node

func _on_Sprite2D_position_changed(node, new_pos):
    print("The position of " + node.get_class() + " is now " + str(new_pos))

Chaque seconde, on envoie notre position à la console.

La suite

We hope the above example showed you the basics. You can build upon this example to create full-fledged scripts to control nodes in Godot using C++.