Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Приклад GDExtension C
Вступ
Це простий приклад того, як працювати з GDExtension безпосередньо з кодом C. Зауважте, що API не призначений для безпосереднього використання, тому це, безперечно, буде досить багатослівним і вимагатиме багато кроків навіть для невеликого прикладу. Однак він служить посиланням для створення прив’язок для іншої мови. Ви все ще можете використовувати API напряму, якщо хочете, що може бути зручним, коли лише прив’язуєте сторонню бібліотеку.
У цьому прикладі ми створимо спеціальний вузол, який переміщує спрайт на екрані на основі параметрів користувача. Хоча він дуже простий, він служить для того, щоб показати, як робити деякі речі з GDExtension, наприклад, реєструвати спеціальні класи з методами, властивостями та сигналами. Він дає уявлення про API GDExtension.
Налаштування проекту
Вам знадобиться кілька передумов:
виконуваний файл Godot 4.2 (або новішої версії).,
компілятор C,
SCons як інструмент побудови.
Оскільки тут використовується безпосередньо API, немає потреби використовувати репозиторій godot-cpp.
Структура файлу
Щоб упорядкувати наші файли, ми розділимо їх на дві папки:
gdextension_c_example/
|
+--project/ # game example/demo to test the extension
|
+--src/ # source code of the extension we are building
Нам також потрібна копія заголовка gdextension_interface.h з вихідного коду Godot, яку можна отримати безпосередньо з виконуваного файлу Godot, виконавши таку команду:
godot --dump-gdextension-interface
Це створює заголовок у поточній папці, тож ви можете просто скопіювати його до папки src у прикладі проекту.
Нарешті, є ще одне джерело інформації, на яке нам потрібно звернутися, це файл JSON із посиланням на Godot API. Цей файл не використовуватиметься кодом безпосередньо, ми використовуватимемо його лише для отримання деякої інформації вручну.
Щоб отримати цей файл JSON, просто викличте виконуваний файл Godot:
godot --dump-extension-api
Отриманий файл extension_api.json буде створено в поточній папці. Ви можете скопіювати цей файл до папки прикладу, щоб мати його під рукою.
Примітка
Це розширення націлено на Godot 4.2, але воно має працювати й на пізніших версіях. Якщо ви хочете націлити іншу мінімальну версію, переконайтеся, що ви отримали заголовок і JSON із версії Godot, на яку ви націлюєте.
Buildsystem
Використання buildsystem значно полегшує наше життя під час роботи з кодом C. Для зручності ми будемо використовувати SCons, оскільки це те саме, що використовує сам Godot.
Наступний файл SConstruct є простим файлом, який створить ваше розширення для поточної платформи, яку ви використовуєте, будь то Linux, macOS або Windows. Це буде неоптимізована збірка для цілей налагодження. Він також передбачає 64-розрядну збірку, яка актуальна для деяких частин прикладу коду. Створення інших типів збірок і крос-компіляція виходять за рамки цього посібника. Збережіть цей файл у кореневій папці.
#!/bin/env python
from SCons.Script import Environment
from os import path
import sys
env = Environment()
# Set the target path and name.
target_path = "project/bin/"
target_name = "libgdexample"
# Set the compiler and flags.
env.Append(CPPPATH=["src"]) # Add the src folder to the include path.
env.Append(CFLAGS=["-O0", "-g"]) # Make it a debug build.
# Use Clang on macOS.
if sys.platform == "darwin":
env["CC"] = "clang"
# Add all C files in "src" folder as sources.
sources = env.Glob("src/*.c")
# Create a shared library.
library = env.SharedLibrary(
target=path.join(target_path, target_name),
source=sources,
)
# Set the library as the default target.
env.Default(library)
Це включатиме всі файли C у папці src, тому нам не потрібно змінювати цей файл під час додавання нових вихідних файлів.
Ініціалізація розширення
Перший біт коду відповідатиме за ініціалізацію розширення. Саме завдяки цьому Godot дізнається про те, що надає наше GDExtension, як-от класи та плагіни.
Створіть файл init.h у папці src з таким вмістом:
#pragma once
#include "defs.h"
#include "gdextension_interface.h"
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level);
void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level);
GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization);
Оголошені тут функції мають сигнатури, очікувані API GDExtension.
Зверніть увагу на включення файлу defs.h. Це один із наших помічників для спрощення написання коду розширення. Наразі він міститиме лише визначення GDE_EXPORT, макросу, який робить функцію загальнодоступною у спільній бібліотеці, щоб Godot міг правильно її викликати. Цей макрос допомагає абстрагувати те, що очікує кожен компілятор.
Створіть файл defs.h у папці src із таким вмістом:
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#if !defined(GDE_EXPORT)
#if defined(_WIN32)
#define GDE_EXPORT __declspec(dllexport)
#elif defined(__GNUC__)
#define GDE_EXPORT __attribute__((visibility("default")))
#else
#define GDE_EXPORT
#endif
#endif // ! GDE_EXPORT
Ми також включаємо деякі стандартні заголовки, щоб полегшити роботу. Тепер нам залишається лише включити defs.h, і це буде бонусом.
Тепер давайте реалізуємо функції, які ми щойно оголосили. Створіть файл під назвою init.c у папці src і додайте цей код:
#include "init.h"
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
}
void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
}
GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
{
r_initialization->initialize = initialize_gdexample_module;
r_initialization->deinitialize = deinitialize_gdexample_module;
r_initialization->userdata = NULL;
r_initialization->minimum_initialization_level = GDEXTENSION_INITIALIZATION_SCENE;
return true;
}
Це встановлює дані ініціалізації, які очікує Godot. Функції для ініціалізації та деініціалізації налаштовано таким чином, щоб Godot викликав тоді, коли це необхідно. Він також встановлює рівень ініціалізації, який залежить від розширення. Оскільки ми плануємо додати спеціальний вузол, достатньо рівня SCENE.
Ми заповнимо функцію initialize_gdexample_module() пізніше, щоб зареєструвати наш власний клас.
Базовий клас
Щоб створити фактичний вузол, спочатку ми створимо структуру C для зберігання даних і функцій, які діятимуть як методи. План полягає в тому, щоб зробити це спеціальним вузлом, який успадковує від Sprite2D.
Створіть файл під назвою gdexample.h у папці src з таким вмістом:
#pragma once
#include "gdextension_interface.h"
#include "defs.h"
// Struct to hold the node data.
typedef struct
{
// Metadata.
GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;
// Constructor for the node.
void gdexample_class_constructor(GDExample *self);
// Destructor for the node.
void gdexample_class_destructor(GDExample *self);
// Bindings.
void gdexample_class_bind_methods();
Тут варто звернути увагу на поле object, яке містить покажчик на об’єкт Godot, і gdexample_class_bind_methods() функцію, яка реєструватиме метадані нашого спеціального класу (властивості, методи та сигнали). Останнє не є абсолютно необхідним, оскільки ми можемо це зробити під час реєстрації класу, але це дає змогу чіткіше розділити проблеми та дозволити нашому класу зареєструвати власні метадані.
Поле object є необхідним, оскільки наш клас успадкує клас Godot. Оскільки ми не можемо успадкувати його безпосередньо, оскільки ми не взаємодіємо з вихідним кодом (а C навіть не має класів), ми натомість кажемо Godot створити об’єкт того типу, який йому відомий, і приєднати до нього наше розширення. Нам знадобиться посилання на такі об’єкти, наприклад, під час виклику методів батьківського класу.
Давайте створимо вихідний аналог цього заголовка. Створіть файл gdexample.c у папці src і додайте до нього такий код:
#include "gdexample.h"
void gdexample_class_constructor(GDExample *self)
{
}
void gdexample_class_destructor(GDExample *self)
{
}
void gdexample_class_bind_methods()
{
}
Оскільки ми ще не маємо нічого спільного з цими функціями, вони деякий час залишатимуться порожніми.
Наступний крок - реєстрація нашого класу. Однак для цього нам потрібно створити StringName, а для цього нам потрібно отримати функцію з GDExtension API. Оскільки це нам знадобиться кілька разів, і нам також знадобляться інші речі, давайте створимо API-оболонку, щоб полегшити цю роботу.
API оболонки
Ми почнемо зі створення файлу api.h у папці src:
#pragma once
/*
This file works as a collection of helpers to call the GDExtension API
in a less verbose way, as well as a cache for methods from the discovery API,
just so we don't have to keep loading the same methods again.
*/
#include "gdextension_interface.h"
#include "defs.h"
extern GDExtensionClassLibraryPtr class_library;
// API methods.
extern struct Constructors
{
GDExtensionInterfaceStringNameNewWithLatin1Chars string_name_new_with_latin1_chars;
} constructors;
extern struct Destructors
{
GDExtensionPtrDestructor string_name_destructor;
} destructors;
extern struct API
{
GDExtensionInterfaceClassdbRegisterExtensionClass2 classdb_register_extension_class2;
} api;
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address);
Цей файл міститиме багато інших помічників, коли ми наповнюємо наше розширення чимось корисним. Наразі він має лише вказівник на функцію, яка створює StringName із рядка C (у кодуванні Latin-1), і інший для знищення StringName, який нам потрібно буде використовувати, щоб уникнути витоку пам’яті, а також функцію для реєстрації класу, що є нашою початковою метою.
Ми також зберігаємо тут посилання на class_library. Це те, що Godot надає нам під час ініціалізації розширення, і нам потрібно буде використовувати це під час реєстрації речей, які ми створюємо, щоб Godot міг визначити, яке розширення здійснює виклик.
Існує також функція для завантаження цих покажчиків на функції з API GDExtension.
Давайте попрацюємо над вихідним аналогом цього заголовка. Створіть файл api.c у папці src, додавши такий код:
#include "api.h"
GDExtensionClassLibraryPtr class_library = NULL;
struct Constructors constructors;
struct Destructors destructors;
struct API api;
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// Get helper functions first.
GDExtensionInterfaceVariantGetPtrDestructor variant_get_ptr_destructor = (GDExtensionInterfaceVariantGetPtrDestructor)p_get_proc_address("variant_get_ptr_destructor");
// API.
api.classdb_register_extension_class2 = (GDExtensionInterfaceClassdbRegisterExtensionClass2)p_get_proc_address("classdb_register_extension_class2");
// Constructors.
constructors.string_name_new_with_latin1_chars = (GDExtensionInterfaceStringNameNewWithLatin1Chars)p_get_proc_address("string_name_new_with_latin1_chars");
// Destructors.
destructors.string_name_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
}
Перша важлива річ тут – це p_get_proc_address. Це функція з API GDExtension, яка передається під час ініціалізації. Ви можете використовувати цю функцію, щоб запитувати певні функції в API за їх іменем. Тут ми кешуємо результати, тому нам не потрібно скрізь зберігати посилання на p_get_proc_address і використовувати натомість нашу оболонку.
На початку ми запитуємо функцію variant_get_ptr_destructor(). Це не буде використовуватися поза цією функцією, тому ми не додаємо до нашої оболонки, а лише кешуємо її локально. Приведення необхідне, щоб заглушити попередження компілятора.
Тоді ми отримуємо функцію, яка створює StringName із рядка C, саме те, що ми згадували раніше як необхідну функцію. Ми зберігаємо це в нашій структурі constructors.
Далі ми використовуємо функцію variant_get_ptr_destructor(), яку ми щойно отримали, щоб запитати деструктор для StringName, використовуючи значення enum з API gdextension_interface.h як параметр. Ми могли б отримати деструктори для інших типів подібним чином, але ми обмежимося тим, що потрібно для прикладу.
Нарешті, ми отримуємо функцію classdb_register_extension_class2(), яка нам знадобиться для реєстрації нашого спеціального класу.
Примітка
Ви можете здивуватися, чому 2 є в назві функції. Це означає, що це друга версія цієї функції. Стара версія зберігається для забезпечення зворотної сумісності зі старими розширеннями, але оскільки у нас є друга версія, найкраще використовувати нову, оскільки ми не маємо наміру підтримувати старіші версії Godot у цьому прикладі.
Заголовок gdextension_interface.h документує, у якій версії Godot було представлено кожну функцію.
Ми також визначаємо тут змінну class_library, яка буде встановлена під час ініціалізації.
Говорячи про ініціалізацію, тепер нам потрібно змінити файл init.c, щоб заповнити те, що ми щойно додали:
GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
{
class_library = p_library;
load_api(p_get_proc_address);
...
Тут ми за потреби встановлюємо class_library і викликаємо нашу нову функцію load_api(). Не забудьте також включити нові заголовки у верхній частині цього файлу:
#include "init.h"
#include "api.h"
#include "gdexample.h"
...
Оскільки ми тут, ми можемо зареєструвати наш новий спеціальний клас. Давайте заповнимо функцію initialize_gdexample_module():
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
if (p_level != GDEXTENSION_INITIALIZATION_SCENE)
{
return;
}
// Register class.
StringName class_name;
constructors.string_name_new_with_latin1_chars(&class_name, "GDExample", false);
StringName parent_class_name;
constructors.string_name_new_with_latin1_chars(&parent_class_name, "Sprite2D", false);
GDExtensionClassCreationInfo2 class_info = {
.is_virtual = false,
.is_abstract = false,
.is_exposed = true,
.set_func = NULL,
.get_func = NULL,
.get_property_list_func = NULL,
.free_property_list_func = NULL,
.property_can_revert_func = NULL,
.property_get_revert_func = NULL,
.validate_property_func = NULL,
.notification_func = NULL,
.to_string_func = NULL,
.reference_func = NULL,
.unreference_func = NULL,
.create_instance_func = gdexample_class_create_instance,
.free_instance_func = gdexample_class_free_instance,
.recreate_instance_func = NULL,
.get_virtual_func = NULL,
.get_virtual_call_data_func = NULL,
.call_virtual_with_data_func = NULL,
.get_rid_func = NULL,
.class_userdata = NULL,
};
api.classdb_register_extension_class2(class_library, &class_name, &parent_class_name, &class_info);
// Bind methods.
gdexample_class_bind_methods();
// Destruct things.
destructors.string_name_destructor(&class_name);
destructors.string_name_destructor(&parent_class_name);
}
Структура з інформацією про клас — це найбільше тут. Жодне з його полів не є обов’язковим, за винятком create_instance_func і free_instance_func. Ми ще не створили ці функції, тому незабаром нам доведеться над ними працювати. Зауважте, що ми пропускаємо ініціалізацію, якщо вона не на рівні SCENE. Цю функцію можна викликати кілька разів, один раз для кожного рівня, але ми хочемо зареєструвати наш клас лише один раз.
Інша невизначена річ тут – StringName. Це буде непрозора структура, призначена для зберігання даних Godot StringName у нашому розширенні. Ми визначимо його у файлі defs.h з відповідною назвою:
...
// The sizes can be obtained from the extension_api.json file.
#ifdef BUILD_32
#define STRING_NAME_SIZE 4
#else
#define STRING_NAME_SIZE 8
#endif
// Types.
typedef struct
{
uint8_t data[STRING_NAME_SIZE];
} StringName;
Як зазначено в коментарі, розміри можна знайти у файлі extension_api.json, який ми згенерували раніше, у властивості builtin_class_sizes. BUILD_32 ніколи не визначається, оскільки ми припускаємо, що тут ми працюємо з 64-розрядною збіркою Godot, але якщо вам це потрібно, ви можете додати env.Append(CPPDEFINES=["BUILD_32"]) до вашого файлу SConstruct.
Коментар // Types. передвіщає, що ми додамо більше типів до цього файлу. Залишимо це на потім.
Структура StringName призначена лише для зберігання даних Godot, тому нам не дуже важливо, що всередині неї. Хоча в даному випадку це лише вказівник на дані в купі. Ми будемо використовувати цю структуру, коли нам потрібно буде самостійно виділити дані для StringName, як ми це робимо під час реєстрації нашого класу.
Повертаючись до реєстрації, нам потрібно попрацювати над нашими функціями створення та безкоштовного доступу. Давайте включимо їх у gdexample.h, оскільки вони специфічні для спеціального класу:
...
// Bindings.
void gdexample_class_bind_methods();
GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata);
void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance);
...
Перш ніж ми зможемо реалізувати ці функції, нам знадобиться ще кілька речей у нашому API. Нам потрібен спосіб розподілу та звільнення пам’яті. Хоча ми могли б зробити це за допомогою старого доброго malloc(), натомість ми можемо використати функції керування пам’яттю Godot. Нам також знадобиться спосіб створити об’єкт Godot і встановити його за допомогою нашого спеціального екземпляра.
Отже, давайте змінимо api.h, щоб включити ці нові функції:
...
extern struct API
{
GDExtensionInterfaceClassdbRegisterExtensionClass2 classdb_register_extension_class2;
GDExtensionInterfaceClassdbConstructObject classdb_construct_object;
GDExtensionInterfaceObjectSetInstance object_set_instance;
GDExtensionInterfaceObjectSetInstanceBinding object_set_instance_binding;
GDExtensionInterfaceMemAlloc mem_alloc;
GDExtensionInterfaceMemFree mem_free;
} api;
Потім ми змінюємо функцію load_api() в api.c, щоб отримати ці нові функції:
...
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
...
// API.
api.classdb_register_extension_class2 = p_get_proc_address("classdb_register_extension_class2");
api.classdb_construct_object = (GDExtensionInterfaceClassdbConstructObject)p_get_proc_address("classdb_construct_object");
api.object_set_instance = (GDExtensionInterfaceObjectSetInstance)p_get_proc_address("object_set_instance");
api.object_set_instance_binding = (GDExtensionInterfaceObjectSetInstanceBinding)p_get_proc_address("object_set_instance_binding");
api.mem_alloc = (GDExtensionInterfaceMemAlloc)p_get_proc_address("mem_alloc");
api.mem_free = (GDExtensionInterfaceMemFree)p_get_proc_address("mem_free");
}
Тепер ми можемо повернутися до gdexample.c і визначити нові функції, не забувши включити api.h заголовок:
#include "gdexample.h"
#include "api.h"
...
const GDExtensionInstanceBindingCallbacks gdexample_class_binding_callbacks = {
.create_callback = NULL,
.free_callback = NULL,
.reference_callback = NULL,
};
GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata)
{
// Create native Godot object;
StringName class_name;
constructors.string_name_new_with_latin1_chars(&class_name, "Sprite2D", false);
GDExtensionObjectPtr object = api.classdb_construct_object(&class_name);
destructors.string_name_destructor(&class_name);
// Create extension object.
GDExample *self = (GDExample *)api.mem_alloc(sizeof(GDExample));
gdexample_class_constructor(self);
self->object = object;
// Set the extension instance in the native Godot object.
constructors.string_name_new_with_latin1_chars(&class_name, "GDExample", false);
api.object_set_instance(object, &class_name, self);
api.object_set_instance_binding(object, class_library, self, &gdexample_class_binding_callbacks);
destructors.string_name_destructor(&class_name);
return object;
}
void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance)
{
if (p_instance == NULL)
{
return;
}
GDExample *self = (GDExample *)p_instance;
gdexample_class_destructor(self);
api.mem_free(self);
}
Під час створення екземпляра об’єкта ми спочатку створюємо новий об’єкт Sprite2D, оскільки це батьківський об’єкт нашого класу. Потім ми виділяємо пам’ять для нашої спеціальної структури та викликаємо її конструктор. Ми також зберігаємо покажчик на об’єкт Godot у структурі, як ми згадували раніше.
Потім ми встановлюємо нашу спеціальну структуру як дані екземпляра. Це дасть Godot знати, що об’єкт є екземпляром нашого спеціального класу, і, наприклад, правильно викликати наші власні методи, а також передати ці дані назад.
Зауважте, що ми повертаємо створений нами об’єкт Godot, а не нашу спеціальну структуру.
Для функції gdextension_free_instance() ми викликаємо лише деструктор і звільняємо пам’ять, яку ми виділили для спеціальних даних. Не обов'язково руйнувати об'єкт Godot, оскільки про це подбає сам двигун.
Демонстраційний проект
Тепер, коли ми можемо створити та звільнити наш власний об'єкт, ми повинні мати змогу випробувати його в реальному проекті. Для цього вам потрібно відкрити Godot та створити новий проект у папці project. Керівник проекту може попередити вас, що папка не порожня, якщо ви вже компілювали розширення раніше, цього разу ви можете сміливо проігнорувати це попередження.
Якщо ви ще не скомпілювали розширення, настав час зробити це зараз. Для цього відкрийте термінал або командний рядок, перейдіть до кореневої папки розширення та запустіть scons. Він має швидко компілюватися, оскільки розширення дуже просте.
Потім створіть файл під назвою gdexample.gdextension у папці project. Це ресурс Godot, який описує розширення, дозволяючи рушію правильно його завантажити. Помістіть у цей файл наступний вміст:
[configuration]
entry_symbol = "gdexample_library_init"
compatibility_minimum = "4.2"
[libraries]
macos.debug = "res://bin/libgdexample.dylib"
linux.debug = "res://bin/libgdexample.so"
windows.debug = "res://bin/libgdexample.dll"
Як бачите, gdexample_library_init() — це та сама назва функції, яку ми визначили у нашому файлі init.c. Важливо, щоб імена збігалися, оскільки саме так Godot називає точку входу розширення.
Ми також встановили мінімальну сумісність 4.2, оскільки ми орієнтуємося на цю версію. Він все одно має працювати на пізніших версіях. Якщо ви використовуєте пізнішу версію Godot і покладаєтеся на нові функції, вам потрібно збільшити це значення до номера версії, який містить усе, що ви використовуєте. Перегляньте Сумісність версій для отримання додаткової інформації.
У розділі [libraries] ми встановлюємо шляхи до спільної бібліотеки на різних платформах. Тут є лише версії для налагодження, оскільки це те, над чим ми працюємо для прикладу. Використовуючи feature tags, ви можете точно налаштувати це, щоб також надавати версії випусків, додавати більше цільових операційних систем, а також надавати 32-розрядні та 64-розрядні двійкові файли.
Ви також можете додати до цього файлу залежності бібліотеки та спеціальні піктограми для ваших класів, але це виходить за рамки цього посібника.
Після збереження файлу поверніться до редактора. Godot має автоматично завантажити розширення. Нічого не буде видно, оскільки наше розширення реєструє лише новий клас. Щоб використовувати цей клас, додайте Node2D як корінь сцени. Перемістіть його в середину вікна перегляду для кращої видимості. Потім додайте новий дочірній вузол до кореня та в діалоговому вікні Створити новий вузол знайдіть «GDExample», ім’я нашого класу, яке там має бути зазначено. Якщо ні, це означає, що Godot не завантажив розширення належним чином, тому спробуйте перезапустити редактор і повторити кроки, щоб побачити, чи щось не пропало.
Наш спеціальний клас походить від Sprite2D, тому він має властивість Texture в інспекторі. Установіть для цього файл icon.svg, який Godot зручно створив для нас під час створення проекту. Збережіть цю сцену як main.tscn і запустіть її. Ви можете встановити його як основну сцену для зручності.
Вуаля! У нас є спеціальний вузол, який працює в Godot. Однак він нічого не робить і нічим не відрізняється від звичайного вузла Sprite2D. Далі ми виправимо це, додавши спеціальні методи та властивості.
Спеціальні методи
Звичайна річ у розширеннях – це створення методів для користувальницьких класів і надання їх API Godot. Ми збираємося створити пару геттерів і сеттерів, які будуть потрібні для подальшого зв’язування властивостей.
По-перше, давайте додамо нові поля в нашу структуру для зберігання значень для amplitude і speed, які ми будемо використовувати пізніше під час створення поведінки для вузла. Додайте їх до файлу gdexample.h, змінивши структуру GDExample:
...
typedef struct
{
// Public properties.
double amplitude;
double speed;
// Metadata.
GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;
...
У цьому ж файлі додайте оголошення для геттерів і сеттерів одразу після деструктора.
...
// Destructor for the node.
void gdexample_class_destructor(GDExample *self);
// Properties.
void gdexample_class_set_amplitude(GDExample *self, double amplitude);
double gdexample_class_get_amplitude(const GDExample *self);
void gdexample_class_set_speed(GDExample *self, double speed);
double gdexample_class_get_speed(const GDExample *self);
...
У файлі gdexample.c ми ініціалізуємо ці значення в конструкторі та додамо реалізації для цих нових функцій, які є досить тривіальними:
void gdexample_class_constructor(GDExample *self)
{
self->amplitude = 10.0;
self->speed = 1.0;
}
void gdexample_class_set_amplitude(GDExample *self, double amplitude)
{
self->amplitude = amplitude;
}
double gdexample_class_get_amplitude(const GDExample *self)
{
return self->amplitude;
}
void gdexample_class_set_speed(GDExample *self, double speed)
{
self->speed = speed;
}
double gdexample_class_get_speed(const GDExample *self)
{
return self->speed;
}
Щоб ці прості функції працювали, коли їх викликає Godot, нам знадобляться деякі оболонки, які допоможуть нам належним чином конвертувати дані в механізм і з нього.
Спочатку ми створимо оболонки для ptrcall. Це те, що Godot використовує, коли відомо, що типи значень точні, що дозволяє уникнути використання Variant. Нам знадобляться дві з них: одна для функцій, які не приймають аргументів і повертають double (для геттерів), а інша для функцій, які приймають один double аргумент і не повертають нічого (для сеттерів).
Додайте оголошення до файлу api.h:
void ptrcall_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);
void ptrcall_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);
Ці дві функції відповідають типу GDExtensionClassMethodPtrCall, як визначено в gdextension_interface.h. Ми використовуємо float як назву тут, оскільки в Godot тип float має подвійну точність, тому ми дотримуємося цієї угоди.
Потім ми реалізуємо ці функції у файлі api.c:
void ptrcall_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
// Call the function.
double (*function)(void *) = method_userdata;
*((double *)r_ret) = function(p_instance);
}
void ptrcall_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
// Call the function.
void (*function)(void *, double) = method_userdata;
function(p_instance, *((double *)p_args[0]));
}
Аргумент method_userdata — це користувацьке значення, яке ми надаємо Godot. У цьому випадку ми встановимо вказівник на функцію для тієї, яку ми хочемо викликати. Тож спочатку ми перетворюємо її на тип функції, а потім просто викликаємо її, передаючи аргументи, коли потрібно, або встановлюючи значення, що повертається.
Аргумент p_instance містить спеціальний екземпляр нашого класу, який ми передали за допомогою object_set_instance() під час створення об’єкта.
p_args - це масив аргументів. Зауважте, що це містить вказівники на значення. Ось чому ми розіменовуємо його при переході до наших функцій. Кількість аргументів буде оголошено під час зв’язування функції (що ми зробимо незабаром), і вона завжди включатиме параметри за замовчуванням, якщо вони існують.
Нарешті, r_ret є покажчиком на змінну, для якої потрібно встановити значення, що повертається. Як і аргументи, це буде правильний тип, як оголошено. Для функції, яка не повертає, ми повинні уникати її встановлення.
Зверніть увагу, що підрахунок типів і аргументів точний, тому, наприклад, якщо нам потрібні різні типи, нам доведеться створити більше обгорток. Це можна автоматизувати за допомогою генерації коду, але це виходить за рамки цього посібника.
Хоча функції ptrcall використовуються, коли типи є точними, іноді Godot не може знати, чи це так (коли виклик надходить з мови з динамічним типом, наприклад GDScript). У таких ситуаціях він використовує звичайні функції виклику, тому нам також потрібно надати їх під час зв’язування.
Давайте створимо дві нові оболонки у файлі api.h:
void call_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error);
void call_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error);
Вони йдуть за типом GDExtensionClassMethodCall, який дещо відрізняється. По-перше, ви отримуєте вказівники на варіанти замість точних типів. Існує також кількість аргументів і структура помилок, які ви можете встановити, якщо щось піде не так.
Щоб перевірити тип, а також витягнути взаємодію з Variant, нам знадобиться ще кілька функцій з API GDExtension. Отже, давайте розширимо наші структури оболонки:
extern struct Constructors {
...
GDExtensionVariantFromTypeConstructorFunc variant_from_float_constructor;
GDExtensionTypeFromVariantConstructorFunc float_from_variant_constructor;
} constructors;
extern struct API
{
...
GDExtensionInterfaceGetVariantFromTypeConstructor get_variant_from_type_constructor;
GDExtensionInterfaceGetVariantToTypeConstructor get_variant_to_type_constructor;
GDExtensionInterfaceVariantGetType variant_get_type;
} api;
Назви говорять усе про те, чим вони займаються. У нас є кілька конструкторів для створення та вилучення значення з плаваючою комою до та з Variant. У нас також є кілька помічників, щоб фактично отримати ці конструктори, а також функція, щоб дізнатися тип Variant.
Давайте отримаємо їх з API, як ми робили раніше, змінивши функцію load_api() у файлі api.c:
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
...
// API.
...
api.get_variant_from_type_constructor = (GDExtensionInterfaceGetVariantFromTypeConstructor)p_get_proc_address("get_variant_from_type_constructor");
api.get_variant_to_type_constructor = (GDExtensionInterfaceGetVariantToTypeConstructor)p_get_proc_address("get_variant_to_type_constructor");
api.variant_get_type = (GDExtensionInterfaceVariantGetType)p_get_proc_address("variant_get_type");
...
// Constructors.
...
constructors.variant_from_float_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_FLOAT);
constructors.float_from_variant_constructor = api.get_variant_to_type_constructor(GDEXTENSION_VARIANT_TYPE_FLOAT);
...
}
Тепер, коли ми маємо ці набори, ми можемо реалізувати наші оболонки викликів у тому самому файлі:
void call_0_args_ret_float(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error)
{
// Check argument count.
if (p_argument_count != 0)
{
r_error->error = GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS;
r_error->expected = 0;
return;
}
// Call the function.
double (*function)(void *) = method_userdata;
double result = function(p_instance);
// Set resulting Variant.
constructors.variant_from_float_constructor(r_return, &result);
}
void call_1_float_arg_no_ret(void *method_userdata, GDExtensionClassInstancePtr p_instance, const GDExtensionConstVariantPtr *p_args, GDExtensionInt p_argument_count, GDExtensionVariantPtr r_return, GDExtensionCallError *r_error)
{
// Check argument count.
if (p_argument_count < 1)
{
r_error->error = GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS;
r_error->expected = 1;
return;
}
else if (p_argument_count > 1)
{
r_error->error = GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS;
r_error->expected = 1;
return;
}
// Check the argument type.
GDExtensionVariantType type = api.variant_get_type(p_args[0]);
if (type != GDEXTENSION_VARIANT_TYPE_FLOAT)
{
r_error->error = GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT;
r_error->expected = GDEXTENSION_VARIANT_TYPE_FLOAT;
r_error->argument = 0;
return;
}
// Extract the argument.
double arg1;
constructors.float_from_variant_constructor(&arg1, (GDExtensionVariantPtr)p_args[0]);
// Call the function.
void (*function)(void *, double) = method_userdata;
function(p_instance, arg1);
}
Ці функції трохи довші, але їх легко використовувати. Спочатку вони перевіряють, чи кількість аргументів відповідає очікуванням, а якщо ні, вони встановлюють структуру помилки та повертають. Для того, що має один параметр, він також перевіряє правильність типу аргументу. Це важливо, оскільки невідповідні типи під час вилучення з Variant можуть спричинити збої.
Потім він переходить до вилучення аргументу за допомогою конструктора, який ми встановили раніше. Натомість без аргументів встановлюється значення, що повертається після виклику функції. Зверніть увагу, як вони використовують вказівник на змінну double, оскільки це те, що ці конструктори очікують.
Перш ніж ми зможемо прив’язати наші методи, нам потрібен спосіб створити екземпляри GDExtensionPropertyInfo. Хоча ми могли б виконати їх у функціях зв’язування, які ми реалізуємо пізніше, простіше мати для цього помічника, оскільки він нам знадобиться кілька разів, зокрема, коли ми зв’язуємо властивості.
Давайте створимо ці дві функції у файлі api.h:
// Create a PropertyInfo struct.
GDExtensionPropertyInfo make_property(
GDExtensionVariantType type,
const char *name);
GDExtensionPropertyInfo make_property_full(
GDExtensionVariantType type,
const char *name,
uint32_t hint,
const char *hint_string,
const char *class_name,
uint32_t usage_flags);
void destruct_property(GDExtensionPropertyInfo *info);
Перший є спрощеною версією другого, оскільки зазвичай нам не потрібні всі аргументи для властивості, і ми згодні з типовими значеннями. Крім того, у нас є функція для знищення PropertyInfo, оскільки нам потрібно створити рядки та імена рядків, від яких потрібно правильно позбутися.
Говорячи про це, нам також потрібен спосіб створення та знищення рядків, тому ми додамо доповнення до існуючих структур у цьому самому файлі. Ми також отримаємо нову функцію API для фактичного зв’язування нашого спеціального методу.
extern struct Constructors
{
...
GDExtensionInterfaceStringNewWithUtf8Chars string_new_with_utf8_chars;
} constructors;
extern struct Destructors
{
...
GDExtensionPtrDestructor string_destructor;
} destructors;
extern struct API
{
...
GDExtensionInterfaceClassdbRegisterExtensionClassMethod classdb_register_extension_class_method;
} api;
Перш ніж реалізувати їх, давайте коротко зупинимося у файлі defs.h і додамо розмір типу String і кілька переліків:
// The sizes can be obtained from the extension_api.json file.
#ifdef BUILD_32
#define STRING_SIZE 4
#define STRING_NAME_SIZE 4
#else
#define STRING_SIZE 8
#define STRING_NAME_SIZE 8
#endif
...
typedef struct
{
uint8_t data[STRING_SIZE];
} String;
// Enums.
typedef enum
{
PROPERTY_HINT_NONE = 0,
} PropertyHint;
typedef enum
{
PROPERTY_USAGE_NONE = 0,
PROPERTY_USAGE_STORAGE = 2,
PROPERTY_USAGE_EDITOR = 4,
PROPERTY_USAGE_DEFAULT = PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
} PropertyUsageFlags;
Хоча він має той самий розмір, що й StringName, зрозуміліше використовувати іншу назву для нього.
Переліки тут є лише помічниками, щоб дати назви числам, які вони представляють. Інформація про них міститься у файлі extension_api.json. Тут ми просто встановили ті, які нам потрібні для підручника, щоб зробити його більш лаконічним.
Переходячи до api.c, нам потрібно завантажити покажчики на нові функції, які ми додали до API.
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
...
// API
...
api.classdb_register_extension_class_method = (GDExtensionInterfaceClassdbRegisterExtensionClassMethod)p_get_proc_address("classdb_register_extension_class_method");
// Constructors.
...
constructors.string_new_with_utf8_chars = (GDExtensionInterfaceStringNewWithUtf8Chars)p_get_proc_address("string_new_with_utf8_chars");
// Destructors.
...
destructors.string_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING);
}
Тоді ми також можемо реалізувати функції для створення структури PropertyInfo.
GDExtensionPropertyInfo make_property(
GDExtensionVariantType type,
const char *name)
{
return make_property_full(type, name, PROPERTY_HINT_NONE, "", "", PROPERTY_USAGE_DEFAULT);
}
GDExtensionPropertyInfo make_property_full(
GDExtensionVariantType type,
const char *name,
uint32_t hint,
const char *hint_string,
const char *class_name,
uint32_t usage_flags)
{
StringName *prop_name = api.mem_alloc(sizeof(StringName));
constructors.string_name_new_with_latin1_chars(prop_name, name, false);
String *prop_hint_string = api.mem_alloc(sizeof(String));
constructors.string_new_with_utf8_chars(prop_hint_string, hint_string);
StringName *prop_class_name = api.mem_alloc(sizeof(StringName));
constructors.string_name_new_with_latin1_chars(prop_class_name, class_name, false);
GDExtensionPropertyInfo info = {
.name = prop_name,
.type = type,
.hint = hint,
.hint_string = prop_hint_string,
.class_name = prop_class_name,
.usage = usage_flags,
};
return info;
}
void destruct_property(GDExtensionPropertyInfo *info)
{
destructors.string_name_destructor(info->name);
destructors.string_destructor(info->hint_string);
destructors.string_name_destructor(info->class_name);
api.mem_free(info->name);
api.mem_free(info->hint_string);
api.mem_free(info->class_name);
}
Проста версія make_property() просто викликає повнішу версію з деякими аргументами за замовчуванням. Те, що саме означають ці значення, виходить за рамки цього підручника. Перегляньте сторінку про Object class, щоб дізнатися більше про методи зв’язування та властивості.
Повна версія більш складна. По-перше, він створює String і StringName для необхідних полів шляхом виділення пам'яті та виклику їхніх конструкторів. Потім він створює структуру GDExtensionPropertyInfo і встановлює всі поля з наданими аргументами. Нарешті він повертає створену структуру.
Функція destruct_property() проста, вона просто викликає деструктори для створених об’єктів і звільняє виділену для них пам’ять.
Давайте знову повернемося до заголовка api.h, щоб створити функції, які фактично прив’язуватимуть методи:
// Version for 0 arguments, with return.
void bind_method_0_r(
const char *class_name,
const char *method_name,
void *function,
GDExtensionVariantType return_type);
// Version for 1 argument, no return.
void bind_method_1(
const char *class_name,
const char *method_name,
void *function,
const char *arg1_name,
GDExtensionVariantType arg1_type);
Потім поверніться до файлу api.c, щоб реалізувати ці:
// Version for 0 arguments, with return.
void bind_method_0_r(
const char *class_name,
const char *method_name,
void *function,
GDExtensionVariantType return_type)
{
StringName method_name_string;
constructors.string_name_new_with_latin1_chars(&method_name_string, method_name, false);
GDExtensionClassMethodCall call_func = call_0_args_ret_float;
GDExtensionClassMethodPtrCall ptrcall_func = ptrcall_0_args_ret_float;
GDExtensionPropertyInfo return_info = make_property(return_type, "");
GDExtensionClassMethodInfo method_info = {
.name = &method_name_string,
.method_userdata = function,
.call_func = call_func,
.ptrcall_func = ptrcall_func,
.method_flags = GDEXTENSION_METHOD_FLAGS_DEFAULT,
.has_return_value = true,
.return_value_info = &return_info,
.return_value_metadata = GDEXTENSION_METHOD_ARGUMENT_METADATA_NONE,
.argument_count = 0,
};
StringName class_name_string;
constructors.string_name_new_with_latin1_chars(&class_name_string, class_name, false);
api.classdb_register_extension_class_method(class_library, &class_name_string, &method_info);
// Destruct things.
destructors.string_name_destructor(&method_name_string);
destructors.string_name_destructor(&class_name_string);
destruct_property(&return_info);
}
// Version for 1 argument, no return.
void bind_method_1(
const char *class_name,
const char *method_name,
void *function,
const char *arg1_name,
GDExtensionVariantType arg1_type)
{
StringName method_name_string;
constructors.string_name_new_with_latin1_chars(&method_name_string, method_name, false);
GDExtensionClassMethodCall call_func = call_1_float_arg_no_ret;
GDExtensionClassMethodPtrCall ptrcall_func = ptrcall_1_float_arg_no_ret;
GDExtensionPropertyInfo args_info[] = {
make_property(arg1_type, arg1_name),
};
GDExtensionClassMethodArgumentMetadata args_metadata[] = {
GDEXTENSION_METHOD_ARGUMENT_METADATA_NONE,
};
GDExtensionClassMethodInfo method_info = {
.name = &method_name_string,
.method_userdata = function,
.call_func = call_func,
.ptrcall_func = ptrcall_func,
.method_flags = GDEXTENSION_METHOD_FLAGS_DEFAULT,
.has_return_value = false,
.argument_count = 1,
.arguments_info = args_info,
.arguments_metadata = args_metadata,
};
StringName class_name_string;
constructors.string_name_new_with_latin1_chars(&class_name_string, class_name, false);
api.classdb_register_extension_class_method(class_library, &class_name_string, &method_info);
// Destruct things.
destructors.string_name_destructor(&method_name_string);
destructors.string_name_destructor(&class_name_string);
destruct_property(&args_info[0]);
}
Обидві функції дуже схожі. Спочатку вони створюють StringName з назвою методу. Це створюється в стеку, оскільки нам не потрібно зберігати його після завершення функції. Потім вони створюють локальні змінні для зберігання call_func і ptrcall_func, вказуючи на допоміжні функції, які ми визначили раніше.
На наступному етапі вони трохи розходяться. Перший створює властивість для значення, що повертається, яке має порожнє ім’я, оскільки воно не потрібно. Інший створює масив властивостей для аргументів, який у цьому випадку має один елемент. Цей також має масив метаданих, які можна використовувати, якщо в аргументі є щось особливе (наприклад, якщо значення int має 32 біти замість 64 бітів за замовчуванням).
Після цього вони створюють GDExtensionClassMethodInfo з обов’язковими полями для кожного випадку. Потім вони створюють StringName для імені класу, щоб асоціювати метод із класом. Потім вони викликають функцію API, щоб фактично прив’язати цей метод до класу. Нарешті, ми знищуємо створені об’єкти, оскільки вони більше не потрібні.
Примітка
Помічники зв’язування тут використовують помічники викликів, які ми створили раніше, тому зауважте, що ці помічники викликів приймають лише тип Godot FLOAT (який еквівалентний double у C). Якщо ви збираєтеся використовувати це для інших типів, вам потрібно буде перевірити тип аргументів і тип повернення та вибрати відповідний зворотний виклик функції. Тут цього уникають лише для того, щоб приклад не став ще довшим.
Тепер, коли у нас є засоби зв’язування методів, ми можемо зробити це в нашому спеціальному класі. Перейдіть до файлу gdexample.c і заповніть функцію gdexample_class_bind_methods():
void gdexample_class_bind_methods()
{
bind_method_0_r("GDExample", "get_amplitude", gdexample_class_get_amplitude, GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_1("GDExample", "set_amplitude", gdexample_class_set_amplitude, "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_0_r("GDExample", "get_speed", gdexample_class_get_speed, GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_1("GDExample", "set_speed", gdexample_class_set_speed, "speed", GDEXTENSION_VARIANT_TYPE_FLOAT);
}
Оскільки ця функція вже викликається процесом ініціалізації, ми можемо зупинитися на цьому. Ця функція стала набагато простішою після того, як ми створили всю інфраструктуру для її роботи. Ви бачите, що реалізація вбудованих функцій зв’язування тут займе трохи місця, а також буде досить повторюваною. Це також спрощує додавання іншого методу в майбутньому.
Якщо ви скомпілюєте код і знову відкриєте проєкт Godot, спочатку нічого не зміниться, оскільки ми додали лише два нових методи. Щоб переконатися, що вони зареєстровані правильно, ви можете знайти GDExample у довідці редактора та перевірити, чи вони присутні на сторінці документації.
Спеціальні властивості
Оскільки ми вже зв’язали геттер і сеттер для наших властивостей, ми можемо перейти до створення фактичних властивостей, які відображатимуться в інспекторі редактора Godot.
Враховуючи наше обширне налаштування в попередньому розділі, є лише кілька речей, необхідних для того, щоб ми могли прив’язувати властивості. По-перше, давайте отримаємо нову функцію API у файлі api.h:
extern struct API {
...
GDExtensionInterfaceClassdbRegisterExtensionClassProperty classdb_register_extension_class_property;
} api;
Давайте також оголосимо тут функцію для зв’язування властивостей:
void bind_property(
const char *class_name,
const char *name,
GDExtensionVariantType type,
const char *getter,
const char *setter);
У файлі api.c ми можемо завантажити нову функцію API:
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// API
...
api.classdb_register_extension_class_property = (GDExtensionInterfaceClassdbRegisterExtensionClassProperty)p_get_proc_address("classdb_register_extension_class_property");
...
}
Тоді ми можемо реалізувати нашу нову допоміжну функцію в тому самому файлі:
void bind_property(
const char *class_name,
const char *name,
GDExtensionVariantType type,
const char *getter,
const char *setter)
{
StringName class_string_name;
constructors.string_name_new_with_latin1_chars(&class_string_name, class_name, false);
GDExtensionPropertyInfo info = make_property(type, name);
StringName getter_name;
constructors.string_name_new_with_latin1_chars(&getter_name, getter, false);
StringName setter_name;
constructors.string_name_new_with_latin1_chars(&setter_name, setter, false);
api.classdb_register_extension_class_property(class_library, &class_string_name, &info, &setter_name, &getter_name);
// Destruct things.
destructors.string_name_destructor(&class_string_name);
destruct_property(&info);
destructors.string_name_destructor(&getter_name);
destructors.string_name_destructor(&setter_name);
}
Ця функція подібна до функції для методів прив’язки. Основна відмінність полягає в тому, що нам не потрібна додаткова структура, оскільки ми можемо просто використати GDExtensionPropertyInfo, створений нашою допоміжною функцією, тому це більш просто. Він лише створює значення StringName із рядків C, створює структуру інформації про властивості за допомогою нашого помічника, викликає функцію API для реєстрації властивості в класі, а потім знищує всі створені нами об’єкти.
Зробивши це, ми можемо розширити функцію gdexample_class_bind_methods() у файлі gdexample.c:
void gdexample_class_bind_methods()
{
bind_method_0_r("GDExample", "get_amplitude", gdexample_class_get_amplitude, GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_1("GDExample", "set_amplitude", gdexample_class_set_amplitude, "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_property("GDExample", "amplitude", GDEXTENSION_VARIANT_TYPE_FLOAT, "get_amplitude", "set_amplitude");
bind_method_0_r("GDExample", "get_speed", gdexample_class_get_speed, GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_method_1("GDExample", "set_speed", gdexample_class_set_speed, "speed", GDEXTENSION_VARIANT_TYPE_FLOAT);
bind_property("GDExample", "speed", GDEXTENSION_VARIANT_TYPE_FLOAT, "get_speed", "set_speed");
}
Якщо ви створюєте розширення за допомогою scons, ви побачите в редакторі Godot нову властивість, показану не лише на сторінці документації для спеціального класу, а й у доці Inspector, коли вибрано вузол GDExample.
Прив'язка віртуальних методів
Наш спеціальний вузол тепер має властивості впливати на його роботу, але він все ще нічого не робить. У цьому розділі ми прив’яжемо віртуальний метод _process() і змусимо наш власний спрайт трохи рухатися.
У файл gdexample.h давайте додамо функцію, яка представляє спеціальний метод _process():
// Methods.
void gdexample_class_process(GDExample *self, double delta);
Ми також додамо поле «приватне» для відстеження часу, витраченого в нашій користувацькій структурі. Це «приватне» лише в тому сенсі, що воно не буде прив’язане до Godot API, навіть якщо воно загальнодоступне на стороні C, враховуючи, що мова не має модифікаторів доступу.
typedef struct
{
// Private properties.
double time_passed;
...
} GDExample;
У відповідному вихідному файлі gdexample.c нам потрібно ініціалізувати нове поле в конструкторі:
void gdexample_class_constructor(GDExample *self)
{
self->time_passed = 0.0;
self->amplitude = 10.0;
self->speed = 1.0;
}
Тоді ми можемо створити найпростішу реалізацію для методу _process:
void gdexample_class_process(GDExample *self, double delta)
{
self->time_passed += self->speed * delta;
}
Наразі він не робитиме нічого, крім оновлення приватного поля, яке ми створили. Ми повернемося до цього після належного зв’язування методу.
Віртуальні методи дещо відрізняються від звичайних прив’язок. Замість того, щоб явно реєструвати сам метод, ми зареєструємо спеціальну функцію, яку Godot викличе, щоб запитати, чи реалізовано конкретний віртуальний метод у нашому розширенні. Механізм передаватиме StringName як аргумент, тому, дотримуючись духу цього підручника, ми створимо допоміжну функцію, щоб перевірити, чи дорівнює він рядку C.
Давайте додамо оголошення до файлу api.h:
// Compare a StringName with a C string.
bool is_string_name_equal(GDExtensionConstStringNamePtr p_a, const char *p_b);
Ми також додамо нову структуру до цього файлу, щоб утримувати покажчики функцій для спеціальних операторів:
extern struct Operators
{
GDExtensionPtrOperatorEvaluator string_name_equal;
} operators;
Потім у файл api.c ми завантажимо вказівник на функцію з API:
struct Operators operators;
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// Get helper functions first.
...
GDExtensionInterfaceVariantGetPtrOperatorEvaluator variant_get_ptr_operator_evaluator = (GDExtensionInterfaceVariantGetPtrOperatorEvaluator)p_get_proc_address("variant_get_ptr_operator_evaluator");
...
// Operators.
operators.string_name_equal = variant_get_ptr_operator_evaluator(GDEXTENSION_VARIANT_OP_EQUAL, GDEXTENSION_VARIANT_TYPE_STRING_NAME, GDEXTENSION_VARIANT_TYPE_STRING_NAME);
}
Як бачите, нам потрібен новий локальний помічник, щоб отримати покажчик функції для оператора.
Завдяки цьому ми можемо легко створити нашу функцію порівняння в тому самому файлі:
bool is_string_name_equal(GDExtensionConstStringNamePtr p_a, const char *p_b)
{
// Create a StringName for the C string.
StringName string_name;
constructors.string_name_new_with_latin1_chars(&string_name, p_b, false);
// Compare both StringNames.
bool is_equal = false;
operators.string_name_equal(p_a, &string_name, &is_equal);
// Destroy the created StringName.
destructors.string_name_destructor(&string_name);
// Return the result.
return is_equal;
}
Ця функція створює StringName з аргументу, порівнює з іншим за допомогою покажчика операторної функції та повертає результат. Зверніть увагу, що значення, що повертається для оператора, передається як вихідне посилання, це звичайна річ в API.
Давайте повернемося до файлу gdexample.h і додамо кілька функцій, які використовуватимуться як зворотні виклики для Godot API:
void *gdexample_class_get_virtual_with_data(void *p_class_userdata, GDExtensionConstStringNamePtr p_name);
void gdexample_class_call_virtual_with_data(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, void *p_virtual_call_userdata, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);
Фактично існує два способи реєстрації віртуальних методів. Лише один має частину get, у якій ви надаєте Godot правильно створений покажчик на функцію, яка буде викликана. Для цього нам потрібно було б створити ще одного помічника для кожного віртуального методу, що не дуже зручно. Замість цього ми використовуємо другий метод, який дозволяє повертати будь-які дані, а потім Godot викличе другий зворотний виклик і поверне нам ці дані разом з інформацією про виклик. Ми можемо просто надати наш власний покажчик на функцію як власні дані, а потім мати єдиний зворотний виклик для всіх віртуальних методів. Хоча в цьому прикладі ми будемо використовувати його лише для одного методу, цей спосіб простіше розширити.
Отже, давайте реалізуємо ці дві функції у файлі gdexample.c:
void *gdexample_class_get_virtual_with_data(void *p_class_userdata, GDExtensionConstStringNamePtr p_name)
{
// If it is the "_process" method, return a pointer to the gdexample_class_process function.
if (is_string_name_equal(p_name, "_process"))
{
return (void *)gdexample_class_process;
}
// Otherwise, return NULL.
return NULL;
}
void gdexample_class_call_virtual_with_data(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, void *p_virtual_call_userdata, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret)
{
// If it is the "_process" method, call it with a helper.
if (p_virtual_call_userdata == &gdexample_class_process)
{
ptrcall_1_float_arg_no_ret(p_virtual_call_userdata, p_instance, p_args, r_ret);
}
}
Ці функції також досить прості після попереднього створення всіх помічників.
Для першого ми просто перевіряємо, чи ім’я запитуваної функції є _process, і якщо це так, ми повертаємо вказівник функції на нашу її реалізацію. В іншому випадку ми повертаємо NULL, сигналізуючи, що метод не перевизначено. Ми не використовуємо тут p_class_userdata, оскільки ця функція призначена лише для одного класу, і ми не маємо жодних даних, пов’язаних з ним.
Другий схожий. Якщо це метод _process(), він використовує заданий покажчик на функцію для виклику помічника ptrcall, передаючи аргументи виклику вперед. Інакше він просто нічого не робить, оскільки у нас немає інших реалізованих віртуальних методів.
Єдине, чого не вистачає, це використання цих зворотних викликів під час реєстрації класу. Перейдіть до файлу init.c і змініть ініціалізацію class_info, щоб включити їх, замінивши значення NULL, яке використовувалося раніше:
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
...
GDExtensionClassCreationInfo2 class_info = {
...
.get_virtual_call_data_func = gdexample_class_get_virtual_with_data,
.call_virtual_with_data_func = gdexample_class_call_virtual_with_data,
...
};
...
}
Цього достатньо для прив'язки віртуального методу. Якщо ви зберете розширення та знову запустите проект Godot, буде викликана функція _process(). Ви просто не зможете цього сказати, оскільки сама функція не робить нічого видимого. Ми вирішимо це зараз, змусивши користувацький вузол рухатися за певним шаблоном.
Щоб змусити наш вузол щось робити, нам потрібно буде викликати методи Godot. Функціонує не лише API GDExtension, як ми це робили досі, а й фактичні методи двигуна, як ми робили б із сценаріями. Для цього, звичайно, потрібні додаткові налаштування.
По-перше, давайте додамо class_Vector2 до нашого файлу defs.h, щоб ми могли використовувати його в нашому методі:
// The sizes can be obtained from the extension_api.json file.
...
#ifdef REAL_T_IS_DOUBLE
#define VECTOR2_SIZE 16
#else
#define VECTOR2_SIZE 8
#endif
...
// Types.
...
typedef struct
{
uint8_t data[VECTOR2_SIZE];
} Vector2;
Визначення REAL_T_IS_DOUBLE потрібне, лише якщо ваша версія Godot була створена з підтримкою подвійної точності, яка не є типовою.
Тепер у файлі api.h ми додамо кілька речей до структур API, включно з новою для утримання методів двигуна для виклику.
extern struct Constructors
{
...
GDExtensionPtrConstructor vector2_constructor_x_y;
} constructors;
...
extern struct Methods
{
GDExtensionMethodBindPtr node2d_set_position;
} methods;
extern struct API
{
...
GDExtensionInterfaceClassdbGetMethodBind classdb_get_method_bind;
GDExtensionInterfaceObjectMethodBindPtrcall object_method_bind_ptrcall;
} api;
Потім у файлі api.c ми можемо отримати вказівники на функції з Godot:
struct Methods methods;
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// Get helper functions first.
...
GDExtensionInterfaceVariantGetPtrConstructor variant_get_ptr_constructor = (GDExtensionInterfaceVariantGetPtrConstructor)p_get_proc_address("variant_get_ptr_constructor");
// API.
...
api.classdb_get_method_bind = (GDExtensionInterfaceClassdbGetMethodBind)p_get_proc_address("classdb_get_method_bind");
api.object_method_bind_ptrcall = (GDExtensionInterfaceObjectMethodBindPtrcall)p_get_proc_address("object_method_bind_ptrcall");
// Constructors.
...
constructors.vector2_constructor_x_y = variant_get_ptr_constructor(GDEXTENSION_VARIANT_TYPE_VECTOR2, 3); // See extension_api.json for indices.
...
}
Єдиною вартою уваги тут є конструктор Vector2, для якого ми запитуємо індекс 3. Оскільки існує кілька конструкторів з різними типами аргументів, нам потрібно вказати, який з них нам потрібен. У цьому випадку ми отримуємо той, який приймає два числа з плаваючою точкою як координати x і y, звідки і назва. Цей індекс можна отримати з файлу extension_api.json. Зверніть увагу, що нам також потрібен новий місцевий помічник, щоб отримати його.
Майте на увазі, що тут ми нічого не отримуємо для структури методів. Це тому, що ця функція викликається занадто рано в процесі ініціалізації, тому класи ще не будуть належним чином зареєстровані.
Замість цього ми будемо використовувати зворотний виклик рівня ініціалізації, щоб отримати їх під час реєстрації нашого спеціального класу. Додайте це до файлу init.c:
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
if (p_level != GDEXTENSION_INITIALIZATION_SCENE)
{
return;
}
// Get ClassDB methods here because the classes we need are all properly registered now.
// See extension_api.json for hashes.
StringName native_class_name;
StringName method_name;
constructors.string_name_new_with_latin1_chars(&native_class_name, "Node2D", false);
constructors.string_name_new_with_latin1_chars(&method_name, "set_position", false);
methods.node2d_set_position = api.classdb_get_method_bind(&native_class_name, &method_name, 743155724);
destructors.string_name_destructor(&native_class_name);
destructors.string_name_destructor(&method_name);
...
}
Тут ми створюємо StringName для класу та методу, які ми хочемо отримати, а потім використовуємо API GDExtension, щоб отримати їх MethodBind, який є об’єктом, який представляє зв’язаний метод. Ми отримуємо метод set_position від Node2D, оскільки саме тут він був зареєстрований, навіть якщо ми збираємося використовувати його в Sprite2D, похідному класі.
Здавалося б, випадкове число для отримання прив'язки насправді є хешем сигнатури методу. Це дозволяє Godot відповідати методу, який ви запитуєте, навіть якщо в майбутній версії Godot цей підпис зміниться, надаючи метод сумісності, який відповідає тому, що ви просите. Це одна із систем, яка дозволяє двигуну завантажувати розширення, створені для попередніх версій. Ви можете отримати значення цього хешу з файлу extension_api.json.
З усім цим ми нарешті можемо реалізувати наш спеціальний метод _process() у файлі gdexample.c:
...
#include <math.h>
...
void gdexample_class_process(GDExample *self, double delta)
{
self->time_passed += self->speed * delta;
Vector2 new_position;
// Set up the arguments for the Vector2 constructor.
double x = self->amplitude + (self->amplitude * sin(self->time_passed * 2.0));
double y = self->amplitude + (self->amplitude * cos(self->time_passed * 1.5));
GDExtensionConstTypePtr args[] = {&x, &y};
// Call the Vector2 constructor.
constructors.vector2_constructor_x_y(&new_position, args);
// Set up the arguments for the set_position method.
GDExtensionConstTypePtr args2[] = {&new_position};
// Call the set_position method.
api.object_method_bind_ptrcall(methods.node2d_set_position, self->object, args2, NULL);
}
Після оновлення витраченого часу, масштабованого властивістю speed, він створює на основі цього значення x і y, також модульовані властивістю amplitude. Саме це дасть ефект візерунка. Заголовок math.h потрібен для функцій sin() і cos(), які тут використовуються.
Потім він встановлює масив аргументів для побудови Vector2, після чого викликається конструктор. Він встановлює інший масив аргументів і використовує його для виклику методу set_position() через прив’язку, яку ми отримали раніше.
Оскільки тут ніщо не виділяє пам’ять, немає необхідності очищати.
Тепер ми можемо знову побудувати розширення та знову відкрити Godot. Навіть у редакторі ви побачите рух спеціального спрайту.
Спробуйте змінити властивості Speed і Amplitude і подивіться, як відреагує спрайт.
Реєстрація та випромінювання сигналу
Щоб завершити цей підручник, давайте подивимося, як можна зареєструвати спеціальний сигнал і випромінювати його, коли це необхідно. Як ви могли здогадатися, нам знадобиться ще кілька покажчиків на функції з API та більше допоміжних функцій.
У файлі api.h ми додаємо дві речі. Одна — це функція API для реєстрації сигналу, інша — допоміжна функція для обгортання зв'язування сигналу.
extern struct API
{
...
GDExtensionInterfaceClassdbRegisterExtensionClassSignal classdb_register_extension_class_signal;
} api;
...
// Version for 1 argument.
void bind_signal_1(
const char *class_name,
const char *signal_name,
const char *arg1_name,
GDExtensionVariantType arg1_type);
У цьому випадку ми маємо версію лише для одного аргументу, оскільки це те, що ми збираємося використовувати.
Переходячи до файлу api.c, ми можемо завантажити цей новий покажчик на функцію та реалізувати помічник:
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// API.
...
api.classdb_register_extension_class_signal = (GDExtensionInterfaceClassdbRegisterExtensionClassSignal)p_get_proc_address("classdb_register_extension_class_signal");
...
}
void bind_signal_1(
const char *class_name,
const char *signal_name,
const char *arg1_name,
GDExtensionVariantType arg1_type)
{
StringName class_string_name;
constructors.string_name_new_with_latin1_chars(&class_string_name, class_name, false);
StringName signal_string_name;
constructors.string_name_new_with_latin1_chars(&signal_string_name, signal_name, false);
GDExtensionPropertyInfo args_info[] = {
make_property(arg1_type, arg1_name),
};
api.classdb_register_extension_class_signal(class_library, &class_string_name, &signal_string_name, args_info, 1);
// Destruct things.
destructors.string_name_destructor(&class_string_name);
destructors.string_name_destructor(&signal_string_name);
destruct_property(&args_info[0]);
}
Ця функція дуже схожа на функцію зв’язування методів. Основна відмінність полягає в тому, що нам не потрібно заповнювати іншу структуру, ми просто передаємо необхідні імена та масив аргументів. 1 в кінці означає кількість аргументів, які надає сигнал.
За допомогою цього ми можемо зв’язати сигнал у gdexample.c:
void gdexample_class_bind_methods()
{
...
bind_signal_1("GDExample", "position_changed", "new_position", GDEXTENSION_VARIANT_TYPE_VECTOR2);
}
Щоб подати сигнал, нам потрібно викликати метод emit_signal() на нашому спеціальному вузлі. Оскільки це функція vararg (це означає, що вона приймає будь-яку кількість аргументів), ми не можемо використовувати ptrcall. Щоб здійснити звичайний дзвінок, ми маємо створити варіанти, для виконання яких потрібно виконати ще кілька кроків.
Спочатку у файлі defs.h ми створюємо визначення для Variant:
...
// The sizes can be obtained from the extension_api.json file.
...
#ifdef REAL_T_IS_DOUBLE
#define VARIANT_SIZE 40
#define VECTOR2_SIZE 16
#else
#define VARIANT_SIZE 24
#define VECTOR2_SIZE 8
#endif
...
// Types.
...
typedef struct
{
uint8_t data[VARIANT_SIZE];
} Variant;
Спочатку ми встановили розмір Variant разом із розміром Vector2, який ми додали раніше. Потім ми використовуємо його для створення непрозорої структури, достатньої для зберігання даних Variant. Знову ж таки, ми встановлюємо розмір для збірок подвійної точності як запасний варіант, оскільки офіційні збірки Godot використовують одинарну точність.
Функція emit_signal() буде викликана з двома аргументами. Перший — це ім’я сигналу, який буде передано, а другий — це аргумент, який ми передаємо сигнальним з’єднанням, який є Vector2, як ми оголосили під час його зв’язування. Отже, ми створимо допоміжну функцію, яка може викликати MethodBind із цими типами. Незважаючи на те, що він повертає щось (код помилки), нам не потрібно мати справу з цим, тому поки що ми просто збираємося його ігнорувати.
У api.h ми додаємо кілька речей до існуючих структур, а також нову допоміжну функцію для виклику:
extern struct Constructors
{
...
GDExtensionVariantFromTypeConstructorFunc variant_from_string_name_constructor;
GDExtensionVariantFromTypeConstructorFunc variant_from_vector2_constructor;
} constructors;
extern struct Destructors
{
..
GDExtensionInterfaceVariantDestroy variant_destroy;
} destructors;
...
extern struct Methods
{
...
GDExtensionMethodBindPtr object_emit_signal;
} methods;
extern struct API
{
...
GDExtensionInterfaceObjectMethodBindCall object_method_bind_call;
} api;
...
// Helper to call with Variant arguments.
void call_2_args_stringname_vector2_no_ret_variant(
GDExtensionMethodBindPtr p_method_bind,
GDExtensionObjectPtr p_instance,
const GDExtensionTypePtr p_arg1,
const GDExtensionTypePtr p_arg2);
Тепер давайте перейдемо до файлу api.c, щоб завантажити ці нові покажчики на функції та реалізувати допоміжну функцію.
void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address)
{
// API.
...
api.object_method_bind_call = (GDExtensionInterfaceObjectMethodBindCall)p_get_proc_address("object_method_bind_call");
// Constructors.
...
constructors.variant_from_string_name_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME);
constructors.variant_from_vector2_constructor = api.get_variant_from_type_constructor(GDEXTENSION_VARIANT_TYPE_VECTOR2);
// Destructors.
...
destructors.variant_destroy = (GDExtensionInterfaceVariantDestroy)p_get_proc_address("variant_destroy");
...
}
...
void call_2_args_stringname_vector2_no_ret_variant(GDExtensionMethodBindPtr p_method_bind, GDExtensionObjectPtr p_instance, const GDExtensionTypePtr p_arg1, const GDExtensionTypePtr p_arg2)
{
// Set up the arguments for the call.
Variant arg1;
constructors.variant_from_string_name_constructor(&arg1, p_arg1);
Variant arg2;
constructors.variant_from_vector2_constructor(&arg2, p_arg2);
GDExtensionConstVariantPtr args[] = {&arg1, &arg2};
// Add dummy return value storage.
Variant ret;
// Call the function.
api.object_method_bind_call(p_method_bind, p_instance, args, 2, &ret, NULL);
// Destroy the arguments.
destructors.variant_destroy(&arg1);
destructors.variant_destroy(&arg2);
destructors.variant_destroy(&ret);
}
Ця допоміжна функція має шаблонний код, але є досить простою. Він встановлює два аргументи всередині виділених стеком варіантів, а потім створює масив із покажчиками на них. Він також встановлює інший Variant для збереження повертаного значення, яке нам не потрібно створювати, оскільки виклик очікує, що воно буде неініціалізованим.
Потім він фактично викликає MethodBind, використовуючи наданий нами екземпляр і аргументи. NULL у кінці буде вказівником на структуру GDExtensionCallError. Це можна використовувати для лікування потенційних помилок під час виклику функцій (наприклад, неправильні аргументи). Заради простоти ми не будемо розглядати це тут.
Наприкінці нам потрібно знищити Варіанти, які ми створили. Хоча технічно Vector2 не потребує знищення, зрозуміліше очистити все.
Нам також потрібно завантажити MethodBind, що ми зробимо у файлі init.c одразу після завантаження методу set_position, який ми робили раніше:
void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level)
{
...
constructors.string_name_new_with_latin1_chars(&native_class_name, "Object", false);
constructors.string_name_new_with_latin1_chars(&method_name, "emit_signal", false);
methods.object_emit_signal = api.classdb_get_method_bind(&native_class_name, &method_name, 4047867050);
destructors.string_name_destructor(&native_class_name);
destructors.string_name_destructor(&method_name);
// Register class.
...
}
Зверніть увагу, що тут ми повторно використовуємо змінні native_class_name і method_name, тому нам не потрібно оголошувати нові.
Тепер перейдіть до файлу gdexample.h, де ми збираємося додати кілька полів:
typedef struct
{
// Private properties.
..
double time_emit;
..
// Metadata.
StringName position_changed; // For signal.
} GDExample;
Перший зберігає час, що минув з моменту надходження останнього сигналу, оскільки ми будемо робити це через регулярні проміжки часу. Інший — просто кешувати назву сигналу, щоб нам не потрібно було щоразу створювати нове StringName.
У вихідному файлі gdexample.c ми можемо змінити конструктор і деструктор для роботи з новими полями:
void gdexample_class_constructor(GDExample *self)
{
...
self->time_emit = 0.0;
// Construct the StringName for the signal.
constructors.string_name_new_with_latin1_chars(&self->position_changed, "position_changed", false);
}
void gdexample_class_destructor(GDExample *self)
{
// Destruct the StringName for the signal.
destructors.string_name_destructor(&self->position_changed);
}
Важливо знищити StringName, щоб уникнути витоків пам’яті.
Тепер ми можемо додати до функції gdexample_class_process(), щоб фактично видавати сигнал:
void gdexample_class_process(GDExample *self, double delta)
{
...
self->time_emit += delta;
if (self->time_emit >= 1.0)
{
// Call the emit_signal method.
call_2_args_stringname_vector2_no_ret_variant(methods.object_emit_signal, self->object, &self->position_changed, &new_position);
self->time_emit = 0.0;
}
}
Це оновлює час, витрачений на випромінювання сигналу, і, якщо він перевищує одну секунду, викликає функцію emit_signal() для поточного екземпляра, передаючи назву сигналу та нову позицію як аргументи.
Тепер ми закінчили з нашим розширенням C GDExtension. Зберіть його ще раз і знову відкрийте проєкт Godot у редакторі.
На сторінці документації для GDExample ви можете побачити новий зв’язаний сигнал:
Щоб перевірити, чи він працює, давайте додамо невеликий скрипт до кореневого вузла, батьківського для нашого спеціального вузла, який друкує позицію на виході кожного разу, коли отримує сигнал:
extends Node2D
func _ready():
$GDExample.position_changed.connect(on_position_changed)
func on_position_changed(new_position):
prints("New position:", new_position)
Запустіть проект, і ви зможете спостерігати за значеннями, які друкуються в док-станції «Вивід» у редакторі:
Висновки
У цьому підручнику показано базове розширення з власними методами, властивостями та сигналами. Хоча для цього потрібна достатня кількість шаблонів, його можна добре масштабувати, створюючи допоміжні функції для вирішення нудних завдань.
Це повинно послужити гарною основою для розуміння API GDExtension і відправною точкою для створення спеціальних генераторів зв’язування. Насправді можна було б створити зв’язки для C за допомогою такого типу генератора, завдяки чому фактичне кодування виглядатиме більше як файл gdexample.c у цьому прикладі, який є досить простим і не дуже багатослівним.
Якщо ви хочете створювати фактичні розширення, краще використовувати прив'язки C++, оскільки це позбавляє ваш код усіх шаблонів. Перегляньте документацію godot-cpp documentation, щоб дізнатися, як це зробити.