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

Stellen Sie vor dem Erstellen eines Moduls sicher, dass Sie den Quellcode von Godot herunterladen und kompilieren. In der Dokumentation dazu finden Sie Anleitungen.

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.

Das Beispielmodul heißt "summator" und befindet sich im Godot-Quellbaum (C:\godot zeigt auf diese Godot-Quellen):

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

Im Inneren erstellen wir eine einfache Summator-Klasse:

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

Wenn Sie beim Erstellen Ihres Moduls benutzerdefinierte Compiler-Flags hinzufügen möchten, müssen Sie zuerst env klonen, damit diese Flags nicht zum gesamten Godot-Build hinzugefügt werden (was zu Fehlern führen kann). Beispiel SCsub mit benutzerdefinierten Flags:

# 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

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

Bisher haben wir einen sauberen und einfachen SCsub definiert mit dem wir die Quellen unseres neuen Moduls als Teil der Godot-Binärdatei hinzufügen können.

Dieser statische Ansatz ist in Ordnung, wenn wir eine Release-Version unseres Spiels erstellen möchten, in der alle Module in einer einzigen Binärdatei sind.

Der Kompromiss ist jedoch, dass jede einzelne Änderung eine vollständige Neukompilierung des Spiels bedeutet. Selbst wenn SCons nur die geänderten Dateien erkennen und neu kompilieren kann, ist es ein langer und kostspieliger Teil, solche Dateien zu finden und schließlich die endgültige Binärdatei zu verknüpfen.

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

Sie müssen die Umgebungsvariable exportieren, sonst können Sie Ihr Projekt nicht im Editor abspielen.

Darüber hinaus wäre es schön zu wählen, ob unser Modul als gemeinsam genutzte Bibliothek (für die Entwicklung) oder als Teil der Godot-Binärdatei (für die Veröffentlichung) kompiliert werden soll. Dazu können wir mit dem Befehl ARGUMENT ein benutzerdefiniertes Flag definieren, das an SCons übergeben werden soll:

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

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.