Benutzerdefinierte Module in C++

Module

Godot ermöglicht den modularen Ausbau der Engine. Neue Module können erstellt und dann aktiviert bzw. deaktiviert werden. Dies ermöglicht das Hinzufügen neuer Enginefunktionen auf jeder Ebene ohne den Kern zu ändern, der zur Verwendung und Wiederverwendung in verschiedenen Modulen aufgeteilt werden kann.

Module befinden sich im Unterverzeichnis modules/ des Build-Systems. Standardmäßig sind Dutzende von Modulen aktiviert, z.B. GDScript (das ja nicht Teil der Basis-Engine ist), die Mono-Laufzeit, ein Modul für reguläre Ausdrücke und andere. Es können beliebig viele neue Module erstellt und kombiniert werden. Das SCons-Build-System kümmert sich transparent darum.

Wofür?

Es wird zwar empfohlen den größten Teil eines Spiels in Skripten zu schreiben (da dies eine enorme Zeitersparnis bedeutet), es ist jedoch durchaus möglich stattdessen C++ zu verwenden. Das Hinzufügen von C++ - Modulen kann in den folgenden Szenarien hilfreich sein:

  • Binden einer externen Bibliothek an Godot (wie PhysX, FMOD usw.).

  • Optimieren kritischer Teile eines Spiels.

  • Hinzufügen neuer Funktionen zur Engine oder zum Editor.

  • Portieren eines vorhandenen Spiels.

  • Schreiben eines ganz neuen Spiels in C++, weil Sie ohne C++ nicht leben können.

Neues Modul erstellen

Before creating a module, make sure to download the source code of Godot and compile it.

Um ein neues Modul zu erstellen müssen Sie zunächst ein Verzeichnis in modules/ erstellen. Wenn Sie das Modul separat warten möchten, können Sie ein anderes VCS in Module auschecken und verwenden.

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

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

Und dann die cpp Datei.

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

Dann muss die neue Klasse irgendwie registriert werden, sodass zwei weitere Dateien erstellt werden müssen:

register_types.h
register_types.cpp

Wichtig

Diese Dateien müssen sich im obersten Ordner Ihres Moduls befinden (neben Ihren Dateien SCsub und config.py), damit das Modul ordnungsgemäß registriert werden kann.

Diese Dateien sollten Folgendes enthalten:

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

Als nächstes müssen wir eine SCsub Datei erstellen, damit das Build-System dieses Modul kompiliert:

# SCsub

Import('env')

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

Bei mehreren Quellen können Sie jede Datei auch einzeln zu einer Python-Zeichenfolgenliste hinzufügen:

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

Dies erlaubt leistungsstarke Möglichkeiten mit Python, die Dateiliste mithilfe von Schleifen und logischen Anweisungen zu erstellen. Schauen Sie sich einige Module an, die standardmäßig mit Godot geliefert werden.

Um Include-Verzeichnisse hinzuzufügen, die der Compiler anzeigen kann, können Sie sie an die Pfade der Umgebung anhängen:

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

If you want to add custom compiler flags when building your module, you need to clone env first, so it won't add those flags to whole Godot build (which can cause errors). Example SCsub with custom flags:

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

Und schließlich, die Konfigurationsdatei für das Modul, ist dies ein einfaches Python-Skript, das den Namen config.py haben muss:

# config.py

def can_build(env, platform):
    return True

def configure(env):
    pass

Das Modul wird gefragt, ob es für die jeweilige Plattform in Ordnung ist, zu builden (in diesem Fall bedeutet True, dass es für jede Plattform erstellt wird).

Und das ist es, hoffentlich war es nicht zu komplex. Ihr Modul sollte folgendermaßen aussehen:

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

Sie können es dann komprimieren und das Modul mit allen anderen teilen. Beim Erstellen für jede Plattform (Anweisungen in den vorherigen Abschnitten) wird Ihr Modul einbezogen.

Bemerkung

In C++ - Modulen gibt es eine Parametergrenze von 5 für Dinge wie Unterklassen. Dies kann durch Einfügen der Header-Datei core/method_bind_ext.gen.inc auf 13 erhöht werden.

Modul verwenden

Sie können Ihr neu erstelltes Modul jetzt in jedem Skript verwenden:

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

Die Ausgabe wird 60 sein.

Siehe auch

Das vorherige Summator-Beispiel eignet sich hervorragend für kleine, benutzerdefinierte Module. Was ist jedoch, wenn Sie eine größere externe Bibliothek verwenden möchten? Weitere Informationen zum Binden an externe Bibliotheken finden Sie unter Verknüpfung mit externen Bibliotheken.

Warnung

Wenn auf Ihr Modul vom laufenden Projekt aus zugegriffen werden soll (nicht nur vom Editor), müssen Sie auch jede Exportvorlage, die Sie verwenden möchten, neu kompilieren und dann in jeder Exportvoreinstellung den Pfad zur benutzerdefinierten Vorlage angeben. Andernfalls werden beim Ausführen des Projekts Fehler angezeigt, da das Modul nicht in der Exportvorlage kompiliert ist. Weitere Informationen finden Sie auf den Seiten Compiling.

Ein Modul extern kompilieren

Beim Kompilieren eines Moduls werden die Quellen des Moduls direkt in das Verzeichnis modules/ der Engine verschoben. Dies ist zwar die einfachste Methode zum Kompilieren eines Moduls, es gibt jedoch mehrere Gründe, warum dies möglicherweise nicht praktikabel ist:

  1. Sie müssen Modulquellen jedes Mal manuell kopieren, wenn Sie die Engine mit oder ohne Modul kompilieren möchten, oder zusätzliche Schritte ausführen, um ein Modul während der Kompilierung mit einer Erstellungsoption ähnlich module_summator_enabled = no manuell zu deaktivieren. Das Erstellen symbolischer Links kann ebenfalls eine Lösung sein. Möglicherweise müssen Sie jedoch zusätzlich Betriebssystembeschränkungen überwinden, z. B. die Berechtigung für symbolische Links, wenn Sie dies über ein Skript tun.

  2. Abhängig davon, ob Sie mit dem Quellcode der Engine arbeiten müssen, ändern die direkt zu modules/ hinzugefügten Moduldateien den Arbeitsbaum so weit, dass sich die Verwendung einer VCS (wie git) als umständlich erweist Sie müssen sicherstellen, dass nur der enginebezogene Code durch Filtern von Änderungen committed wird.

Wenn Sie also der Meinung sind, dass die unabhängige Struktur benutzerdefinierter Module benötigt wird, nehmen Sie unser "Summator" -Modul und verschieben Sie es in das übergeordnete Verzeichnis der Engine:

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

Kompilieren Sie die Engine mit unserem Modul, indem Sie die Build-Option custom_modules bereitstellen, die eine durch Kommas getrennte Liste von Verzeichnispfaden akzeptiert, die benutzerdefinierte C++ - Module enthalten, ähnlich wie folgt:

scons custom_modules=../modules

Das Build-System erkennt alle Module im Verzeichnis ../modules und kompiliert sie entsprechend, einschließlich unseres "Summator" -Moduls.

Warnung

Jeder an custom_modules übergebene Pfad wird intern in einen absoluten Pfad konvertiert, um zwischen benutzerdefinierten und integrierten Modulen zu unterscheiden. Dies bedeutet, dass Dinge wie das Generieren der Moduldokumentation möglicherweise von einer bestimmten Pfadstruktur auf Ihrem Computer abhängen.

Siehe auch

:ref:``Einführung in das Buildsystem - Build-Option für benutzerdefinierte Module <doc_buildsystem_custom_modules>`.

Verbesserung des Build-Systems für die Entwicklung

Warnung

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

So far, we defined a clean SCsub that allows us to add the sources of our new module as part of the Godot binary.

This static approach is fine when we want to build a release version of our game, given we want all the modules in a single binary.

However, the trade-off is that every single change requires a full recompilation of the game. Even though SCons is able to detect and recompile only the file that was changed, finding such files and eventually linking the final binary takes a long time.

Die Lösung, um solche Kosten zu vermeiden, besteht darin, ein eigenes Modul als gemeinsam genutzte Bibliothek zu erstellen, das beim Starten der Binärdatei unseres Spiels dynamisch geladen wird.

# 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])

Einmal kompiliert, sollten wir ein bin Verzeichnis haben, das sowohl die godot* binär Datei als auch unseren libsummator*.so enthält. Da sich die .so jedoch nicht in einem Standardverzeichnis befindet (wie /usr/lib), müssen wir unserer Binärdatei helfen, sie zur Laufzeit mit der Umgebungsvariablen LD_LIBRARY_PATH zu finden:

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

Bemerkung

You have to export the environment variable. Otherwise, you won't be able to run your project from the editor.

On top of that, it would be nice to be able to select whether to compile our module as shared library (for development) or as a part of the Godot binary (for release). To do that we can define a custom flag to be passed to SCons using the ARGUMENT command:

# 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)

Standardmäßig erstellt der Befehl scons unser Modul als Teil von Godots Binärdatei und als gemeinsam genutzte Bibliothek, wenn summator_shared = yes übergeben wird.

Schließlich können Sie den Build sogar noch weiter beschleunigen, indem Sie Ihr freigegebenes Modul im Befehl SCons explizit als Ziel angeben:

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

Benutzerdefinierte Dokumentation schreiben

Das Schreiben von Dokumentation mag wie eine langweilige Aufgabe erscheinen, es wird jedoch dringend empfohlen, Ihr neu erstelltes Modul zu dokumentieren, damit Benutzer leichter davon profitieren können. Ganz zu schweigen davon, dass der Code, den Sie vor einem Jahr geschrieben haben, möglicherweise nicht mehr von dem Code zu unterscheiden ist, der von jemand anderem geschrieben wurde. Seien Sie also freundlich zu Ihrem zukünftigen Selbst!

Es gibt mehrere Schritte, um benutzerdefinierte Dokumente für das Modul einzurichten:

  1. Erstellen Sie ein neues Verzeichnis im Stammverzeichnis des Moduls. Der Verzeichnisname kann beliebig sein, aber wir werden in diesem Abschnitt den Namen doc_classes verwenden.

  2. Jetzt müssen wir config.py bearbeiten und das folgende Schnipsel hinzufügen:

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

Die Funktion get_doc_path() wird vom Build-System verwendet, um den Speicherort der Dokumente zu bestimmen. In diesem Fall befinden sie sich im Verzeichnis modules/summator/doc_classes. Wenn Sie dies nicht definieren, wird der Dokumentpfad für Ihr Modul auf das Hauptverzeichnis doc/classes zurückgesetzt.

Die Methode get_doc_classes() ist erforderlich, damit das Build-System weiß, welche registrierten Klassen zum Modul gehören. Sie müssen alle Ihre Klassen hier auflisten. Die Klassen, die Sie nicht auflisten, landen im Hauptverzeichnis doc/classes.

Tipp

Sie können Git verwenden, um zu überprüfen, ob Sie einige Ihrer Klassen verpasst haben, indem Sie die nicht verfolgten Dateien mit git status überprüfen. Zum Beispiel:

user@host:~/godot$ git status

Beispielausgabe:

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. Jetzt können wir die Dokumentation erstellen:

Wir können dies tun, indem wir Godots doctool ausführen, d.H. godot --doctool <Pfad>, wodurch die Engine-API-Referenz auf den angegebenen <Pfad> im XML-Format ausgegeben wird.

In unserem Fall verweisen wir auf das Stammverzeichnis des geklonten Repositorys. Sie können es auf einen anderen Ordner verweisen und einfach die benötigten Dateien kopieren.

Führen Sie den Befehl aus:

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

Wenn Sie nun zum Ordner godot/modules/summator/doc_classes gehen, werden Sie sehen, dass er eine Summator.xml-Datei oder andere Klassen enthält, auf die Sie in Ihren get_doc_classes Funktion verwiesen haben.

Bearbeiten Sie die Datei(en) gemäß der Anleitung Beitrag zur Klassenreferenz und kompilieren Sie die Engine neu.

Sobald der Kompilierungsprozess abgeschlossen ist, können Sie auf die Dokumente im integrierten Dokumentationssystem der Engine zugreifen.

Um die Dokumentation auf dem neuesten Stand zu halten, müssen Sie lediglich eine der XML-Dateien ändern und die Engine von nun an neu kompilieren.

Wenn Sie die API Ihres Moduls ändern, können Sie die Dokumente auch erneut extrahieren. Sie enthalten die zuvor hinzugefügten Elemente. Wenn Sie auf Ihren Godot-Ordner verweisen, stellen Sie sicher, dass Sie keine Arbeit verlieren, indem Sie ältere Dokumente aus einer älteren Engine extrahieren, die auf den neueren basiert.

Beachten Sie, dass möglicherweise ein Fehler auftritt, der dem folgenden ähnelt, wenn Sie keine Schreibzugriffsrechte für Ihren angegebenen <Pfad> haben:

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

Hinzufügen von benutzerdefinierten Editor-Symbolen

Ähnlich wie Sie eine in sich geschlossene Dokumentation innerhalb eines Moduls schreiben, können Sie auch eigene benutzerdefinierte Symbole erstellen, damit Klassen im Editor angezeigt werden.

Informationen zum eigentlichen Erstellen von Editor-Symbolen für die Integration in die Engine finden Sie zuerst unter Editor Icons.

Führen Sie die folgenden Schritte aus, nachdem Sie Ihre Symbole erstellt haben:

  1. Erstellen Sie ein neues Verzeichnis im Stammverzeichnis des Moduls mit dem Namen icons. Dies ist der Standardpfad für das Modul, um nach den Editorsymbolen des Moduls zu suchen.

  2. Verschieben Sie Ihre neu erstellten svg Symbole (optimiert oder nicht) in diesen Ordner.

  3. Kompilieren Sie die Engine neu und führen Sie den Editor aus. Jetzt werden die Symbole gegebenenfalls in der Benutzeroberfläche des Editors angezeigt.

Wenn Sie Ihre Symbole an einer anderen Stelle in Ihrem Modul speichern möchten, fügen Sie den folgenden Codeausschnitt zu config.py hinzu, um den Standardpfad zu überschreiben:

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

Zusammenfassend

Nicht vergessen:

  • Verwenden Sie das Makro GDCLASS für die Vererbung, damit Godot es einschließen kann

  • Verwenden Sie _bind_methods, um Ihre Funktionen an Skripte zu binden und sie als Rückrufe für Signale zu verwenden.

Aber das ist noch nicht alles, je nachdem, was Sie tun, werden Sie mit einigen (hoffentlich positiven) Überraschungen begrüßt.

  • Wenn Sie von Node (oder einem abgeleiteten Nodetyp wie Sprite) erben, wird Ihre neue Klasse im Editor im Vererbungsbaum im Dialogfeld "Knoten hinzufügen" angezeigt.

  • Wenn Sie von Resource erben, wird es in der Ressourcenliste angezeigt, und alle offengelegten Eigenschaften können beim Speichern/Laden serialisiert werden.

  • Mit derselben Logik können Sie den Editor und fast jeden Bereich der Engine erweitern.