Спеціальні модулі в C++
Модулі
Godot дозволяє розширити двигун за модульним принципом. Нові модулі можна створювати, а потім увімкнути/вимкнути. Це дозволяє додавати нові функції двигуна на кожному рівні без модифікації ядра, яке можна розділити для використання та повторного використання в різних модулях.
Модулі знаходяться в підкаталозі modules/ системи збирання. За замовчуванням увімкнено десятки модулів, таких як GDScript (який, так, не є частиною базового механізму), середовище виконання Mono, модуль регулярних виразів та інші. Можна створювати та комбінувати скільки завгодно нових модулів. Система збірки SCons подбає про це прозоро.
для чого?
Незважаючи на те, що більшість ігор рекомендується писати за допомогою сценаріїв (оскільки це значно економить час), замість цього цілком можливо використовувати C++. Додавання модулів C++ може бути корисним у таких випадках:
Прив’язка зовнішньої бібліотеки до Godot (наприклад, PhysX, FMOD тощо).
Оптимізуйте важливі частини гри.
Додавання нових функцій до движка та/або редактора.
Перенесення існуючої гри на Godot.
Напишіть цілу нову гру на C++, тому що ви не можете жити без C++.
Створення модуля
Перед створенням модуля обов’язково download the source code of Godot and compile it.
Щоб створити новий модуль, першим кроком є створення каталогу всередині modules/. Якщо ви хочете підтримувати модуль окремо, ви можете розділити інший VCS на модулі та використовувати його.
Приклад модуля буде називатися "summator" (godot/modules/summator). Всередині ми створимо клас суматора:
#ifndef SUMMATOR_H
#define SUMMATOR_H
#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();
};
#endif // SUMMATOR_H
А потім файл 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
Потім ви можете заархівувати його та поділитися модулем з усіма іншими. Під час створення для кожної платформи (інструкції в попередніх розділах) ваш модуль буде включено.
Примітка
У модулях C++ для таких речей, як підкласи, існує обмеження на 5 параметрів. Його можна збільшити до 13, включивши файл заголовка core/method_bind_ext.gen.inc.
Користування модулем
Тепер ви можете використовувати ваш щойно створений модуль з будь-якого сценарію:
var s = Summator.new()
s.add(10)
s.add(20)
s.add(30)
print(s.get_total())
s.reset()
Результатом буде 60.
Дивись також
Попередній приклад Summator чудово підходить для невеликих спеціальних модулів, але що, якщо ви хочете використовувати більшу зовнішню бібліотеку? Зверніться до Прив'язка до зовнішніх бібліотек, щоб дізнатися більше про прив’язку до зовнішніх бібліотек.
Попередження
Якщо ваш модуль призначений для доступу з запущеного проекту (а не лише з редактора), ви також повинні перекомпілювати кожен шаблон експорту, який ви плануєте використовувати, а потім указати шлях до спеціального шаблону в кожному стилі експорту. Інакше ви отримаєте помилки під час запуску проекту, оскільки модуль не скомпільовано в шаблон експорту. Перегляньте сторінки Compiling для отримання додаткової інформації.
Зовнішня компіляція модуля
Компіляція модуля передбачає переміщення вихідних кодів модуля безпосередньо в каталог 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.
}
Удосконалення системи побудови для розробки
Попередження
Ця підтримка спільної бібліотеки не призначена для підтримки розповсюдження модуля іншим користувачам без перекомпіляції механізму. Для цього замість цього використовуйте GDExtension.
Наразі ми визначили чистий SCsub, який дозволяє нам додавати вихідні коди нашого нового модуля як частину двійкового файлу Godot.
Цей статичний підхід підходить, коли ми хочемо створити випускну версію нашої гри, враховуючи, що всі модулі потрібні в одному двійковому файлі.
Однак компроміс полягає в тому, що кожна зміна вимагає повної перекомпіляції гри. Незважаючи на те, що SCons здатний виявити та перекомпілювати лише файл, який було змінено, пошук таких файлів і зрештою зв’язування остаточного двійкового файлу займає багато часу.
Щоб уникнути таких витрат, ми можемо створити наш власний модуль як спільну бібліотеку, яка динамічно завантажуватиметься під час запуску двійкового файлу нашої гри.
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. ".linuxbsd.tools.64") but without the final ".so".
shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
env.Append(LIBS=[shared_lib_shim])
Після компіляції ми маємо отримати каталог bin, що містить двійковий файл godot* і наш libsummator*.so. Однак, враховуючи, що .so не знаходиться в стандартному каталозі (наприклад, /usr/lib), ми повинні допомогти нашому двійковому файлу знайти його під час виконання за допомогою змінної середовища LD_LIBRARY_PATH:
export LD_LIBRARY_PATH="$PWD/bin/"
./bin/godot*
Примітка
Ви повинні експортувати змінну середовища. Інакше ви не зможете запустити свій проект із редактора.
Крім того, було б добре мати можливість вибрати, чи скомпілювати наш модуль як спільну бібліотеку (для розробки) чи як частину двійкового файлу Godot (для випуску). Для цього ми можемо визначити спеціальний прапор, який буде передано SCons за допомогою команди ARGUMENT:
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)
Тепер за замовчуванням команда scons побудує наш модуль як частину двійкового файлу Godot та як спільну бібліотеку, якщо передати summator_shared=yes.
Нарешті, ви навіть можете ще більше прискорити збірку, явно вказавши свій спільний модуль як ціль у команді SCons:
scons summator_shared=yes platform=linuxbsd bin/libsummator.linuxbsd.tools.64.so
Написання індивідуальної документації
Написання документації може здатися нудним завданням, але настійно рекомендуємо задокументувати ваш щойно створений модуль, щоб користувачам було легше використовувати його. Не кажучи вже про те, що код, який ви написали рік тому, може стати невідрізнимим від коду, написаного кимось іншим, тому будьте добрі до себе в майбутньому!
Існує кілька кроків, щоб налаштувати власні документи для модуля:
Створіть новий каталог у корені модуля. Ім’я каталогу може бути будь-яким, але в цьому розділі ми використовуватимемо ім’я
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 функція.
Відредагуйте файл(и) після Базовий буквар класу і перекомпілюйте механізм.
Після завершення процесу компіляції документи стануть доступними у вбудованій системі документації двигуна.
Щоб підтримувати документацію в актуальному стані, все, що вам потрібно зробити, це просто змінити один із файлів 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, де виконуються тести.Напишіть кілька тестів. Ось приклад:
#ifndef TEST_SUMMATOR_H
#define TEST_SUMMATOR_H
#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
#endif // TEST_SUMMATOR_H
Скомпілюйте механізм за допомогою
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, він з’явиться у списку ресурсів, і всі доступні властивості можна буде серіалізувати під час збереження/завантаження.
За цією ж логікою ви можете розширити редактор і майже будь-яку область движка.