Пример C++ с GDExtension

Введение

Привязки C++ для GDExtension построены поверх C API GDExtension и предоставляют более удобный способ "расширять" узлы и другие встроенные классы Godot, используя C++. Эта новая система позволяет расширять Godot почти на том же уровне, что и статически скомпонованные модули C++.

Примеры можно загрузить из папки test в репозитории godot-cpp на GitHub.

Настройка проекта

Вам необходимо выполнить несколько условий:

  • Исполняемый файл (.exe) Godot 4,

  • C++ компилятор,

  • SCons как инструмент сборки,

  • копия godot-cpp репозитория.

См. также разделы Настройка IDE и Компиляция, поскольку инструменты сборки идентичны тем, которые используются для компиляции Godot из исходного кода.

Вы можете загрузить godot-cpp репозиторий из GitHub, или позволить Git сделать это за вас. Обратите внимание, что в этом репозитории есть разные ветки для разных версий Godot. GDExtensions не будет работать в старых версиях Godot (Только версии 4 и выше) и наоборот, поэтому обязательно скачивайте правильную ветку.

Примечание

Чтобы использовать GDExtension, вам потребуется ветка godot-cpp, соответствующая версии Godot, с которой вы работаете. Например, если вы используете Godot 4.1, применяйте ветку 4.1. В этом руководстве мы используем обозначение 4.x — замените его на актуальную для вас версию Godot.

Master - ветка разработки, которая обновляется регулярно, для работы с веткой master Godot.

Предупреждение

Наша долгосрочная цель — обеспечить совместимость GDExtension, созданных для более ранних версий Godot, с более новыми минорными версиями, но не наоборот. Например, расширение, созданное для Godot 4.1, должно корректно работать в Godot 4.2, но расширение для Godot 4.2 не будет работать в Godot 4.1.

However, GDExtension is currently experimental, which means that we may break compatibility in order to fix major bugs or include critical features. For example, GDExtensions created for Godot 4.0 aren't compatible with Godot 4.1 (see Обновление ваших GDExtension на 4.1).

Если вы используете Git для управления версиями проекта, рекомендуется добавить его как подмодуль Git:

mkdir gdextension_cpp_example
cd gdextension_cpp_example
git init
git submodule add -b 4.x https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init

Или вы можете клонировать его в папку проекта:

mkdir gdextension_cpp_example
cd gdextension_cpp_example
git clone -b 4.x https://github.com/godotengine/godot-cpp

Примечание

Если вы решили загрузить репозиторий, или клонировать его в свою папку, убедитесь, что структура вашей папки аналогична той, что показана здесь. Большая часть кода, что мы покажем здесь, подразумевает такую структуру.

Если вы клонировали пример по ссылке, указанной во введении, подмодули не инициализируются автоматически. Вам будет нужно выполнить эти команды:

cd gdextension_cpp_example
git submodule update --init

Так вы инициализируете репозиторий в вашей папке проекта.

Сборка привязок C++

Теперь, когда вы загрузили все, что нужно, настало время собрать привязки C++.

Репозиторий содержит копию метаданных для текущего релиза Godot, но если вам нужно собрать эти привязки для более новой версии Godot, вызовите исполняемый файл Godot:

godot --dump-extension-api

Результирующий файл extension_api.json будет создан в директории исполняемого файла. Скопируйте его в папку проекта и добавьте custom_api_file=<ПУТЬ_К_ФАЙЛУ> в команду scons ниже.

Чтобы сгенерировать и скомпилировать привязки, используйте эту команду (замените <платформа> на windows, linux или macos в зависимости от вашей ОС):

Процесс сборки автоматически определяет количество потоков CPU для параллельной сборки. Чтобы указать количество потоков CPU, добавьте -jN в конце командной строки SCons, где N — количество используемых потоков CPU.

cd godot-cpp
scons platform=<platform> custom_api_file=<PATH_TO_FILE>
cd ..

Этот шаг займёт некоторое время. По завершении у вас должны быть статические библиотеки, которые можно скомпилировать в ваш проект, хранящиеся в godot-cpp/bin/.

Примечание

Вам может потребоваться добавить bits=64 к команде на Windows или Linux.

Создание простого плагина

Теперь пришло время собрать настоящий плагин. Мы начнём с создания пустого проекта Godot, в который поместим несколько файлов.

Откройте Godot и создайте новый проект. В этом примере мы разместим его в папке demo внутри структуры папок нашего GDExtension.

В нашем демо-проекте мы создадим сцену, содержащую узел с именем "Main", и сохраним её как main.tscn. Мы вернёмся к этому позже.

Вернувшись в корневую папку модуля GDExtension, мы также создадим в нем папку src, в которую поместим наши исходные файлы.

Теперь у вас должны быть папки demo, godot-cpp и src в вашем модуле GDExtension.

Ваша структура папок теперь должна выглядеть так:

gdextension_cpp_example/
|
+--demo/                  # game example/demo to test the extension
|
+--godot-cpp/             # C++ bindings
|
+--src/                   # source code of the extension we are building

В папке src мы начнём с создания заголовочного файла для узла GDExtension, который мы будем создавать. Мы назовём его gdexample.h:

gdextension_cpp_example/src/gdexample.h
#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H

#include <godot_cpp/classes/sprite2d.hpp>

namespace godot {

class GDExample : public Sprite2D {
    GDCLASS(GDExample, Sprite2D)

private:
    double time_passed;

protected:
    static void _bind_methods();

public:
    GDExample();
    ~GDExample();

    void _process(double delta) override;
};

}

#endif

Есть несколько примечательных моментов в вышеприведённом коде. Мы включаем sprite2d.hpp, который содержит привязки к классу Sprite2D. Мы будем расширять этот класс в нашем модуле.

Мы используем пространство имён godot, так как всё в GDExtension определено в этом пространстве имён.

Затем у нас есть определение нашего класса, который наследуется от Sprite2D через класс-контейнер. Позже мы увидим несколько побочных эффектов этого. Макрос GDCLASS настраивает для нас несколько внутренних вещей.

После этого мы объявляем единственную переменную с именем time_passed.

В следующем блоке мы определяем наши методы: у нас определены конструктор и деструктор, но есть две другие функции, которые могут показаться знакомыми, и один новый метод.

Первая - _bind_methods, статическая функция, которую Godot вызывает, чтобы определить, какие методы можно вызывать и какие свойства доступны. Вторая - наша функция _process, которая будет работать точно так же, как и знакомая вам функция _process в GDScript.

Давайте реализуем наши функции, создав файл gdexample.cpp:

gdextension_cpp_example/src/gdexample.cpp
#include "gdexample.h"
#include <godot_cpp/core/class_db.hpp>

using namespace godot;

void GDExample::_bind_methods() {
}

GDExample::GDExample() {
    // Initialize any variables here.
    time_passed = 0.0;
}

GDExample::~GDExample() {
    // Add your cleanup here.
}

void GDExample::_process(double delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(10.0 + (10.0 * sin(time_passed * 2.0)), 10.0 + (10.0 * cos(time_passed * 1.5)));

    set_position(new_position);
}

Всё должно быть понятно. Мы реализуем каждый метод нашего класса, который определили в заголовочном файле.

Обратите внимание на нашу функцию _process, которая отслеживает прошедшее время и вычисляет новую позицию для спрайта с использованием функций синуса и косинуса.

Нам нужен ещё один файл C++; мы назовём его register_types.cpp. Наш плагин GDExtension может содержать несколько классов, каждый со своим заголовочным и исходным файлом, как мы реализовали GDExample выше. Сейчас нам нужен небольшой фрагмент кода, который сообщит Godot обо всех классах в нашем плагине GDExtension.

gdextension_cpp_example/src/register_types.cpp
#include "register_types.h"

#include "gdexample.h"

#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>

using namespace godot;

void initialize_example_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }

    GDREGISTER_RUNTIME_CLASS(GDExample);
}

void uninitialize_example_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
}

extern "C" {
// Initialization.
GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
    godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);

    init_obj.register_initializer(initialize_example_module);
    init_obj.register_terminator(uninitialize_example_module);
    init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);

    return init_obj.init();
}
}

Функции initialize_example_module и uninitialize_example_module вызываются соответственно при загрузке и выгрузке нашего плагина в Godot. Здесь мы просто проходим по функциям в нашем модуле привязок, чтобы инициализировать их, но в зависимости от ваших потребностей может потребоваться настроить больше вещей. Мы вызываем макрос GDREGISTER_RUNTIME_CLASS для каждого класса в нашей библиотеке. Это позволит им работать только в игре, как и по умолчанию в GDScript.

Важная функция - третья, называемая example_library_init. Сначала мы вызываем функцию в нашей библиотеке привязок, которая создаёт объект инициализации. Этот объект регистрирует функции инициализации и завершения работы GDExtension. Кроме того, он устанавливает уровень инициализации (ядро, серверы, сцена, редактор, уровень).

Наконец, нам нужен заголовочный файл для register_types.cpp под названием register_types.h.

gdextension_cpp_example/src/register_types.h
#ifndef GDEXAMPLE_REGISTER_TYPES_H
#define GDEXAMPLE_REGISTER_TYPES_H

#include <godot_cpp/core/class_db.hpp>

using namespace godot;

void initialize_example_module(ModuleInitializationLevel p_level);
void uninitialize_example_module(ModuleInitializationLevel p_level);

#endif // GDEXAMPLE_REGISTER_TYPES_H

Компиляция плагина

Для компиляции проекта нам нужно определить, как SCons должен его компилировать, используя файл SConstruct, который ссылается на файл в godot-cpp. Написание этого файла с нуля выходит за рамки данного руководства, но вы можете скачать подготовленный нами файл SConstruct. В последующих руководствах мы рассмотрим более настраиваемый и подробный пример использования этих файлов сборки.

Примечание

Этот файл SConstruct был написан для использования с последней версией godot-cpp master. Возможно, вам потребуется внести небольшие изменения при использовании со старыми версиями или обратиться к файлу SConstruct в документации Godot 4.x.

После загрузки файла SConstruct поместите его в структуру папок GDExtension рядом с godot-cpp, src и demo, затем выполните:

scons platform=<platform>

Теперь вы должны найти модуль в demo/bin/<platform>.

При сборке для iOS упакуйте модуль как статический .xcframework. Для этого можно использовать следующие команды:

# compile simulator and device modules
scons arch=universal ios_simulator=yes platform=ios target=<target>
scons arch=arm64 ios_simulator=no platform=ios target=<target>

# assemble xcframeworks
xcodebuild -create-xcframework -library demo/bin/libgdexample.ios.<target>.a -library demo/bin/libgdexample.ios.<target>.simulator.a -output demo/bin/libgdexample.ios.<target>.xcframework
xcodebuild -create-xcframework -library godot-cpp/bin/libgodot-cpp.ios.<target>.arm64.a -library godot-cpp/bin/libgodot-cpp.ios.<target>.universal.simulator.a  -output demo/bin/libgodot-cpp.ios.<target>.xcframework

Примечание

Здесь мы скомпилировали как godot-cpp, так и нашу библиотеку gdexample в режиме отладки. Для оптимизированных сборок следует использовать переключатель target=template_release.

Использование модуля GDExtension

Прежде чем вернуться в Godot, нам нужно создать ещё один файл в demo/bin/.

Этот файл сообщает Godot, какие динамические библиотеки следует загружать для каждой платформы и какова входная функция модуля. Он называется gdexample.gdextension.

[configuration]

entry_symbol = "example_library_init"
compatibility_minimum = "4.1"
reloadable = true

[libraries]

macos.debug = "res://bin/libgdexample.macos.template_debug.framework"
macos.release = "res://bin/libgdexample.macos.template_release.framework"
ios.debug = "res://bin/libgdexample.ios.template_debug.xcframework"
ios.release = "res://bin/libgdexample.ios.template_release.xcframework"
windows.debug.x86_32 = "res://bin/libgdexample.windows.template_debug.x86_32.dll"
windows.release.x86_32 = "res://bin/libgdexample.windows.template_release.x86_32.dll"
windows.debug.x86_64 = "res://bin/libgdexample.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://bin/libgdexample.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "res://bin/libgdexample.linux.template_debug.x86_64.so"
linux.release.x86_64 = "res://bin/libgdexample.linux.template_release.x86_64.so"
linux.debug.arm64 = "res://bin/libgdexample.linux.template_debug.arm64.so"
linux.release.arm64 = "res://bin/libgdexample.linux.template_release.arm64.so"
linux.debug.rv64 = "res://bin/libgdexample.linux.template_debug.rv64.so"
linux.release.rv64 = "res://bin/libgdexample.linux.template_release.rv64.so"
android.debug.x86_64 = "res://bin/libgdexample.android.template_debug.x86_64.so"
android.release.x86_64 = "res://bin/libgdexample.android.template_release.x86_64.so"
android.debug.arm64 = "res://bin/libgdexample.android.template_debug.arm64.so"
android.release.arm64 = "res://bin/libgdexample.android.template_release.arm64.so"

[dependencies]
ios.debug = {
    "res://bin/libgodot-cpp.ios.template_debug.xcframework": ""
}
ios.release = {
    "res://bin/libgodot-cpp.ios.template_release.xcframework": ""
}

This file contains a configuration section that controls the entry function of the module. You should also set the minimum compatible Godot version with compatibility_minimum, which prevents older version of Godot from trying to load your extension. The reloadable flag enables automatic reloading of your extension by the editor every time you recompile it, without needing to restart the editor. This only works if you compile your extension in debug mode (default).

Раздел libraries является ключевым: он сообщает Godot расположение динамической библиотеки в файловой системе проекта для каждой поддерживаемой платформы. Это также приведёт к тому, что только этот файл будет экспортироваться при экспорте проекта, что означает, что пакет данных не будет содержать библиотеки, несовместимые с целевой платформой.

Наконец, раздел dependencies позволяет указать дополнительные динамические библиотеки, которые так же будут включены. Это важно, когда ваш плагин GDExtension реализует чью-то библиотеку и требует предоставления сторонней динамической библиотеки вместе с проектом.

Вот ещё один обзор для проверки правильной структуры файлов:

gdextension_cpp_example/
|
+--demo/                  # game example/demo to test the extension
|   |
|   +--main.tscn
|   |
|   +--bin/
|       |
|       +--gdexample.gdextension
|
+--godot-cpp/             # C++ bindings
|
+--src/                   # source code of the extension we are building
|   |
|   +--register_types.cpp
|   +--register_types.h
|   +--gdexample.cpp
|   +--gdexample.h

Пора вернуться в Godot. Мы загружаем главную сцену, созданную в самом начале, и теперь добавляем в сцену новый узел GDExample:

../../../_images/gdextension_cpp_nodes.webp

Мы назначим логотип Godot этому узлу в качестве текстуры и отключим свойство centered:

../../../_images/gdextension_cpp_sprite.webp

Наконец-то мы готовы к запуску проекта:

Добавление свойств

GDScript позволяет добавлять свойства в скрипт с помощью ключевого слова export. В GDExtension вы должны регистрировать свойства с помощью функций геттера и сеттера или напрямую реализовывать методы _get_property_list, _get и _set объекта (но это выходит далеко за рамки данного руководства).

Добавим свойство, позволяющее управлять амплитудой нашей волны.

В файле gdexample.h нам нужно добавить переменную-член и функции геттера/сеттера:

...
private:
    double time_passed;
    double amplitude;

public:
    void set_amplitude(const double p_amplitude);
    double get_amplitude() const;
...

В файле gdexample.cpp нам нужно внести несколько изменений. Мы покажем только изменяемые методы, не удаляйте строки, которые мы опускаем:

void GDExample::_bind_methods() {
    ClassDB::bind_method(D_METHOD("get_amplitude"), &GDExample::get_amplitude);
    ClassDB::bind_method(D_METHOD("set_amplitude", "p_amplitude"), &GDExample::set_amplitude);

    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "amplitude"), "set_amplitude", "get_amplitude");
}

GDExample::GDExample() {
    // Initialize any variables here.
    time_passed = 0.0;
    amplitude = 10.0;
}

void GDExample::_process(double delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);
}

void GDExample::set_amplitude(const double p_amplitude) {
    amplitude = p_amplitude;
}

double GDExample::get_amplitude() const {
    return amplitude;
}

После компиляции модуля с этими изменениями вы увидите, что в интерфейс добавлено свойство. Теперь вы можете изменять это свойство, и при запуске проекта увидите, что иконка Godot движется по большей траектории.

Сделаем то же самое для скорости анимации, используя функции сеттера и геттера. В заголовочный файл gdexample.h снова нужно добавить всего несколько строк кода:

...
    double amplitude;
    double speed;
...
    void _process(double delta) override;
    void set_speed(const double p_speed);
    double get_speed() const;
...

Это требует ещё нескольких изменений в файле gdexample.cpp; снова показываем только изменённые методы, не удаляйте ничего из опущенного:

void GDExample::_bind_methods() {
    ...
    ClassDB::bind_method(D_METHOD("get_speed"), &GDExample::get_speed);
    ClassDB::bind_method(D_METHOD("set_speed", "p_speed"), &GDExample::set_speed);

    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,20,0.01"), "set_speed", "get_speed");
}

GDExample::GDExample() {
    time_passed = 0.0;
    amplitude = 10.0;
    speed = 1.0;
}

void GDExample::_process(double delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);
}

...

void GDExample::set_speed(const double p_speed) {
    speed = p_speed;
}

double GDExample::get_speed() const {
    return speed;
}

Теперь при компиляции проекта мы увидим ещё одно свойство speed. Изменение его значения ускорит или замедлит анимацию. Кроме того, мы добавили диапазон свойств, описывающий, в каких пределах может быть значение. Первые два аргумента - минимальное и максимальное значение, третий - шаг изменения.

Примечание

Для простоты мы использовали только hint_range метода свойства. Есть гораздо больше вариантов на выбор. Их можно использовать для дальнейшей настройки отображения и установки свойств на стороне Godot.

Сигналы

Последнее, но не менее важное: сигналы также полностью работают в GDExtension. Чтобы ваше расширение реагировало на сигнал, испускаемый другим объектом, вам нужно вызвать connect для этого объекта. Мы не можем придумать хороший пример для нашей колеблющейся иконки Godot, нам потребовался бы гораздо более полный пример.

Вот требуемый синтаксис:

some_other_node->connect("the_signal", Callable(this, "my_method"));

Чтобы подключить наш сигнал the_signal из другого узла к нашему методу my_method, нам нужно передать методу connect имя сигнала и Callable. Callable содержит информацию об объекте, для которого можно вызвать метод. В нашем случае он связывает наш текущий экземпляр объекта this с методом my_method этого объекта. Затем метод connect добавит это к наблюдателям the_signal. Теперь, когда the_signal испускается, Godot знает, какой метод какого объекта нужно вызвать.

Обратите внимание, что вы можете вызывать my_method только если предварительно зарегистрировали его в методе _bind_methods. В противном случае Godot не будет знать о существовании my_method.

Чтобы узнать больше о Callable, ознакомьтесь со справочником класса: Callable.

Более распространённой является ситуация, когда ваш объект испускает сигналы. Для нашей колеблющейся иконки Godot мы сделаем что-то простое, чтобы показать, как это работает. Мы будем испускать сигнал каждую секунду и передавать новое местоположение.

В заголовочном файле gdexample.h нам нужно определить новый член time_emit:

...
    double time_passed;
    double time_emit;
    double amplitude;
...

На этот раз изменения в gdexample.cpp более сложные. Сначала нужно установить time_emit = 0.0; либо в методе _init, либо в конструкторе. Мы рассмотрим остальные два необходимых изменения по очереди.

В методе _bind_methods нам нужно объявить наш сигнал. Это делается следующим образом:

void GDExample::_bind_methods() {
    ...
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,20,0.01"), "set_speed", "get_speed");

    ADD_SIGNAL(MethodInfo("position_changed", PropertyInfo(Variant::OBJECT, "node"), PropertyInfo(Variant::VECTOR2, "new_pos")));
}

Здесь наш макрос ADD_SIGNAL может быть одиночным вызовом с аргументом MethodInfo. Первый параметр MethodInfo - это имя сигнала, а остальные параметры - типы PropertyInfo, описывающие сущность каждого параметра метода. Параметры PropertyInfo определяются типом данных параметра и именем, которое параметр будет иметь по умолчанию.

Таким образом, мы добавляем сигнал с MethodInfo, который называет сигнал "position_changed". Параметры PropertyInfo описывают два основных аргумента: один типа Object, другой типа Vector2, названные соответственно "node" и "new_pos".

Далее нам нужно изменить наш метод _process:

void GDExample::_process(double delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);

    time_emit += delta;
    if (time_emit > 1.0) {
        emit_signal("position_changed", this, new_position);

        time_emit = 0.0;
    }
}

После прохождения секунды мы испускаем наш сигнал и сбрасываем счётчик. Мы можем напрямую добавить значения параметров в emit_signal.

После компиляции библиотеки GDExtension мы можем зайти в Godot и выбрать наш узел спрайта. В доке Node мы можем найти наш новый сигнал и подключить его, нажав кнопку Connect или дважды щёлкнув по сигналу. Мы добавили скрипт на наш главный узел и реализовали наш сигнал следующим образом:

extends Node

func _on_Sprite2D_position_changed(node, new_pos):
    print("The position of " + node.get_class() + " is now " + str(new_pos))

Каждую секунду мы выводим нашу позицию в консоль.

Следующие шаги

Мы надеемся, что приведённый пример показал вам основы. Вы можете расширить этот пример для создания полноценных скриптов управления узлами в Godot с использованием C++.