Начало работы

Обзор рабочего процесса

Godot-cpp, как GDExtension, сложнее в использовании, чем GDScript и C#. Если вы решите работать с ним, вот как будет выглядеть ваш рабочий процесс:

  • Создайте новый проект godot-cpp (из template или с нуля, как описано ниже).

  • Разрабатывайте свой код локально, используя вашу favorite IDE.

  • Создавайте и тестируйте свой код с использованием самой ранней совместимой версии Godot.

  • Create builds for all platforms you want to support (e.g. using GitHub Actions).

  • Необязательно: Опубликовать на Godot Asset Library.

Пример проекта

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

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

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

  • Исполняемый файл 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.

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

Расширения GDExtensions, предназначенные для более ранних версий Godot, должны работать в более поздних младших версиях, но не наоборот. Например, расширение GDExtension, предназначенное для Godot 4.2, должно работать без проблем в Godot 4.3, но расширение, предназначенное для Godot 4.3, не будет работать в Godot 4.2.

Из этого правила есть одно исключение: расширения, предназначенные для Godot 4.0, не будут работать с Godot 4.1 и более поздними версиями (см. Обновление ваших 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

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

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

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

Open Godot and create a new project. For this example, we will place it in a folder called project inside our GDExtension's folder structure.

In our project, we'll create a scene containing a Node called "Main" and we'll save it as main.tscn. We'll come back to that later.

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

You should now have project, godot-cpp, and src directories in your GDExtension module.

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

gdextension_cpp_example/
|
+--project/                  # 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
#pragma once

#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;
};

} // namespace godot

Есть несколько примечательных моментов в вышеприведённом коде. Мы включаем 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_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();
}
}

The initialize_example_module and uninitialize_example_module functions get called respectively when Godot loads our plugin and when it unloads it. All we're doing here is parse through the functions in our bindings module to initialize them, but you might have to set up more things depending on your needs. We call the GDREGISTER_CLASS macro for each of our classes in our library.

Примечание

You can find information about GDREGISTER_CLASS (and alternatives) at Класс Object.

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

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

gdextension_cpp_example/src/register_types.h
#pragma once

#include <godot_cpp/core/class_db.hpp>

using namespace godot;

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

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

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

Примечание

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

Once you've downloaded the SConstruct file, place it in your GDExtension folder structure alongside godot-cpp, src, and project, then run:

scons platform=<platform>

You should now be able to find the module in project/bin/<platform>.

Примечание

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

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

Before we jump back into Godot, we need to create one more file in project/bin/.

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

[configuration]

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

[libraries]

macos.debug = "./libgdexample.macos.template_debug.dylib"
macos.release = "./libgdexample.macos.template_release.dylib"
windows.debug.x86_32 = "./gdexample.windows.template_debug.x86_32.dll"
windows.release.x86_32 = "./gdexample.windows.template_release.x86_32.dll"
windows.debug.x86_64 = "./gdexample.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "./gdexample.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "./libgdexample.linux.template_debug.x86_64.so"
linux.release.x86_64 = "./libgdexample.linux.template_release.x86_64.so"
linux.debug.arm64 = "./libgdexample.linux.template_debug.arm64.so"
linux.release.arm64 = "./libgdexample.linux.template_release.arm64.so"
linux.debug.rv64 = "./libgdexample.linux.template_debug.rv64.so"
linux.release.rv64 = "./libgdexample.linux.template_release.rv64.so"

Этот файл содержит раздел configuration, управляющий функцией входа модуля. Также следует установить минимальную совместимую версию Godot с помощью compatibility_minimum, что предотвращает попытки загрузки вашего расширения более старыми версиями Godot. Флаг reloadable позволяет редактору автоматически перезагружать расширение при каждой его перекомпиляции, без необходимости перезапуска редактора. Это работает только при компиляции расширения в режиме отладки (по умолчанию).

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

You can learn more about .gdextension files at Файл .gextension.

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

gdextension_cpp_example/
|
+--project/                  # 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++!

Вместо того, чтобы основывать свой проект на примере выше, мы рекомендуем начать заново, клонировав шаблон godot-cpp, и создать свой проект на его основе. Он лучше охватывает такие функции, как сборка через GitHub и дополнительный полезный шаблон SConstruct.