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.
  • Porter un jeu déjà existant.
  • É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, assurer vous d'abord de récupérer le code source de Godot et de le compiler. Il y a des tutoriels dans la documentation à ce sujet.

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.

Le module d'exemple sera appelé "summator", et est placé à l'intérieur de l'arbre des sources Godot (C:\godot fait référence à l'endroit où les sources Godot sont situées) :

C:\godot> cd modules
C:\godot\modules> mkdir summator
C:\godot\modules> cd summator
C:\godot\modules\summator>

À l'intérieur, nous allons créer une classe simple de summator :

/* summator.h */

#ifndef SUMMATOR_H
#define SUMMATOR_H

#include "core/reference.h"

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

    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 */

void register_summator_types();
void unregister_summator_types();
/* yes, the word in the middle must be the same as the module folder name */
/* register_types.cpp */

#include "register_types.h"

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

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

void unregister_summator_types() {
   // 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")
module_env.Append(CCFLAGS=['-O2']) # Flags for C and C++ code
module_env.Append(CXXFLAGS=['-std=c++11']) # Flags for C++ code only

Et enfin, le fichier de configuration du module, c'est un simple script python qui doit être nommé 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.

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

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

Cette approche statique est très bien quand nous voulons construire une version release de notre jeu étant donné que 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. ".x11.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 export la variable d'environnement, sinon vous ne pourrez pas jouer 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 release). Pour ce faire, nous pouvons définir un drapeau 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'])
module_env.Append(CXXFLAGS=['-std=c++11'])

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=x11 bin/libsummator.x11.tools.64.so

Rédaction de documentation personnalisée

La rédaction de documentation peut sembler une tâche ennuyeuse, mais il est fortement recommandé de documenter votre nouveau module afin de permettre aux utilisateurs de l'utiliser plus facilement. Sans compter que le code que vous avez écrit il y a un an peut devenir indiscernable du code qui a été écrit par quelqu'un d'autre, alors soyez gentil avec votre futur vous !

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$ ./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.

Editez le(s) fichier(s) suivant(s) Contributing to the class reference et recompilez le moteur.

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

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 :

  • Utiliser la macro GDCLASS pour l'héritage, pour que Godot puisse l'envelopper
  • Utilisez _bind_methods pour lier vos fonctions au scripting, et pour leur permettre de fonctionner comme des rappels pour les signaux.

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

  • Si vous héritez depuis Node (ou d'un type de nœud dérivé, tel que Sprite), votre nouvelle classe apparaîtra dans l'éditeur, dans l'arbre d'héritage dans la boîte de dialogue "Ajouter Nœud".
  • 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.