Exemple de GDNative en C

Introduction

Ce tutoriel vous présentera le strict minimum requis pour créer des modules GDNative. Ceci devrait être votre point de départ dans le monde de GDNative. Comprendre le contenu de ce tutoriel vous aidera à comprendre tout ce qui va suivre.

Avant de commencer, vous pouvez télécharger le code source de l'objet exemple décrit ci-dessous dans le dépôt GDNative-demos.

Cet exemple de projet contient également un fichier SConstruct qui rend la compilation un peu plus facile, mais dans ce tutoriel nous allons faire les choses à la main pour comprendre le processus.

GDNative peut être utilisé pour créer plusieurs types d'ajouts à Godot, en utilisant des interfaces telles que PluginScript ou ARVRInterfaceGDNative. Dans ce tutoriel, nous allons voir comment créer un module NativeScript. NativeScript vous permet d'écrire la logique en C ou C++ de la même manière que vous écrivez un fichier GDScript. Nous allons créer l'équivalent en C de ce GDScript :

extends Reference

var data

func _ready():
    data = "World from GDScript!"

func get_data():
    return data

Les prochains tutoriels se concentreront sur les autres types de modules GDNative et expliqueront quand et comment les utiliser.

Prérequis

Voici les prérequis nécessaires :

  1. Un exécutable Godot pour votre version cible.

  2. Un compilateur C. Sur Linux, installez gcc ou clang depuis votre gestionnaire de paquets. Sur macOS, vous pouvez installer Xcode depuis le Mac App Store. Sur Windows, vous pouvez utiliser Visual Studio 2015 (ou plus récent) ou MinGW-w64.

  3. Un clone Git du dépôt godot_headers : ce sont les en-têtes C de l'API publique de Godot exposée à GDNative.

Pour le dépôt des headers, il est suggéré de créer un dossier dédié pour cet exemple de projet GDNative. Ouvrez un terminal dans ce dossier et exécuter :

git clone https://github.com/godotengine/godot-headers.git --branch=3.4

Cette commande va télécharger les fichiers nécessaires dans le dossier.

Astuce

Si vous prévoyez d'utiliser Git pour votre projet GDNative, vous pouvez également ajouter godot-headers comme sous-module Git.

Note

The godot-headers repository has different branches. As Godot evolves, so does GDNative. While we try to preserve compatibility between version, you should always build your GDNative module against headers matching the Godot stable branch (e.g. 3.4) and ideally actual release (e.g. 3.4.4-stable) that you use. GDNative modules built against older versions of the Godot headers may work with newer versions of the engine, but not the other way around.

La branche master du dépôt godot_headers est en phase avec la branche master de Godot et de ce fait contient la classe GDNative et les définitions de structure qui fonctionneront avec le dernier code compilé en développement.

If you want to write a GDNative module for a stable version of Godot, look at the available Git tags (with git tags) for the one matching your engine version. In the godot-headers repository, such tags are prefixed with godot-, so you can e.g. checkout the godot-3.4.4-stable tag for use with Godot 3.4.4. In your cloned repository, you can do:

git checkout godot-3.4.4-stable

If a tag matching your stable release is missing for any reason, you can fall back to the matching stable branch (e.g. 3.4), which you would also check out with git checkout 3.4.

Si vous compilez Godot depuis les source avec des changements qui impactent GDNative, vous pouvez trouver la classe mise à jour et la définition de structure dans <godotsource>/modules/gdnative/include

Nos sources en C

Commençons par écrire notre code principal. Nous voulons finir par obtenir une structure de fichier qui ressemble à cela :

+ <your development folder>
  + godot-headers
    - <lots of files here>
  + simple
    + bin
      - libsimple.dll/so/dylib
      - libsimple.gdnlib
      - simple.gdns
    main.tscn
    project.godot
  + src
    - simple.c

Ouvrez Godot et créez un nouveau projet appelé "simple" à côté de votre clone Git godot_headers. Cela créera le dossier simple et le fichier project.godot. Ensuite, créez manuellement un dossier src à côté du dossier simple, et un sous-dossier bin dans le dossier simple.

Nous allons commencer par examiner le contenu de notre fichier simple.c. Maintenant, pour notre exemple ici, nous faisons un seul fichier source C sans en-tête pour garder les choses simples. Une fois que vous commencez à écrire des projets plus importants, il est conseillé de diviser votre projet en plusieurs fichiers. Cela n'entre cependant pas dans le cadre de ce tutoriel.

Nous allons examiner le code source petit à petit, de sorte que toutes les parties ci-dessous devraient être réunies dans un seul gros fichier. Chaque section sera expliquée au fur et à mesure que nous l'ajouterons.

#include <gdnative_api_struct.gen.h>

#include <string.h>

const godot_gdnative_core_api_struct *api = NULL;
const godot_gdnative_ext_nativescript_api_struct *nativescript_api = NULL;

Le code ci-dessus comprend l'en-tête de structure API GDNative et l'en-tête standard que nous utiliserons plus bas pour les opérations sur les chaînes de caractères. Il définit ensuite deux pointeurs vers deux structures différentes. GDNative supporte une large collection de fonctions pour rappeler l'exécutable principal de Godot. Pour que votre module puisse avoir accès à ces fonctions, GDNative fournit à votre application une structure contenant des pointeurs vers toutes ces fonctions.

Pour garder cette implémentation modulaire et facilement extensible, les fonctions de base sont disponibles directement via la structure API "core", mais les fonctions supplémentaires ont leurs propres "structures GDNative" qui sont accessibles via des extensions.

Dans notre exemple, nous accédons à l'une de ces extensions pour accéder aux fonctions spécifiquement nécessaires à NativeScript.

Un NativeScript se comporte comme n'importe quel autre script de Godot. Étant donné que l'API NativeScript est plutôt de bas niveau, elle nécessite que la bibliothèque spécifie beaucoup de choses plus verbalement que d'autres systèmes de script, tels que GDScript. Lorsqu'une instance NativeScript est créée, un constructeur donné par la bibliothèque est appelé. Lorsque cette instance sera détruite, le destructeur donné sera exécuté.

void *simple_constructor(godot_object *p_instance, void *p_method_data);
void simple_destructor(godot_object *p_instance, void *p_method_data, void *p_user_data);
godot_variant simple_get_data(godot_object *p_instance, void *p_method_data,
        void *p_user_data, int p_num_args, godot_variant **p_args);

Ce sont des déclarations avancées pour les fonctions que nous implémenterons pour notre objet. Un constructeur et un destructeur sont nécessaires. De plus, l'objet aura une seule méthode appelée get_data.

Ensuite, le premier des points d'entrée que Godot appellera lorsque notre bibliothèque dynamique sera chargée. Ces méthodes sont toutes préfixées par godot_ (vous pouvez le changer plus tard) suivi de leur nom. gdnative_init est une fonction qui initialise notre bibliothèque dynamique. Godot lui donnera un pointeur vers une structure qui contient divers morceaux d'informations que nous pouvons trouver utiles parmi lesquels les pointeurs vers nos structures API.

Pour toute structure API supplémentaire, nous devons passer en boucle dans notre tableau d'extensions et vérifier le type d'extension.

void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *p_options) {
    api = p_options->api_struct;

    // Now find our extensions.
    for (int i = 0; i < api->num_extensions; i++) {
        switch (api->extensions[i]->type) {
            case GDNATIVE_EXT_NATIVESCRIPT: {
                nativescript_api = (godot_gdnative_ext_nativescript_api_struct *)api->extensions[i];
            }; break;
            default: break;
        }
    }
}

La prochaine étape est gdnative_terminate qui est appelée avant que la bibliothèque ne soit libérée. Godot libérera la bibliothèque lorsqu'aucun objet ne l'utilisera plus. Ici, vous pouvez faire tout le nettoyage nécessaire. Pour notre exemple, nous allons simplement nettoyer nos pointeurs d'API.

void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *p_options) {
    api = NULL;
    nativescript_api = NULL;
}

Enfin, nous avons la fonction nativescript_init qui est la plus importante dont nous aurons besoin aujourd'hui. Cette fonction sera appelée par Godot dans le cadre du chargement d'une bibliothèque GDNative et communique en retour au moteur les objets que nous mettons à disposition.

void GDN_EXPORT godot_nativescript_init(void *p_handle) {
    godot_instance_create_func create = { NULL, NULL, NULL };
    create.create_func = &simple_constructor;

    godot_instance_destroy_func destroy = { NULL, NULL, NULL };
    destroy.destroy_func = &simple_destructor;

    nativescript_api->godot_nativescript_register_class(p_handle, "SIMPLE", "Reference",
            create, destroy);

    godot_instance_method get_data = { NULL, NULL, NULL };
    get_data.method = &simple_get_data;

    godot_method_attributes attributes = { GODOT_METHOD_RPC_MODE_DISABLED };

    nativescript_api->godot_nativescript_register_method(p_handle, "SIMPLE", "get_data",
            attributes, get_data);
}

Nous indiquons d'abord au moteur quelles classes sont implémentées en appelant nativescript_register_class. Le premier paramètre ici est le pointeur handle qui nous est donné. Le second est le nom de notre classe d'objets. Le troisième est le type d'objet de Godot dont nous 'héritons' ; ce n'est pas un véritable héritage mais c'est assez proche. Enfin, nos quatrième et cinquième paramètres sont des descriptions de notre constructeur et destructeur.

Nous informons ensuite Godot de nos méthodes (enfin notre seule méthode dans ce cas), en appelant nativescript_register_method pour chaque méthode de notre classe. Dans notre cas, c'est juste get_data. Notre premier paramètre est encore une fois notre pointeur handle. Le second est à nouveau le nom de la classe d'objets que nous enregistrons. Le troisième est le nom de notre fonction tel qu'elle sera connu de GDScript. Le quatrième est notre réglage d'attributs (voir l'énumération godot_method_rpc_mode dans godot_headers / nativescript / godot_nativescript.h pour les valeurs possibles). Le cinquième et dernier paramètre est une description de la fonction à appeler lorsque la méthode est appelée.

La structure de description instance_method contient le pointeur de fonction vers la fonction elle-même comme premier champ. Les deux autres champs de ces structures servent à spécifier les données utilisateur par méthode. Le second est le champ method_data qui est transmis à chaque appel de fonction comme argument p_method_data. Ceci est utile pour réutiliser une fonction pour différentes méthodes sur éventuellement plusieurs classes de script différentes. Si la valeur method_data est un pointeur vers une mémoire qui doit être libérée, le troisième champ free_func peut contenir un pointeur vers une fonction qui libérera cette mémoire. Cette fonction libérer est appelée lorsque le script lui-même (pas l'instance !) est libéré (donc généralement au moment de la libération de la bibliothèque).

Maintenant, il est temps de commencer à travailler sur les fonctions de notre objet. Tout d'abord, nous définissons une structure que nous utilisons pour stocker les données des membres d'une instance de notre classe GDNative.

typedef struct user_data_struct {
    char data[256];
} user_data_struct;

Et puis, nous définissons notre constructeur. Tout ce que nous faisons dans notre constructeur est d'allouer de la mémoire pour notre structure et de la remplir avec des données. Notez que nous utilisons les fonctions de mémoire de Godot pour que la mémoire soit suivie, puis nous renvoyons le pointeur vers notre nouvelle structure. Ce pointeur agira comme notre identifiant d'instance au cas où plusieurs objets seraient instanciés.

Ce pointeur sera passé à l'une de nos fonctions liées à notre objet sous la forme d'un paramètre appelé p_user_data, et peut être utilisé à la fois pour identifier notre instance et pour accéder aux données de ses membres.

void *simple_constructor(godot_object *p_instance, void *p_method_data) {
    user_data_struct *user_data = api->godot_alloc(sizeof(user_data_struct));
    strcpy(user_data->data, "World from GDNative!");

    return user_data;
}

Notre destructeur est appelé lorsque Godot a fini avec notre objet et que nous libérons les données des membres de nos instances.

void simple_destructor(godot_object *p_instance, void *p_method_data, void *p_user_data) {
    api->godot_free(p_user_data);
}

Et enfin, nous implémentons notre fonction get_data. Les données sont toujours envoyées et renvoyées sous forme de variants. Afin de renvoyer nos données, qui sont une string, nous devons d'abord convertir notre string C en un objet string Godot, puis copier cet objet string dans le variant que nous renvoyons.

godot_variant simple_get_data(godot_object *p_instance, void *p_method_data,
        void *p_user_data, int p_num_args, godot_variant **p_args) {
    godot_string data;
    godot_variant ret;
    user_data_struct *user_data = (user_data_struct *)p_user_data;

    api->godot_string_new(&data);
    api->godot_string_parse_utf8(&data, user_data->data);
    api->godot_variant_new_string(&ret, &data);
    api->godot_string_destroy(&data);

    return ret;
}

Les Strings sont allouées en tas dans Godot, ils ont donc un destructeur qui libère la mémoire. Les destructeurs sont nommés godot_TYPENAME_destroy. Lorsqu'un variant est créé avec un String, il référence le String. Cela signifie que la chaîne d'origine peut être "détruite" pour diminuer le nombre de références. Si cela ne se produit pas, la mémoire du String fuira car le décompte ne sera jamais nul et la mémoire ne sera jamais désallouée. Le variant retournée est automatiquement détruit par Godot.

Note

Dans les opérations plus complexes, il peut être difficile de savoir quelle valeur doit être désallouée et quelle valeur ne doit pas l'être. En règle générale : appelez godot_TYPENAME_destroy quand un destructeur C++ serait appelé à la place. Le destructeur de String serait appelé en C++ après la création du Variant, il est donc nécessaire de faire de même en C.

Le variant que nous renvoyons est détruit automatiquement par Godot.

Et c'est tout le code source de notre module.

Compilation

Nous devons maintenant compiler notre code source. Comme mentionné, notre exemple de projet sur GitHub contient une configuration SCons qui fait tout le travail pour vous, mais pour notre tutoriel ici, nous allons appeler directement les compilateurs.

En supposant que vous vous en tenez à la structure de dossiers suggérée ci-dessus, il est préférable d'ouvrir une session de terminal dans le dossier src et d'exécuter les commandes à partir de là. Assurez-vous de créer le dossier bin avant de continuer.

Sur Linux :

gcc -std=c11 -fPIC -c -I../godot-headers simple.c -o simple.o
gcc -rdynamic -shared simple.o -o ../simple/bin/libsimple.so

Sur macOS :

clang -std=c11 -fPIC -c -I../godot-headers simple.c -o simple.os
clang -dynamiclib simple.os -o ../simple/bin/libsimple.dylib

Sur Windows :

cl /Fosimple.obj /c simple.c /nologo -EHsc -DNDEBUG /MD /I. /I..\godot-headers
link /nologo /dll /out:..\simple\bin\libsimple.dll /implib:..\simple\bin\libsimple.lib simple.obj

Note

Sur la version Windows, vous vous retrouvez également avec une bibliothèque libsimple.lib. Il s'agit d'une bibliothèque que vous pouvez compiler dans un projet pour fournir l'accès à DLL. Nous l'obtenons en tant que sous-produit et nous n'en avons pas besoin :) Lors de l'exportation de votre jeu pour publication, ce fichier sera ignoré.

Création du fichier GDNativeLibrary (.gdnlib)

Avec notre module compilé, nous devons maintenant créer une ressource correspondante GDNativeLibrary avec l'extension .gdnlib que nous plaçons à côté de nos bibliothèques dynamiques. Ce fichier indique à Godot quelles bibliothèques dynamiques font partie de notre module et doivent être chargées par plate-forme.

Nous pouvons utiliser Godot pour générer ce fichier, alors ouvrez le projet "simple" dans l'éditeur.

Commencez par cliquer sur le bouton pour créer une ressource dans l'inspecteur :

../../../_images/new_resource.gif

Et sélectionnez GDNativeLibrary :

../../../_images/gdnativelibrary_resource.png

Vous devriez voir apparaître un éditeur contextuel dans le panneau inférieur. Utilisez le bouton "Développer le panneau inférieur" en bas à droite pour le développer à pleine hauteur :

../../../_images/gdnativelibrary_editor.png

Propriétés générales

Dans l'inspecteur, vous avez plusieurs propriétés pour contrôler le chargement de la bibliothèque.

Si Load Once est activé, notre bibliothèque est chargé une seule fois et chaque script qui utilise notre bibliothèque utilisera les mêmes données. Toute variable que vous définissez globalement sera accessible depuis n'importe quelle instance de votre objet que vous créez. Si Load Once est désactivé, une nouvelle copie de la bibliothèque est chargée en mémoire chaque fois qu'un script accède à la bibliothèque.

Si Singleton est activé, notre bibliothèque est automatiquement chargée et une fonction nommée godot_gdnative_singleton est appelée. Laissons cela pour un autre tutoriel.

Le Symbol Prefix est un préfixe pour nos fonctions core, comme par exemple godot_ dans godot_nativescript_init vu plus tôt. Si vous utilisez plusieurs bibliothèques GDNative que vous souhaitez lier statiquement, vous devrez utiliser des préfixes différents. Il s'agit encore une fois d'un sujet à approfondir dans un tutoriel séparé, il n'est nécessaire pour le moment que pour le déploiement sur iOS car cette plateforme n'aime pas les bibliothèques dynamiques.

Reloadable définit si la bibliothèque doit être rechargée lorsque l'éditeur perd et gagne le focus, typiquement pour récupérer des symboles nouveaux ou modifiés par toute modification externe de la bibliothèque.

Bibliothèques de plate-forme

L'éditeur de GDNativeLibrary vous permet de configurer deux choses pour chaque plate-forme et architecture que vous souhaitez supporter.

La colonne Bibliothèque dynamique (section entry dans le fichier sauvegardé) nous indique pour chaque combinaison de pate-forme et fonctionnalité, la bibliothèque dynamique devant être chargée. Cela informe également l'exportateur de quels fichier doivent être exportés lors de l'exportation vers une plate-forme spécifique.

La colonne Dépendances (ou la section dependencies) indique à Godot quels autres fichiers doivent être exportés pour chaque plate-forme pour que notre bibliothèque fonctionne. Supposons que votre module GDNative utilise une autre DLL pour implémenter des fonctionnalités à partir d'une bibliothèque tierce, c'est ici que vous devez lister cette DLL.

Pour notre exemple, nous avons uniquement construit des bibliothèques pour Linux, macOS et/ou Windows, afin que vous puissiez les lier dans les champs adaptés en cliquant sur le bouton dossier. Si vous avec construit ces trois bibliothèques, vous devriez avoir quelque chose comme ceci :

../../../_images/gdnativelibrary_editor_complete.png

Sauvegarder la ressource

Nous pouvons alors sauvegarder notre ressource GDNativeLibrary sous le nom de bin/libsimple.gdnlib avec le bouton Sauvegarder dans l'inspecteur :

../../../_images/gdnativelibrary_save.png

Le fichier est enregistré dans un format texte et doit avoir un contenu similaire à celui-ci :

[general]

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

[entry]

OSX.64="res://bin/libsimple.dylib"
OSX.32="res://bin/libsimple.dylib"
Windows.64="res://bin/libsimple.dll"
X11.64="res://bin/libsimple.so"

[dependencies]

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

Création du fichier NativeScript (.gdns)

Avec notre fichier .gdnlib nous avons dit à Godot comment charger notre bibliothèque, maintenant nous devons lui parler de notre classe d'objets "SIMPLE". Pour ce faire, nous créons un fichier de ressources NativeScript avec l'extension .gdns.

Comme pour la ressource GDNativeLibrary, cliquez sur le bouton pour créer une nouvelle ressource dans l'inspecteur et sélectionnez NativeScript :

../../../_images/nativescript_resource.png

L'inspecteur nous montrera quelques propriétés que nous devons remplir. Comme Class Name nous entrons "SIMPLE" qui est le nom de la classe d'objet que nous avons déclaré dans notre source C en appelant godot_nativescript_register_class. Nous devons également sélectionner notre fichier .gdnlib en cliquant sur Library et en sélectionnant Load :

../../../_images/nativescript_library.png

Note

Le Class Name doit avoir la même orthographe que celle donnée dans godot_nativescript_init lors de l'enregistrement de la classe.

Enfin, cliquez sur l'icône de sauvegarde et enregistrez le fichier sous le nom bin/simple.gdns :

../../../_images/save_gdns.gif

Il est maintenant temps de construire notre scène. Ajoutez un nœud de contrôle à votre scène comme racine et appelez le main. Ensuite, ajoutez un Bouton et un Label comme nœuds enfants. Placez-les à un endroit agréable sur l'écran et donnez un nom à votre bouton.

../../../_images/c_main_scene_layout.png

Sélectionnez le nœud de contrôle et attachez-y un script :

../../../_images/add_main_script.gif

Reliez ensuite le signal pressed du bouton à votre script :

../../../_images/connect_button_signal.gif

N'oubliez pas de sauvegarder votre scène, appelez la main.tscn.

Nous pouvons maintenant mettre en œuvre notre code main.gd :

extends Control

# load the Simple library
onready var data = preload("res://bin/simple.gdns").new()

func _on_Button_pressed():
    $Label.text = "Data = " + data.get_data()

Après tout cela, notre projet devrait fonctionner. La première fois que vous le lancez, Godot vous demande quelle est votre scène principale et vous sélectionnez votre fichier main.tscn et presto :

../../../_images/c_sample_result.png