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 示例
介绍
这是一个关于如何在 C 代码中直接使用 GDExtension 的简单示例。请注意,相关 API 并不是为直接使用而设计的,所以注定会相当冗长,即便是小型示例也需要大量步骤。不过,该示例可以用作绑定其他语言的参考。如果你愿意,仍然可以直接使用这些 API,这在只用来绑定第三方库的情况下可能比较方便。
在本示例中,我们将创建一个自定义节点,它会根据用户参数在屏幕上移动精灵。虽然非常简单,但它展示了 GDExtension 的使用方法,比如注册带有方法、属性和信号的自定义类,让你能够对 GDExtension API 有一个初步的了解。
设置项目
你需要具备以下几个前置条件:
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
我们还需要复制一份 Godot 源码中的 gdextension_interface.h 头文件,可以通过运行以下命令直接从 Godot 可执行文件中获取:
godot --dump-gdextension-interface
这样就会在当前文件夹中创建这个头文件,你可以直接将其复制到示例项目的 src 文件夹中。
最后,我们还需要参考另一个信息来源,那就是包含 Godot API 参考的 JSON 文件。代码不会直接使用这个文件,我们只会手动从中提取一些信息。
要获取这个 JSON 文件,只需调用 Godot 可执行文件:
godot --dump-extension-api
生成的 extension_api.json 文件会保存在当前文件夹中。你可以将此文件复制到示例文件夹以方便查看。
备注
本扩展的目标平台是 Godot 4.2,但应该也能在其他更高版本上工作。如果你想针对不同的最低版本,请确保从目标版本的 Godot 中获取对应的头文件和 JSON 文件。
构建系统
使用构建系统可以大幅简化 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)
这会包含 src 文件夹中的所有 C 文件,因此在添加新的源文件时就不需要更改此文件了。
初始化扩展
第一段代码负责初始化扩展,这使得 Godot 能够知道我们的 GDExtension 提供哪些功能,例如类和插件。
请在 src 文件夹中创建名为 init.h 的文件,内容如下:
#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);
这里声明的函数具有 GDExtension API 所期望的签名。
注意其中包含了 defs.h 文件。这是我们用来简化编写扩展代码的辅助工具之一。目前它仅包含 GDE_EXPORT 的定义,这个宏可以将函数在共享库中导出,以便 Godot 正确调用。这个宏有助于抽象各个编译器的差异。
请在 src 文件夹中创建名为 defs.h 的文件,内容如下:
#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,标准头文件也会被一并引入。
现在来实现刚刚声明的函数吧。在 src 文件夹中创建名为 init.c 的文件,并添加以下代码:
#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。
请在 src 文件夹中创建名为 gdexample.h 的文件,内容如下:
#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 创建一个已知类型的对象并将我们的扩展附加到它上面。举例来说,在调用父类的方法时会需要引用此类对象。
请为这个头文件创建对应的源文件。在 src 文件夹中创建名为 gdexample.c 的文件,并添加如下代码:
#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
首先我们在 src 文件夹中创建 api.h 文件:
#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);
随着我们用实际功能完善扩展,这个文件也会逐步引入许多其他辅助函数。目前它就仅有三个函数指针:一个用于根据 C 字符串(Latin-1 编码)创建 StringName;另一个用于析构 StringName,避免内存泄漏;还有就是我们最初的目标,用于注册类的函数。
我们在这里还保留了对 class_library 的引用。这是 Godot 在初始化扩展时提供给我们的内容,当注册我们创建的内容时需要用到它,这样才能让 Godot 识别是哪个扩展发出的调用。
还有一个函数的作用是从 GDExtension API 中加载这些函数指针。
我们来编写这个头文件对应的源文件吧。在 src 文件夹中创建名为 api.c 的文件,并添加如下代码:
#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。这是来自 GDExtension API 的一个函数,会在初始化时传递进来。你可以使用这个函数通过名称向 API 请求特定函数。我们在此处缓存结果,这样就不必在各个地方都保存对 p_get_proc_address 的引用,而是可以直接使用我们的封装。
一开始我们请求的是 variant_get_ptr_destructor() 函数。它不会被此函数外的代码使用,所以我们不将它添加到我们的封装中,仅在本地缓存。这个强制类型转换是必要的,用于消除编译器警告。
接着我们获取的是之前提到的用于根据 C 字符串创建 StringName 的函数。我们将其存储在我们定义的 constructors 结构体中。
接下来,我们使用刚刚获取的 variant_get_ptr_destructor() 函数来查询 StringName 的析构函数,参数是 gdextension_interface.h API 中的枚举值。其他类型的析构函数也可以通过类似的方式获取,但这里仅限制在示例所需范围。
最后,我们获取 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 属性下面。这里假设我们使用的是 64 位版本的 Godot,因此永远不会定义 BUILD_32;但如果你确有需要,可以在 SConstruct 文件中加入 env.Append(CPPDEFINES=["BUILD_32"])。
// Types. 注释预示我们之后还会在此文件中添加更多类型。之后再说吧。
这里的 StringName 结构体是用来存放 Godot 数据的,所以我们对于它的内部具体内容并不在意。要说的话,它只是一个指向堆中数据的指针。当我们为自己分配 StringName 的数据时需要用到这个结构体,就像注册类时所做的那样。
回到注册部分,我们需要处理 create 和 free 两个函数。因为它们专属于自定义类,我们将把它们的声明放到 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;
然后修改 api.c 中的 load_api() 函数以获取这些新函数:
...
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。由于这个扩展非常简单,编译应该会很快完成。
接着,在 project 文件夹里创建一个名为 gdexample.gdextension 的文件。这是一个 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,因为我们目标是 4.2。这在更高的 Godot 版本上也仍应正常工作。如果你正在使用更高版本的 Godot 并依赖其新功能,则需要同步提高此版本号到包含所有你用到的特性的版本。详细信息请参阅版本兼容性。
在 [libraries] 部分中,我们设置了不同平台上共享库的路径。这里只有调试版本,因为我们在这个示例中使用的是调试版本。可以使用功能标记 "微调此项设置,从而增加发布版本、添加更多的目标平台以及 32位和 64 位的二进制文件。
你还可以在这个文件中添加库依赖和你自定义类的图标,但这不在本教程范围内。
保存文件后,回到编辑器。Godot 会自动加载扩展。因为我们的扩展只注册了一个新类,此时还看不到任何效果。要使用这个类,先添加一个 Node2D 作为场景的根节点,并把它移动到视窗的中央以获得更好的可见性。然后向根节点添加一个新子节点,并在创建新节点对话框中搜索“GDExample”(即我们所注册的自定义类的名字),它会显示在列表里。如果没有显示,说明 Godot 未能正确加载这个扩展,请尝试重启编辑器并重走之前的步骤,看看是否有遗漏。
自定义类由 Sprite2D 派生而来,所以在检查器中有一个 Texture 属性。将其设置为 Godot 在生成项目时为我们顺手创建的 icon.svg 文件。将场景另存为 main.tscn 然后运行它。为了方便,你可能想将其设置为主场景。
好了!我们已经创建了一个可以在 Godot 中运行的自定义节点。但目前它还不能做任何事情,也没有任何不同于普通 Sprite2D 节点的地方。接下来,我们将通过添加自定义方法和属性来解决这个问题。
自定义方法
在扩展中,一个常见的做法是为自定义类创建方法并将其暴露给 Godot API。我们将创建一些 getter 和 setter,这在后续绑定属性时是必需的。
首先,在结构体中添加一些新字段来保存 amplitude 和 speed 的值,稍后在为节点创建行为时将用到。将它们添加到 gdexample.h 文件中,修改 GDExample 结构体:
...
typedef struct
{
// Public properties.
double amplitude;
double speed;
// Metadata.
GDExtensionObjectPtr object; // Stores the underlying Godot object.
} GDExample;
...
在同一文件中,紧接着析构函数之后为 getter 和 setter 添加声明。
...
// 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 的函数(对应 getter),另一个针对接受单个 double 参数、无返回值的函数(对应 setter)。
在 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);
这两个函数遵循 gdextension_interface.h 中定义的 GDExtensionClassMethodPtrCall 类型。这里之所以使用 float 作为名称是因为 Godot 中的 float 类型实际上是双精度(double)类型的,因此遵循这一惯例。
然后在 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 等动态类型语言的调用)。在这些情况下,它会使用常规的 call 函数,因此在绑定时我们也需要提供这些函数。
请在 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 的指针,而不是确切的类型。此外,还有参数数量和出现错误时你可以设置的错误结构体。
为了检查类型并与 Variant 进行交互,我们需要从 GDExtension API 中获取另外几个函数。请扩展我们的封装结构体:
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 提取和创建浮点值。我们还有一些辅助函数来实际获取这些构造函数,以及一个用于确定 Variant 类型的函数。
跟之前一样,通过修改 api.c 文件中的 load_api() 函数从 API 获取这些数据:
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,因为我们需要创建需要适当处理的 String 和 StringName。
说到这里,我们还需要一种创建和销毁 String 的方法,因此我们将在同一个文件中为现有结构体添加内容。我们还将获得一个新的 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 类的页面以获取有关绑定方法和属性的更多详细信息。
完整版本更为复杂。首先,它通过分配内存并调用构造函数,为所需字段创建 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 类型(相当于 C 语言中的 double)。如果你打算将其用于其他类型,你需要检查参数和返回值的类型,并选择合适的函数回调。这里为了避免示例变得过长,我们省略了这部分处理。
现在我们有了绑定方法的工具,可以在自定义类中实际进行绑定操作。打开 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,并确认它们是否出现在文档页面中。
自定义属性
由于我们现在已经绑定了属性的 getter 和 setter,我们可以继续创建将在 Godot 编辑器检查器中显示的实际属性。
在前一节中我们已经进行了大量的设置,现在只需要做一些小调整就可以实现属性绑定。首先,让我们在 api.h 文件中添加一个新的 API 函数:
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,所以更加直接。它只会从 C 字符串创建 StringName 值,使用我们的辅助函数创建属性信息结构体,调用 API 函数在类中注册属性,然后销毁我们创建的所有对象。
完成这些后,我们可以在 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_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 编辑器中看到新属性不仅显示在自定义类的文档页面上,还会在选中 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,使用操作符函数指针与另一个进行比较,并返回结果。注意,操作符的返回值是通过 out 引用传递的,这是 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 的方法。不仅仅是像我们之前一直在使用的 GDExtension API 函数,还包括实际的引擎方法,就像我们使用脚本时那样。这自然需要一些额外的设置。
首先,让我们将 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 文件中检索到。注意我们还需要一个新的本地辅助函数来获取它。
请注意,我们无法在此处获取 methods 结构体的任何内容。这是因为该函数在初始化过程中调用得过早,类尚未正确注册。
相反,我们将在注册自定义类时使用初始化级别的回调来获取这些内容。将此添加到 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,然后使用 GDExtension API 来检索它们的 MethodBind,这是一个表示绑定方法的对象。我们从 Node2D 中获取 set_position 方法,因为这是它被注册的地方,即使我们将在派生类 Sprite2D 中使用它。
获取绑定的看似随机数实际上是方法签名的哈希值。这使得 Godot 能够在未来版本中即使这个签名发生变化,也能通过提供与你请求匹配的兼容方法来匹配你请求的方法。这是允许引擎加载为先前版本制作的扩展的系统之一。你可以从 extension_api.json 文件中获取这个哈希值。
有了这些准备,我们终于可以在 gdexample.c 文件中实现自定义的 _process() 方法了:
...
#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 属性的调制。这就是产生图案效果的原因。这里使用的 sin() 和 cos() 函数需要包含 math.h 头文件。
然后它会设置一个参数数组来构造一个 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。要进行常规调用,我们必须创建 Variant,这需要更多的步骤来完成。
首先,在 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 变量来设置参数,然后创建了一个包含这些变量指针的数组。它还设置了另一个 Variant 来保存返回值,由于调用期望它是未初始化的,所以我们不需要构造它。
然后它实际上会使用我们提供的实例和参数来调用 MethodBind。末尾的 NULL 是一个指向 GDExtensionCallError 结构体的指针。这可以用于处理调用函数时可能出现的错误(例如错误的参数)。为了简单起见,我们这里不处理这个。
最后,我们需要销毁我们创建的 Variant。虽然从技术上讲,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;
}
}
这将更新信号发射的时间,如果超过 1 秒,它将在当前实例上调用 emit_signal() 函数,并将信号名称和新位置作为参数传递。
现在我们已经完成了 C GDExtension 的编写。再次构建它,并在编辑器中重新打开演示项目。
在 GDExample 的文档页面中,你可以看到我们绑定的新信号:
要检查它是否正常工作,让我们在根节点(即我们自定义节点的父节点)上添加一个小脚本,每当它接收到信号时,就会在输出中打印位置信息:
extends Node2D
func _ready():
$GDExample.position_changed.connect(on_position_changed)
func on_position_changed(new_position):
prints("New position:", new_position)
运行项目后,你可以在编辑器的输出面板中观察到打印的值:
总结
本教程展示了如何创建一个包含自定义方法、属性和信号的基础扩展。虽然这需要编写大量样板代码,但只要通过创建辅助函数来处理繁琐的任务,这种方式具有良好的扩展性。
这应该能很好地帮助你理解 GDExtension API,并为你创建自定义绑定生成器提供一个起点。实际上,你可以使用这种类型的生成器为 C 语言创建绑定,使实际编码看起来更像本示例中的 gdexample.c 文件,这种方式非常直接且并不冗长。
如果你想创建实际的扩展,建议使用 C++ 绑定,因为它可以消除代码中的所有样板文件。查看 godot-cpp 文档了解如何实现这一点。