Up to date

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

Modules personnalisées en C++

Modules

Godot permet d'étendre le moteur de façon modulaire. Les nouveaux modules peuvent être créés et être ensuite activés ou désactivés. Cela permet d'ajouter de nouvelles fonctionnalités au moteur à chaque niveau sans modifier le cœur du moteur , qui peuvent être découpées pour l’utilisation et la réutilisation dans d'autres modules.

Les modules sont situés dans le sous-répertoire modules/ du système de construction. Par défaut, des dizaines de modules sont activés, tels que GDScript (qui, oui, ne fait pas partie du moteur de base), le runtime Mono, un module d'expressions régulières, et d'autres. Il est possible de créer et de combiner autant de nouveaux modules que souhaité. Le système de construction SCons s'en chargera de manière transparente.

Pourquoi ?

Bien qu'il soit recommandé que la majeure partie d'un jeu soit écrite en scripting (car c'est un énorme gain de temps), il est parfaitement possible d'utiliser C++ à la place. L'ajout de modules C++ peut être utile dans les scénarios suivants :

  • Relier une librairie externe à Godot (comme PhysX, FMOD, etc).

  • Optimiser des parties critiques d'un jeu.

  • Ajouter de nouvelles fonctionnalités au moteur et/ou à l'éditeur.

  • Porting an existing game to Godot.

  • Écrire tout un jeu en C++ car vous ne pouvez pas vivre sans C++.

Créer un nouveau module

Avant de créer un module, assurez-vous de télécharger le code source de Godot et de le compiler.

Pour créer un nouveau module, la première étape est de créer un dossier dans modules/. Si vous voulez maintenir votre module séparément, vous pouvez mettre en place un système de gestion de versions dans modules.

The example module will be called "summator" (godot/modules/summator). Inside we will create a summator class:

/* summator.h */

#ifndef SUMMATOR_H
#define SUMMATOR_H

#include "core/object/ref_counted.h"

class Summator : public RefCounted {
    GDCLASS(Summator, RefCounted);

    int count;

protected:
    static void _bind_methods();

public:
    void add(int p_value);
    void reset();
    int get_total() const;

    Summator();
};

#endif // SUMMATOR_H

Et puis le fichier cpp.

/* summator.cpp */

#include "summator.h"

void Summator::add(int p_value) {
    count += p_value;
}

void Summator::reset() {
    count = 0;
}

int Summator::get_total() const {
    return count;
}

void Summator::_bind_methods() {
    ClassDB::bind_method(D_METHOD("add", "value"), &Summator::add);
    ClassDB::bind_method(D_METHOD("reset"), &Summator::reset);
    ClassDB::bind_method(D_METHOD("get_total"), &Summator::get_total);
}

Summator::Summator() {
    count = 0;
}

Ensuite, la nouvelle classe doit être enregistrée d'une manière ou d'une autre, deux fichiers supplémentaires doivent être créés :

register_types.h
register_types.cpp

Important

Ces fichiers doivent se trouver dans le dossier de haut niveau de votre module (à côté de vos fichiers SCsub et config.py) pour que le module soit correctement enregistré.

Ces fichiers doivent contenir les éléments suivants :

/* register_types.h */

#include "modules/register_module_types.h"

void initialize_summator_module(ModuleInitializationLevel p_level);
void uninitialize_summator_module(ModuleInitializationLevel p_level);
/* yes, the word in the middle must be the same as the module folder name */
/* register_types.cpp */

#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

void initialize_summator_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
            return;
    }
    ClassDB::register_class<Summator>();
}

void uninitialize_summator_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
            return;
    }
   // Nothing to do here in this example.
}

Ensuite, nous devons créer un fichier SCsub pour que le système de compilation compile ce module :

# SCsub

Import('env')

env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build

Avec des sources multiples, vous pouvez également ajouter chaque fichier individuellement à une liste de chaînes de caractère Python :

src_list = ["summator.cpp", "other.cpp", "etc.cpp"]
env.add_source_files(env.modules_sources, src_list)

Cela offre de puissantes possibilités en utilisant Python pour construire la liste des fichiers à l'aide de boucles et d'instructions logiques. Regardez certains modules qui sont livrés avec Godot par défaut pour des exemples.

Pour ajouter des répertoires d'include à consulter par le compilateur, vous pouvez l'ajouter aux chemins de l'environnement :

env.Append(CPPPATH=["mylib/include"]) # this is a relative path
env.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path

Si vous voulez ajouter des drapeaux de compilation personnalisés lors de la construction de votre module, vous devez d'abord cloner env, afin qu'il n'ajoute pas ces drapeaux à la construction de Godot (ce qui peut causer des erreurs). Exemple SCsub avec des drapeaux personnalisés :

# SCsub

Import('env')

module_env = env.Clone()
module_env.add_source_files(env.modules_sources, "*.cpp")
# Append CCFLAGS flags for both C and C++ code.
module_env.Append(CCFLAGS=['-O2'])
# If you need to, you can:
# - Append CFLAGS for C code only.
# - Append CXXFLAGS for C++ code only.

And finally, the configuration file for the module, this is a Python script that must be named config.py:

# config.py

def can_build(env, platform):
    return True

def configure(env):
    pass

Il est demandé au module s'il peut être construit pour la plate-forme spécifique (dans ce cas, True signifie qu'il sera construit pour chaque plate-forme).

Et c'est tout. J'espère que ce n'était pas trop complexe ! Votre module devrait ressembler à ceci :

godot/modules/summator/config.py
godot/modules/summator/summator.h
godot/modules/summator/summator.cpp
godot/modules/summator/register_types.h
godot/modules/summator/register_types.cpp
godot/modules/summator/SCsub

Vous pouvez ensuite le zipper et partager le module avec tous les autres. Lors de la construction de chaque plateforme (instructions dans les sections précédentes), votre module sera inclus.

Note

Il existe une limite de 5 paramètres dans les modules C++ pour des choses telles que les sous-classes. Cette limite peut être portée à 13 en incluant le fichier d'en-tête core/method_bind_ext.gen.inc.

Utilisation du module

Vous pouvez maintenant utiliser votre module nouvellement créé à partir de n'importe quel script :

var s = Summator.new()
s.add(10)
s.add(20)
s.add(30)
print(s.get_total())
s.reset()

La sortie sera 60.

Voir aussi

L'exemple précédent de Summator est parfait pour les petits modules personnalisés, mais que faire si vous souhaitez utiliser une bibliothèque externe plus importante ? Référez-vous à Liaison à des bibliothèques externes pour plus de détails sur la liaison avec les bibliothèques externes.

Avertissement

Si votre module est destiné à être accessible depuis le projet en cours (et pas seulement depuis l'éditeur), vous devez également recompiler chaque modèle d'exportation que vous prévoyez d'utiliser, puis spécifier le chemin d'accès au modèle personnalisé dans chaque préréglage d'exportation. Sinon, vous obtiendrez des erreurs lors de l'exécution du projet car le module n'est pas compilé dans le modèle d'exportation. Voir les pages Compilation pour plus d'informations.

Compilation d'un module en externe

La compilation d'un module implique de déplacer les sources du module directement sous le répertoire modules/ du moteur. Bien que ce soit la façon la plus simple de compiler un module, il y a plusieurs raisons pour lesquelles cela n'est pas pratique :

  1. Devoir copier manuellement les sources des modules chaque fois que vous voulez compiler le moteur avec ou sans le module, ou prendre des mesures supplémentaires nécessaires pour désactiver manuellement un module pendant la compilation avec une option de compilation similaire à module_summator_enabled=no. La création de liens symboliques peut également être une solution, mais vous devrez peut-être en outre surmonter des restrictions du système d'exploitation, comme le fait d'avoir besoin du privilège de lien symbolique si vous le faites par script.

  2. Selon que vous devez ou non travailler avec le code source du moteur, les fichiers de module ajoutés directement à modules/ modifient l'arborescence de travail au point où l'utilisation d'un VCS (comme git) s'avère lourde car vous devez vous assurer que seul le code lié au moteur est commit en filtrant les modifications.

Donc, si vous pensez que la structure indépendante des modules personnalisés est nécessaire, prenons notre module "summator" et déplaçons-le dans le répertoire parent du moteur :

mkdir ../modules
mv modules/summator ../modules

Compilez le moteur avec notre module en fournissant l'option de compilation custom_modules qui accepte une liste de chemins de répertoire séparés par des virgules contenant des modules C++ personnalisés, comme ci-dessous :

scons custom_modules=../modules

Le système de compilation doit détecter tous les modules du répertoire ../modules et les compiler en conséquence, y compris notre module "summator".

Avertissement

Tout chemin passé à custom_modules sera converti en un chemin absolu en interne afin de distinguer les modules personnalisés des modules intégrés. Cela signifie que des choses comme la génération de la documentation des modules peuvent dépendre d'une structure de chemin spécifique sur votre machine.

Customizing module types initialization

Modules can interact with other built-in engine classes during runtime and even affect the way core types are initialized. So far, we've been using register_summator_types as a way to bring in module classes to be available within the engine.

A crude order of the engine setup can be summarized as a list of the following type registration methods:

preregister_module_types();
preregister_server_types();
register_core_singletons();
register_server_types();
register_scene_types();
EditorNode::register_editor_types();
register_platform_apis();
register_module_types();
initialize_physics();
initialize_navigation_server();
register_server_singletons();
register_driver_types();
ScriptServer::init_languages();

Our Summator class is initialized during the register_module_types() call. Imagine that we need to satisfy some common module run-time dependency (like singletons), or allow us to override existing engine method callbacks before they can be assigned by the engine itself. In that case, we want to ensure that our module classes are registered before any other built-in type.

This is where we can define an optional preregister_summator_types() method which will be called before anything else during the preregister_module_types() engine setup stage.

We now need to add this method to register_types header and source files:

/* register_types.h */

#define MODULE_SUMMATOR_HAS_PREREGISTER
void preregister_summator_types();

void register_summator_types();
void unregister_summator_types();

Note

Unlike other register methods, we have to explicitly define MODULE_SUMMATOR_HAS_PREREGISTER to let the build system know what relevant method calls to include at compile time. The module's name has to be converted to uppercase as well.

/* register_types.cpp */

#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

void preregister_summator_types() {
    // Called before any other core types are registered.
    // Nothing to do here in this example.
}

void register_summator_types() {
    ClassDB::register_class<Summator>();
}

void unregister_summator_types() {
   // Nothing to do here in this example.
}

Améliorer le système de construction pour le développement

Avertissement

This shared library support is not designed to support distributing a module to other users without recompiling the engine. For that purpose, use a GDExtension instead.

Jusqu'à présent, nous avons défini un SCsub propre et simple qui nous permet d'ajouter les codes sources de notre nouveau module dans le programme binaire de Godot.

Cette approche statique est très bien quand nous voulons construire une version finale ("release") de notre jeu puisque nous voulons tous les modules dans un seul binaire.

Cependant, le compromis est que chaque changement signifie une recompilation complète du jeu. Même si SCons est capable de détecter et de recompiler uniquement les fichiers qui ont changé, trouver de tels fichiers et éventuellement relier le binaire final est une partie longue et coûteuse.

La solution pour éviter un tel coût est de construire notre module comme une bibliothèque partagée qui sera chargée dynamiquement au démarrage du binaire de notre jeu.

# SCsub

Import('env')

sources = [
    "register_types.cpp",
    "summator.cpp"
]

# First, create a custom env for the shared library.
module_env = env.Clone()

# Position-independent code is required for a shared library.
module_env.Append(CCFLAGS=['-fPIC'])

# Don't inject Godot's dependencies into our shared library.
module_env['LIBS'] = []

# Define the shared library. By default, it would be built in the module's
# folder, however it's better to output it into `bin` next to the
# Godot binary.
shared_lib = module_env.SharedLibrary(target='#bin/summator', source=sources)

# Finally, notify the main build environment it now has our shared library
# as a new dependency.

# LIBPATH and LIBS need to be set on the real "env" (not the clone)
# to link the specified libraries to the Godot executable.

env.Append(LIBPATH=['#bin'])

# SCons wants the name of the library with it custom suffixes
# (e.g. ".linuxbsd.tools.64") but without the final ".so".
shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
env.Append(LIBS=[shared_lib_shim])

Une fois compilé, nous devrions aboutir à un répertoire bin contenant à la fois le binaire godot* et notre libsummator*.so. Cependant, étant donné que le .so n'est pas dans un répertoire standard (comme /usr/lib), nous devons aider notre binaire à le trouver pendant l'exécution avec la variable d'environnement LD_LIBRARY_PATH :

export LD_LIBRARY_PATH="$PWD/bin/"
./bin/godot*

Note

Vous devez exporter avec export la variable d'environnement. Sinon, vous ne pourrez pas lancer votre projet depuis l'éditeur.

De plus, il serait bon de pouvoir choisir de compiler notre module comme bibliothèque partagée (pour le développement) ou comme partie du binaire Godot (pour la version finale). Pour ce faire, nous pouvons définir un indicateur personnalisé à passer à SCons en utilisant la commande ARGUMENT :

# SCsub

Import('env')

sources = [
    "register_types.cpp",
    "summator.cpp"
]

module_env = env.Clone()
module_env.Append(CCFLAGS=['-O2'])

if ARGUMENTS.get('summator_shared', 'no') == 'yes':
    # Shared lib compilation
    module_env.Append(CCFLAGS=['-fPIC'])
    module_env['LIBS'] = []
    shared_lib = module_env.SharedLibrary(target='#bin/summator', source=sources)
    shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
    env.Append(LIBS=[shared_lib_shim])
    env.Append(LIBPATH=['#bin'])
else:
    # Static compilation
    module_env.add_source_files(env.modules_sources, sources)

Maintenant, par défaut, la commande scons va construire notre module comme une partie du binaire de Godot et comme une bibliothèque partagée en passant summator_shared=yes.

Enfin, vous pouvez même accélérer la construction en spécifiant explicitement votre module partagé comme cible dans la commande scons :

scons summator_shared=yes platform=linuxbsd bin/libsummator.linuxbsd.tools.64.so

Rédaction de documentation personnalisée

Writing documentation may seem like a boring task, but it is highly recommended to document your newly created module to make it easier for users to benefit from it. Not to mention that the code you've written one year ago may become indistinguishable from the code that was written by someone else, so be kind to your future self!

Il y a plusieurs étapes pour mettre en place de la documentation personnalisée pour le module :

  1. Créez un nouveau répertoire à la racine du module. Le nom du répertoire peut être n'importe quoi, mais nous utiliserons le nom doc_classes tout au long de cette section.

  2. Maintenant, nous devons éditer config.py et ajouter l'extrait suivant :

    def get_doc_path():
        return "doc_classes"
    
    def get_doc_classes():
        return [
            "Summator",
        ]
    

La fonction get_doc_path() est utilisée par le système de compilation pour déterminer l'emplacement des documents. Dans ce cas, ils seront situés dans le répertoire modules/summator/doc_classes. Si vous ne le définissez pas, le chemin de la documentation de votre module reviendra au répertoire principal doc/classes.

La méthode get_doc_classes() est nécessaire pour que le système de compilation sache quelles classes déclarées appartiennent au module. Vous devez lister toutes vos classes ici. Les classes que vous ne listerez pas se retrouveront dans le répertoire principal doc/classes.

Astuce

Vous pouvez utiliser Git pour vérifier si vous avez manqué certaines de vos classes en vérifiant les fichiers non tracés avec git status. Par exemple :

user@host:~/godot$ git status

Exemple de sortie :

Untracked files:
    (use "git add <file>..." to include in what will be committed)

    doc/classes/MyClass2D.xml
    doc/classes/MyClass4D.xml
    doc/classes/MyClass5D.xml
    doc/classes/MyClass6D.xml
    ...
  1. Nous pouvons maintenant générer la documentation :

Nous pouvons le faire en exécutant le doctool de Godot, c'est-à-dire godot --doctool <path>, qui supprimera la référence API du moteur vers le <path> donné au format XML.

Dans notre cas, nous le ferons pointer vers la racine du dépôt cloné. Vous pouvez le faire pointer vers un autre dossier, et il suffit de copier les fichiers dont vous avez besoin.

Exécuter la commande :

user@host:~/godot$ ./bin/<godot_binary> --doctool .

Maintenant, si vous allez dans le dossier godot/modules/summator/doc_classes, vous verrez qu'il contient un fichier Summator.xml, ou toute autre classe, que vous avez référencé dans votre fonction get_doc_classes.

Edit the file(s) following Class reference primer and recompile the engine.

Une fois le processus de compilation terminé, les documents deviendront accessibles dans le système de documentation intégré au moteur.

Afin de maintenir la documentation à jour, il vous suffit de modifier un des fichiers XML et de recompiler le moteur.

Si vous modifiez l'API de votre module, vous pouvez également ré-extraire la documention, elles contiendront les éléments que vous avez précédemment ajoutés. Bien sûr, si vous le pointez vers votre dossier godot, assurez-vous de ne pas perdre de travail en extrayant une ancienne documentation d'une ancienne compilation du moteur sur les nouvelles.

Notez que si vous n'avez pas les droits d'accès en écriture à votre <path> fourni, vous pourriez rencontrer une erreur similaire à la suivante :

ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
   At: editor/doc/doc_data.cpp:956

Writing custom unit tests

It's possible to write self-contained unit tests as part of a C++ module. If you are not familiar with the unit testing process in Godot yet, please refer to Unit testing.

The procedure is the following:

  1. Create a new directory named tests/ under your module's root:

cd modules/summator
mkdir tests
cd tests
  1. Create a new test suite: test_summator.h. The header must be prefixed with test_ so that the build system can collect it and include it as part of the tests/test_main.cpp where the tests are run.

  2. Write some test cases. Here's an example:

// test_summator.h
#ifndef TEST_SUMMATOR_H
#define TEST_SUMMATOR_H

#include "tests/test_macros.h"

#include "modules/summator/summator.h"

namespace TestSummator {

TEST_CASE("[Modules][Summator] Adding numbers") {
    Ref<Summator> s = memnew(Summator);
    CHECK(s->get_total() == 0);

    s->add(10);
    CHECK(s->get_total() == 10);

    s->add(20);
    CHECK(s->get_total() == 30);

    s->add(30);
    CHECK(s->get_total() == 60);

    s->reset();
    CHECK(s->get_total() == 0);
}

} // namespace TestSummator

#endif // TEST_SUMMATOR_H
  1. Compile the engine with scons tests=yes, and run the tests with the following command:

./bin/<godot_binary> --test --source-file="*test_summator*" --success

You should see the passing assertions now.

Ajout d'icônes d'éditeur personnalisé

De la même manière que vous pouvez écrire une documentation autonome dans un module, vous pouvez également créer vos propres icônes personnalisées pour les classes afin qu'elles apparaissent dans l'éditeur.

Pour le processus de création des icônes d'éditeur à intégrer dans le moteur, veuillez d'abord vous référer à Icônes de l'éditeur.

Une fois que vous avez créé votre(vos) icône(s), procédez selon les étapes suivantes :

  1. Faire un nouveau répertoire dans la racine du module nommé icons. Ceci est le chemin par défaut que le moteur utilisera pour chercher les icônes du module pour l'éditeur.

  2. Déplacez vos icônes svg nouvellement créées (optimisé ou non) dans ce dossier.

  3. Recompilez le moteur et exécutez l'éditeur. Maintenant les icônes apparaîtront dans l'interface de l'éditeur où c'est nécessaire.

Si vous souhaitez stocker vos icônes ailleurs dans votre module, ajoutez l'extrait de code suivant à config.py pour remplacer le chemin par défaut :

def get_icons_path():
    return "path/to/icons"

En résumé

Se rappeler de :

  • Use GDCLASS macro for inheritance, so Godot can wrap it.

  • Use _bind_methods to bind your functions to scripting, and to allow them to work as callbacks for signals.

  • Avoid multiple inheritance for classes exposed to Godot, as GDCLASS doesn't support this. You can still use multiple inheritance in your own classes as long as they're not exposed to Godot's scripting API.

Mais ce n'est pas tout, en fonction de ce que vous faites, vous rencontrez quelques surprises (espérons-les positives).

  • If you inherit from Node (or any derived node type, such as Sprite2D), your new class will appear in the editor, in the inheritance tree in the "Add Node" dialog.

  • Si vous héritez de la Resource, elle apparaîtra dans la liste des ressources, et toutes les propriétés exposées peuvent être sérialisées lors de l'enregistrement/chargement.

  • Par cette même logique, vous pouvez étendre l'Éditeur et presque n'importe quelle zone du moteur.