Exemple GDNative C++

Introduction

Ce tutoriel s'appuie sur les informations données dans l'exemple GDNative C, nous vous recommandons donc vivement de le lire en premier.

Les liaisons(bindings) C++ pour GDNative sont construites au-dessus de l'API NativeScript GDNative et fournissent un moyen plus agréable d'"étendre" les nœuds de Godot en utilisant le C++. Cela équivaut à écrire des scripts en GDScript, mais en C++ à la place.

Vous pouvez télécharger l'exemple complet que nous allons créer dans ce tutoriel on GitHub.

Mise en place du projet

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

  • un exécutable Godot 3.x,

  • 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.

Vous pouvez télécharger ces dépôts sur GitHub ou laisser Git faire le travail pour vous. Notez que ces dépôts ont maintenant différentes branches pour différentes versions de Godot. Les modules GDNative écrits pour une version antérieure de Godot fonctionneront dans les versions plus récentes (à l'exception d'une rupture de compatibilité dans les interfaces ARVR entre 3.0 et 3.1) mais pas l'inverse, donc assurez-vous de télécharger la bonne branche. Notez également que la version de Godot que vous utilisez pour générer l'api.json devient votre version minimale.

Note

GDExtension a été fusionnée dans la branche master de godot-cpp, mais elle n'est compatible qu'avec le prochain Godot 4.0. Par conséquent, vous devez utiliser la branche 3.x de godot-cpp pour utiliser GDNative et suivre cet exemple.

Ce tutoriel couvre uniquement GDNative dans Godot 3.x, not GDExtension dans Godot 4.0.

Si vous versionnez votre projet en utilisant Git, il est bon de les ajouter en tant que sous-modules Git :

mkdir gdnative_cpp_example
cd gdnative_cpp_example
git init
git submodule add -b 3.x https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init

Si vous décidez de télécharger simplement les dépôts ou de les cloner dans votre dossier de projet, assurez-vous de garder la disposition du dossier identique à celle décrite ici, car une grande partie du code que nous allons présenter ici suppose que le projet suit cette disposition.

Assurez-vous que vous clonez récursivement pour pull les deux dépôts :

mkdir gdnative_cpp_example
cd gdnative_cpp_example
git clone --recursive -b 3.x https://github.com/godotengine/godot-cpp

Note

godot-cpp inclut maintenant godot_headers comme un sous-module imbriqué, si vous les avez téléchargés manuellement, assurez-vous de placer godot_headers dans le dossier godot-cpp.

Vous n’êtes pas obligé de procéder de cette façon, mais nous avons trouvé que c’était plus facile à gérer. Si vous décidez de télécharger les dépôts ou de les cloner dans votre dossier, assurez-vous de conserver la disposition du dossier telle que nous l'avons configurée ici. La plupart du code que nous allons présenter ici suppose que le projet a cette disposition.

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 gdnative_cpp_example
git submodule update --init --recursive

Cela permettra de cloner ces deux dépôts dans votre dossier de projet.

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 --gdnative-generate-json-api api.json

Placez le fichier api.json résultant dans le dossier du projet et ajoutez use_custom_api_file=yes custom_api_file=../api.json à la commande scons ci-dessous.

Pour générer et compiler les liaisons(bindings), utilisez cette commande (en remplaçant <platform> par windows, linux ou osx selon votre système d'exploitation) :

Pour accélérer la compilation, ajoutez -jN à la fin de la ligne de commande SCons où N est le nombre de threads CPU que vous avez sur votre système. L'exemple ci-dessous utilise 4 threads.

cd godot-cpp
scons platform=<platform> generate_bindings=yes -j4
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.

Ouvrez Godot et créez un nouveau projet. Pour cet exemple, nous allons le placer dans un dossier appelé demo à l'intérieur de la structure des dossiers de notre module GDNative.

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.

De retour dans le dossier du module GDNative, nous allons également créer un sous-dossier appelé src dans lequel nous placerons nos fichiers source.

Vous devriez maintenant avoir les répertoires demo, godot-cpp, godot_headers, et src dans votre module GDNative.

Dans le dossier src, nous commencerons par créer notre fichier d'en-tête pour le nœud GDNative que nous allons créer. Nous le nommerons gdexample.h :

#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H

#include <Godot.hpp>
#include <Sprite.hpp>

namespace godot {

class GDExample : public Sprite {
    GODOT_CLASS(GDExample, Sprite)

private:
    float time_passed;

public:
    static void _register_methods();

    GDExample();
    ~GDExample();

    void _init(); // our initializer called by Godot

    void _process(float delta);
};

}

#endif

Il y a quelques éléments à noter dans ce qui précède. Nous incluons Godot.hpp qui contient toutes nos définitions de base. Ensuite, nous incluons Sprite.hpp qui contient les liens vers la classe Sprite. Nous allons étendre cette classe dans notre module.

Nous utilisons l'espace de noms godot, puisque tout ce qui est en GDNative est défini dans cet espace de noms.

Ensuite, nous avons notre définition de la classe, qui hérite de notre Sprite par le biais d'une classe conteneur. Nous en verrons les effets secondaires plus tard. La macro GODOT_CLASS met en place quelques éléments internes pour nous.

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

Dans le bloc suivant, nous définissons nos méthodes, nous avons évidemment défini notre constructeur et notre destructeur, mais il y a deux autres fonctions qui sembleront probablement familières à certains, et une nouvelle méthode.

La première est _register_methods, qui est une fonction statique que Godot appellera pour savoir quelles méthodes peuvent être appelées sur notre NativeScript et quelles propriétés il expose. La seconde est notre fonction _process, qui fonctionnera exactement comme la fonction _process à laquelle vous êtes habitué dans le GDScript. La troisième est notre fonction _init qui est appelée après que Godot ait correctement configuré notre objet. Elle doit exister même si vous n'y placez aucun code.

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

#include "gdexample.h"

using namespace godot;

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
}

GDExample::GDExample() {
}

GDExample::~GDExample() {
    // add your cleanup here
}

void GDExample::_init() {
    // initialize any variables here
    time_passed = 0.0;
}

void GDExample::_process(float 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);
}

Celui-ci devrait être simple. Nous implémentons chaque méthode de notre classe que nous avons définie dans notre fichier d'en-tête. Notez que l'appel register_method doit exposer la méthode _process, sinon Godot ne pourra pas l'utiliser. Cependant, nous n'avons pas à parler à Godot de nos fonctions constructeur, destructeur et _init.

L'autre méthode importante est notre fonction _process, qui suit tout simplement le temps écoulé et calcule une nouvelle position pour notre sprite en utilisant une fonction sinus et cosinus. Ce qui se démarque est l'appel de owner->set_position pour appeler l'une des méthodes intégrées de notre Sprite. Cela est dû au fait que notre classe est une classe conteneur. owner pointe vers le nœud Sprite réel auquel notre script se rapporte.

Il nous faut encore un fichier C++ ; nous le nommerons gdlibrary.cpp. Notre plugin GDNative peut contenir plusieurs NativeScripts, chacun avec son propre en-tête et son propre fichier source, comme nous l'avons fait pour GDExample ci-dessus. Ce dont nous avons besoin maintenant, c'est d'un petit bout de code qui renseigne Godot sur tous les NativeScripts de notre plugin GDNative.

#include "gdexample.h"

extern "C" void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *o) {
    godot::Godot::gdnative_init(o);
}

extern "C" void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *o) {
    godot::Godot::gdnative_terminate(o);
}

extern "C" void GDN_EXPORT godot_nativescript_init(void *handle) {
    godot::Godot::nativescript_init(handle);

    godot::register_class<godot::GDExample>();
}

Notez que nous n'utilisons pas l'espace de noms godot ici, puisque les trois fonctions implémentées ici doivent être définies sans espace de noms.

Les fonctions godot_gdnative_init et godot_gdnative_terminate sont appelées respectivement quand Godot charge notre plugin et quand il le décharge. Tout ce que nous faisons ici, c'est analyser les fonctions de notre module de liaisons(bindings) pour les initialiser, mais il se peut que vous ayez à configurer d'autres choses en fonction de vos besoins.

La fonction importante est la troisième fonction appelée godot_nativescript_init. Nous appelons d'abord une fonction dans notre bibliothèque de liaisons(bindings) qui fait ses trucs habituels. Ensuite, nous appelons la fonction register_class pour chacune de nos classes dans notre bibliothèque.

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

Ce fichier SConstruct a été écrit pour être utilisé avec le dernier godot-cpp master, vous devrez peut-être faire de petites modifications en l'utilisant avec des versions plus anciennes ou vous référer au fichier SConstruct dans la documentation de Godot 3.0.

Une fois que vous avez téléchargé le fichier SConstruct, placez-le dans le dossier de votre module GDNative à côté de godot-cpp, godot_headers et demo, puis exécutez :

scons platform=<platform>

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

Note

Ici, nous avons compilé à la fois godot-cpp et notre bibliothèque gdexample en tant que constructions(builds) de débogage. Pour des builds optimisés, vous devriez les compiler en utilisant le commutateur target=release.

Utilisation du module GDNative

Avant de revenir à Godot, nous devons créer deux autres fichiers dans demo/bin/. Les deux peuvent être créés à l'aide de l'éditeur Godot, mais il peut être plus rapide de les créer directement.

Le premier est un fichier qui permet à Godot de savoir quelles bibliothèques dynamiques doivent être chargées pour chaque plate-forme et s'appelle gdexample.gdnlib.

[general]

singleton=false
load_once=true
symbol_prefix="godot_"
reloadable=false

[entry]

X11.64="res://bin/x11/libgdexample.so"
Windows.64="res://bin/win64/libgdexample.dll"
OSX.64="res://bin/osx/libgdexample.dylib"

[dependencies]

X11.64=[]
Windows.64=[]
OSX.64=[]

Ce fichier contient une section general qui contrôle la façon dont le module est chargé. Il contient également une section de préfixe qui devrait être laissée sur godot_ pour le moment. Si vous changez cela, vous devrez renommer diverses fonctions qui sont utilisées comme points d'entrée. Ceci a été ajouté pour la plateforme iPhone car elle ne permet pas de déployer des bibliothèques dynamiques, pourtant les modules GDNative sont liés de manière statique.

La section entry est la partie importante : elle indique à Godot l'emplacement de la bibliothèque dynamique dans le système de fichiers du projet pour chaque plate-forme supportée. Elle permet également d'exporter juste ce fichier lorsque vous exportez le projet, ce qui signifie que le paquet de données ne contiendra pas de bibliothèques incompatibles avec la plate-forme cible.

Enfin, la section dependencies vous permet de nommer des bibliothèques dynamiques supplémentaires qui devraient également être incluses. Ceci est important lorsque votre plugin GDNative implémente la bibliothèque de quelqu'un d'autre et vous demande de fournir une bibliothèque dynamique tierce avec votre projet.

Si vous double-cliquez sur le fichier gdexample.gdnlib dans Godot, vous verrez qu'il y a beaucoup plus d'options à définir :

../../../_images/gdnative_library.png

Le deuxième fichier que nous devons créer est un fichier utilisé par chaque NativeScript que nous avons ajouté à notre plugin. Nous le nommerons gdexample.gdns pour notre gdexample NativeScript.

[gd_resource type="NativeScript" load_steps=2 format=2]

[ext_resource path="res://bin/gdexample.gdnlib" type="GDNativeLibrary" id=1]

[resource]

resource_name = "gdexample"
class_name = "GDExample"
library = ExtResource( 1 )

Il s'agit d'une ressource Godot standard ; vous pouvez la créer directement dans votre scène, mais l'enregistrer dans un fichier facilite grandement sa réutilisation dans d'autres endroits. Cette ressource pointe vers notre fichier gdnlib, afin que Godot puisse savoir quelle bibliothèque dynamique contient notre NativeScript. Elle définit également le class_name qui identifie le NativeScript de notre plugin que nous voulons utiliser.

Il est temps de revenir dans Godot. Nous chargeons la scène principale que nous avons créée au début et ajoutons maintenant un Sprite à notre scène :

../../../_images/gdnative_cpp_nodes.png

Nous allons assigner le logo Godot en texture à ce sprite, désactiver la propriété centered et glisser notre fichier gdexample.gdns sur la propriété script du sprite :

../../../_images/gdnative_cpp_sprite.png

Nous sommes prêts à lancer le projet :

../../../_images/gdnative_cpp_animated.gif

Ajouter des propriétés

GDScript vous permet d’ajouter des propriétés à votre script en utilisant le mot-clé export. En GDNative il faut enregistrer les propriétés et il y a deux manières de le faire. Vous pouvez soit lier directement à un membre, soit utiliser des fonctions d’accès en lecture (getter) et en écriture (setter).

Note

Il y a une troisième option : tout comme en GDScript vous pouvez implémenter directement les méthodes _get_property_list, _get et _set d’un objet, mais ça dépasse la portée de ce tutoriel.

Nous allons examiner les deux méthodes, en commençant par la liaison directe. Ajoutons une propriété qui nous permette de contrôler l’amplitude de notre vague.

Dans notre fichier gdexample.h, il faut seulement ajouter une variable membre de la manière suivante :

...
private:
    float time_passed;
    float amplitude;
...

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::_register_methods() {
    register_method("_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
}

void GDExample::_init() {
    // initialize any variables here
    time_passed = 0.0;
    amplitude = 10.0;
}

void GDExample::_process(float 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);
}

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.

Note

La propriété reloadable dans le ficher gdexample.gdnlib doit être définie à true pour que l’éditeur de Godot remarque automatiquement la propriété nouvellement ajoutée.

Toutefois, ce paramètre doit être utilisé avec précaution, notamment lorsque des classes tool sont utilisées, car l'éditeur peut contenir des objets auxquels sont attachées des instances de script gérées par une bibliothèque GDNative.

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 :

...
    float amplitude;
    float speed;
...
    void _process(float delta);
    void set_speed(float p_speed);
    float get_speed();
...

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::_register_methods() {
    register_method("_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
    register_property<GDExample, float>("speed", &GDExample::set_speed, &GDExample::get_speed, 1.0);
}

void GDExample::_init() {
    // initialize any variables here
    time_passed = 0.0;
    amplitude = 10.0;
    speed = 1.0;
}

void GDExample::_process(float 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(float p_speed) {
    speed = p_speed;
}

float GDExample::get_speed() {
    return speed;
}

Maintenant, lorsque le projet est compilé, nous verrons une autre propriété appelée speed. En changeant sa valeur, l'animation sera plus rapide ou plus lente.

Pour cet exemple, il n'y a pas d'avantage évident à utiliser un setter et un getter. Une bonne raison d'utiliser un setter serait de vouloir réagir à la modification de la variable. Si vous n’avez pas besoin de faire quelque chose de ce genre, il suffit de lier la variable.

Les méthodes d’accès deviennent bien plus utiles dans des situations plus complexes où il faut faire des choix en plus en fonction de l’état de vos objets.

Note

Pour simplifier, nous avons omis les paramètres optionnels dans l’appel de méthode register_property<class, type>. Ces paramètres sont rpc_mode, usage, hint et hint_string. Ils peuvent être utilisés pour configurer plus finement la manière dont les propriétés sont affichées et définies du côté de Godot.

Les compilateurs C++ modernes sont capables de deviner la classe et le type de variable, et vous permettent de laisser tomber la partie <GDExample, float> de la méthode register_property. Cela dit, nous n’avons pas eu que de bonnes expériences avec ça.

Signaux

Pour terminer, les signaux sont eux aussi complètement supportés dans GDNative. Pour qu’un module réagisse à un signal transmis par un autre objet, il faut appeler la méthode connect sur cet objet. Nous ne trouvons pas de bon exemple pour notre icône Godot qui bouge ; pour ça il faudrait montrer un exemple beaucoup plus complet.

Voici la syntaxe requise :

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

Remarquez qu’on ne peut appeler my_method que si on l’a au préalable enregistrée dans la méthode _register_methods.

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 :

...
    float time_passed;
    float time_emit;
    float 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.

Dans notre méthode _register_methods, nous devons déclarer notre signal. Cela se fait comme suit :

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
    register_property<GDExample, float>("speed", &GDExample::set_speed, &GDExample::get_speed, 1.0);

    register_signal<GDExample>((char *)"position_changed", "node", GODOT_VARIANT_TYPE_OBJECT, "new_pos", GODOT_VARIANT_TYPE_VECTOR2);
}

Ici, notre méthode register_signal peut être un appel unique prenant d'abord le nom du signal, puis ayant des paires de valeurs spécifiant le nom et le type de chaque paramètre que nous enverrons avec ce signal.

Ensuite, nous devons modifier notre méthode _process :

void GDExample::_process(float 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.

Une fois la bibliothèque GDNative compilée, nous pouvons aller dans Godot et sélectionner notre nœud de sprite. Dans le dock Nœud, nous pouvons trouver notre nouveau signal et le relier en appuyant sur le bouton Connect ou en double-cliquant sur le signal. Nous avons ajouté un script sur notre nœud principal et implémenté notre signal comme ceci :

extends Node

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

Chaque seconde, on envoie notre position à la console.

La suite

Ce qui précède était un simple exemple, mais nous espérons qu’il vous ait montré les bases. Vous pouvez partir de là pour créer des scripts complets afin de contrôler des nœuds dans Godot, en utilisant C++.

Pour modifier et recompiler le plugin alors que l'éditeur Godot reste ouvert, relancez le projet après la fin de la compilation de la bibliothèque.