Собственные модули на C++
Модули
Godot позволяет улучшать себя модульным путём. Новые модули могут быть созданы, а затем включены или выключены. Это позволяет добавлять новую функциональность в движок на любом уровне без модификации ядра, что позволяет разделить для использования или повторного использования разные модули.
Модули находятся в подкаталоге modules/ системы сборки. По умолчанию включены десятки модулей, таких как GDScript (который, да, не входит в базовый движок), поддержка GridMap, модуль регулярных выражений и другие. Можно создавать и объединять любое количество новых модулей. Система сборки SCons сделает это прозрачно.
Для чего это?
Хотя рекомендуется писать большую часть игры на скриптах (это значительно экономит время), вполне возможно использовать C++. Добавление модулей C++ может быть полезно в следующих случаях:
Связка внешней библиотеки с Godot (PhysX, FMOD, итд).
Оптимизация критических частей игры.
Добавление новой функциональности в движок и/или редактор.
Портирование существующей игры на Godot.
Написание целой, новой игры на C++ поскольку вы не можете жить без C++.
Примечание
Хотя можно использовать модули для пользовательской игровой логики, GDExtension, как правило, подходит больше, поскольку не требует перекомпиляции движка после каждого изменения кода.
Модули C++ необходимы в основном тогда, когда GDExtension недостаточно и требуется более глубокая интеграция движка.
Создание нового модуля
Перед созданием модуля обязательно download the source code of Godot and compile it.
Для создания нового модуля, первым шагом создайте директорию внутри modules/. Если вы хотите поддерживать модуль отдельно, вы можете назначить другую систему контроля версий(VCS) на модули и использовать её.
Модуль-пример будет называться "summator" (godot/modules/summator). Внутри него мы создадим класс-summator:
#pragma once
#include "core/object/ref_counted.h"
class Summator : public RefCounted {
GDCLASS(Summator, RefCounted);
int count;
protected:
static void _bind_methods();
public:
void add(int p_value);
void reset();
int get_total() const;
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;
}
Затем, новый класс нужно где-то зарегистрировать, для этого нужно создать еще два файла:
register_types.h
register_types.cpp
Важно
Эти файлы должны находиться в папке верхнего уровня вашего модуля (рядом с файлами SCsub и config.py), чтобы модуль был зарегистрирован правильно.
Эти файлы должны содержать следующее:
#include "modules/register_module_types.h"
void initialize_summator_module(ModuleInitializationLevel p_level);
void uninitialize_summator_module(ModuleInitializationLevel p_level);
/* yes, the word in the middle must be the same as the module folder name */
#include "register_types.h"
#include "core/object/class_db.h"
#include "summator.h"
void initialize_summator_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
ClassDB::register_class<Summator>();
}
void uninitialize_summator_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
// Nothing to do here in this example.
}
Далее нам необходимо создать файл SCsub, чтобы система сборки скомпилировала этот модуль:
# SCsub
Import('env')
env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build
С несколькими файлами кода, вы также можете добавить каждый файл отдельно в список строк Python:
src_list = ["summator.cpp", "other.cpp", "etc.cpp"]
env.add_source_files(env.modules_sources, src_list)
Это открывает широкие возможности для построения списка файлов с помощью циклов и логических операторов в Python. Примеры можно найти в некоторых модулях, которые поставляются с Godot по умолчанию.
Чтобы добавить директории включения(include) для компилятора, вы может добавить их в пути переменных сред:
env.Append(CPPPATH=["mylib/include"]) # this is a relative path
env.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path
Если вы хотите добавить пользовательские флаги компилятора при сборке модуля, сначала нужно клонировать env, чтобы эти флаги не добавлялись во всю сборку Godot (что может привести к ошибкам). Пример 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.
И наконец, файл конфигурации для модуля. Это скрипт Python, который должен называться config.py:
# config.py
def can_build(env, platform):
return True
def configure(env):
pass
Модуль спрашивается, можно ли выполнить сборку для конкретной платформы (в данном случае True означает, что сборка будет выполнена для каждой платформы).
Вот так. Надеемся это не было слишком сложным! Ваш модуль должен выглядеть примерно так:
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
Затем вы можете сжать и раздать модуль всем желающим. При сборке под каждую платформу (инструкции в предыдущих разделах), ваш модуль будет включен.
Использование модуля
Теперь вы можете использовать ваш новый модуль из любого скрипта:
var s = Summator.new()
s.add(10)
s.add(20)
s.add(30)
print(s.get_total())
s.reset()
На выходе получим 60.
См. также
Предыдущий пример Summator отлично подходит для небольших пользовательских модулей, но что делать, если вы хотите использовать более крупную внешнюю библиотеку? Подробнее о привязке к внешним библиотекам см. в документе Связывание внешних библиотек.
Предупреждение
Если ваш модуль предназначен для доступа из работающего проекта (а не только из редактора), необходимо также перекомпилировать каждый шаблон экспорта, который вы планируете использовать, а затем указать путь к пользовательскому шаблону в каждом шаблоне экспорта. В противном случае при запуске проекта возникнут ошибки, поскольку модуль не скомпилирован в шаблоне экспорта. Подробнее см. на страницах Компиляция.
Внешняя компиляция модуля
Компиляция модуля подразумевает перемещение исходных файлов модуля непосредственно в каталог modules/ движка. Хотя это самый простой способ компиляции модуля, есть несколько причин, по которым это может быть непрактично:
Необходимость вручную копировать исходные коды модулей каждый раз, когда требуется скомпилировать движок с модулем или без него, или необходимость дополнительных действий для ручного отключения модуля во время компиляции с помощью параметра сборки, например,
module_summator_enabled=no. Создание символических ссылок также может быть решением, но вам может потребоваться преодолеть ограничения ОС, например, необходимость наличия привилегии на использование символических ссылок при использовании скрипта.В зависимости от того, приходится ли вам работать с исходным кодом движка, файлы модулей, добавленные непосредственно в
modules/, изменяют рабочее дерево до такой степени, что использование VCS (системы контроля версий) (например,git) становится затруднительным, поскольку вам необходимо убедиться, что фиксируется только код, связанный с движком, с помощью фильтрации изменений.
Итак, если вы считаете, что необходима независимая структура пользовательских модулей, давайте возьмем наш модуль "summator" и переместим его в родительский каталог движка:
mkdir ../modules
mv modules/summator ../modules
Скомпилируйте движок с нашим модулем, указав параметр сборки custom_modules, который принимает разделенный запятыми список путей к каталогам, содержащим пользовательские модули C++, подобный следующему:
scons custom_modules=../modules
Система сборки обнаружит все модули в каталоге ../modules и скомпилирует их соответствующим образом, включая наш модуль "summator".
Предупреждение
Любой путь, переданный в custom_modules, будет преобразован в абсолютный путь, чтобы различать пользовательские и встроенные модули. Это означает, что такие процессы, как создание документации к модулям, могут зависеть от определённой структуры путей на вашем компьютере.
Настройка инициализации типов модулей
Модули могут взаимодействовать с другими встроенными классами движка во время выполнения и даже влиять на способ инициализации основных типов. До сих пор мы использовали register_summator_types для обеспечения доступности классов модулей в движке.
Примерный порядок настройки двигателя можно представить в виде списка следующих методов регистрации типов:
preregister_module_types();
preregister_server_types();
register_core_singletons();
register_server_types();
register_scene_types();
EditorNode::register_editor_types();
register_platform_apis();
register_module_types();
initialize_physics();
initialize_navigation_server();
register_server_singletons();
register_driver_types();
ScriptServer::init_languages();
Наш класс Summator инициализируется во время вызова register_module_types(). Представьте, что нам нужно удовлетворить какую-то распространённую зависимость во время выполнения модуля (например, для одиночных объектов) или разрешить переопределять существующие обратные вызовы методов движка до того, как они будут назначены самим движком. В этом случае мы хотим гарантировать, что классы наших модулей будут зарегистрированы до любого другого встроенного типа.
Здесь мы можем определить необязательный метод preregister_summator_types(), который будет вызван перед всем остальным на этапе настройки движка preregister_module_types().
Теперь нам нужно добавить этот метод в заголовочные и исходные файлы register_types:
#define MODULE_SUMMATOR_HAS_PREREGISTER
void preregister_summator_types();
void register_summator_types();
void unregister_summator_types();
Примечание
В отличие от других методов регистра, нам необходимо явно определить MODULE_SUMMATOR_HAS_PREREGISTER, чтобы система сборки знала, какие вызовы соответствующих методов следует включить во время компиляции. Имя модуля также должно быть преобразовано в верхний регистр.
#include "register_types.h"
#include "core/object/class_db.h"
#include "summator.h"
void preregister_summator_types() {
// Called before any other core types are registered.
// Nothing to do here in this example.
}
void register_summator_types() {
ClassDB::register_class<Summator>();
}
void unregister_summator_types() {
// Nothing to do here in this example.
}
Написание собственной документации
Написание документации может показаться скучным занятием, но вообще, настоятельно рекомендуется документировать ваш новый модуль, чтобы пользователям было проще им пользоваться и извлекать из этого выгоду. Не говоря уже о том, что код, написанный вами год назад, может стать неотличим от кода, написанного кем-то другим, так что будьте добры к себе будущему!
Существует много шагов для установки собственной документации в модуль:
Создайте новую папку в корне модуля. Имя папки может быть любым, но мы используем
doc_classesв этом разделе.Теперь нам нужно отредактировать
config.py, добавив следующий фрагмент:def get_doc_path(): return "doc_classes" def get_doc_classes(): return [ "Summator", ]
Функция get_doc_path() используется системой сборки для определения расположения документов. В этом случае они будут расположены в каталоге modules/summator/doc_classes. Если вы не укажете этот путь, путь к документации для вашего модуля будет возвращаться к основному каталогу doc/classes.
Метод get_doc_classes() необходим для того, чтобы система сборки могла определить, какие зарегистрированные классы принадлежат модулю. Здесь необходимо перечислить все ваши классы. Классы, которые вы не перечислите, будут помещены в основной каталог doc/classes.
Совет
Вы можете использовать Git, чтобы проверить, пропустили ли вы какие-либо занятия, проверив неотслеживаемые файлы с помощью git status. Например:
git status
Пример вывода:
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
...
Теперь мы можем генерировать документацию:
Это можно сделать, запустив doctool Godot, т. е. godot --doctool <path>, который выведет ссылку API движка на указанный <path> в формате XML.
В нашем случае мы укажем корень клонированного репозитория. Вы можете указать другую папку и просто скопировать туда нужные файлы.
Команда запуска:
bin/<godot_binary> --doctool .
Теперь если вы перейдете в папку godot/modules/summator/doc_classes, вы увидите, что она содержит файл Summator.xml или любые другие классы, на которые вы ссылаетесь в своей функции get_doc_classes.
Отредактируйте файл(ы) в соответствии с руководством по class reference primer и перекомпилируйте движок.
После завершения процесса компиляции документы станут доступны во встроенной системе документирования движка.
Чтобы поддерживать документацию в актуальном состоянии, вам достаточно просто изменить один из XML-файлов и перекомпилировать движок.
Если вы измените API своего модуля, вы также можете повторно извлечь документы, которые будут содержать ранее добавленные вами элементы. Конечно, если вы укажете папку Godot, убедитесь, что вы не потеряете работу, извлекая старые документы из старой сборки движка поверх новых.
Обратите внимание, что если у вас нет прав на запись к предоставленному вами <path>, вы можете столкнуться с ошибкой, аналогичной следующей:
ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
At: editor/doc/doc_data.cpp:956
Написание пользовательских модульных тестов
Можно писать самостоятельные модульные тесты как часть модуля C++. Если вы ещё не знакомы с процессом модульного тестирования в Godot, обратитесь к документу Модульное тестирование.
Процедура следующая:
Создайте новый каталог с именем
tests/в корневом каталоге вашего модуля:
cd modules/summator
mkdir tests
cd tests
Создайте новый набор тестов:
test_summator.h. Заголовок должен иметь префиксtest_, чтобы система сборки могла собрать его и включить в файлtests/test_main.cpp, где запускаются тесты.Напишите несколько тестовых случаев. Вот пример:
#pragma once
#include "tests/test_macros.h"
#include "modules/summator/summator.h"
namespace TestSummator {
TEST_CASE("[Modules][Summator] Adding numbers") {
Ref<Summator> s = memnew(Summator);
CHECK(s->get_total() == 0);
s->add(10);
CHECK(s->get_total() == 10);
s->add(20);
CHECK(s->get_total() == 30);
s->add(30);
CHECK(s->get_total() == 60);
s->reset();
CHECK(s->get_total() == 0);
}
} // namespace TestSummator
Скомпилируйте движок с
scons tests=yesи запустите тесты с помощью следующей команды:
./bin/<godot_binary> --test --source-file="*test_summator*" --success
Теперь вы должны увидеть проходящие утверждения.
Добавление пользовательских иконок редактора
Подобно тому, как вы можете писать автономную документацию внутри модуля, вы также можете создавать собственные значки для классов, которые будут отображаться в редакторе.
Для получения информации о фактическом процессе создания иконок редактора для интеграции в движок, пожалуйста, сначала обратитесь к Иконки редактора.
После создания значка(ов) выполните следующие шаги:
Создайте новый каталог в корне модуля с именем
icons. Это путь по умолчанию, по которому движок будет искать значки редактора модуля.Переместите вновь созданные значки
svg(оптимизированные или нет) в эту папку.Перекомпилируйте движок и запустите редактор. Теперь значок(и) будет отображаться в интерфейсе редактора там, где это необходимо.
Если вы хотите хранить значки в другом месте вашего модуля, добавьте следующий фрагмент кода в config.py, чтобы переопределить путь по умолчанию:
def get_icons_path(): return "path/to/icons"
Итоги
Запомните:
Используйте макрос
GDCLASSдля наследования, чтобы Godot мог его обернуть.Используйте
_bind_methods, чтобы привязать свои функции к скриптам и разрешить им работать как обратные вызовы для сигналов.Избегайте множественного наследования для классов, доступных Godot, поскольку
GDCLASSего не поддерживает. Вы по-прежнему можете использовать множественное наследование в своих классах, если они не доступны для API скриптов Godot.
Но это еще не все, в зависимости от того что вы делаете, вы столкнётесь с некоторыми (надеемся позитивными) сюрпризами.
Если вы наследуете от Node (или любого производного типа узла, например Sprite2D), ваш новый класс появится в редакторе, в дереве наследования диалога "Добавить узел".
Если вы наследуете от Resource, он будет показан в списке ресурсов, и все ваши открытые свойства будут сериализованы во время загрузки/сохранения.
Следуя той же логике, вы можете расширить возможности Редактора и почти любой области движка.