GDNative C示例

前言

本教程将向你介绍创建GDNative模块的最低要求. 这应该是你进入GDNative世界的起点. 了解本教程的内容将有助于你理解之后的所有内容. GDNative-demos 仓库 下载我们下面描述的例子对象的源代码. 这个例子项目还包含一个SConstruct文件, 使编译变得更容易一些, 但在本教程中, 我们将通过手工操作来了解这个过程. GDNative 可用于创建多种类型的Godot附加功能, 使用的接口有 PluginScript 或 ARVRInterfaceGDNative . 在本教程中, 我们将探讨如何创建一个 NativeScript 模块.NativeScript允许用C或C++写逻辑, 就像写GDScript文件一样. 我们将创建这个GDScript的C语言等效物:

  1. extends Referencevar datafunc _ready(): data = "World from GDScript!"func get_data(): return data

未来的教程将重点介绍其他类型的GDNative模块, 并解释何时以及如何使用每一种模块.

先决条件

在我们开始之前, 您需要一些东西:

  1. 目标版本的Godot可执行文件.
  2. gccclang . 在macOS上, 你可以从Mac App Store上安装Xcode. 在Windows上, 你可以使用Visual Studio 2015或更高版本, 或者MinGW-w64.
  3. godot-headers: Git 克隆存储库是 Godot 公开给 GDNative 的公共 API 的 C 头文件。 对于后者, 我们建议你为这个GDNative示例项目创建一个专用文件夹, 在该文件夹中打开终端并执行:
    1. git clone https://github.com/godotengine/godot-headers.git
    这会将所需文件下载到该文件夹中. 小技巧 如果你打算在你的GDNative项目中使用Git,可以添加godot-headers作为Git子模块。 注解 godot-headers 仓库有不同的分支。随着Godot的发展,GDNative也在发展。虽然我们努力保持不同版本之间的兼容性,但你应该根据Godot稳定分支(如 3.1 )和实际情况使用版本(如 3.1.1-stable )构建你的GDNative模块。用旧版本的Godot头文件构建的GDNative模块 可能 会在新版本的引擎上运行,但反之则不行。 godot-headers 仓库的 master 分支与Godot的 master 分支保持一致,因此包含了GDNative类和结构定义,可以在最新的开发版本中运行。 git tags ,找到与你的引擎版本匹配的标签。在 godot-headers 仓库中,这些标签以 godot- 为前缀,所以你可以检查 godot-3.1.1-stable 标签,以用于Godot 3.1.1。在你的克隆版本库中,可以执行以下操作:
    1. git checkout godot-3.1.1-stable
    3.1 ), 你也可以用 git checkout 3.1 来检查. <godotsource>/modules/gdnative/include 中找到更新的类和结构定义

    我们的C源

    让我们从编写我们的主代码开始. 最终, 我们希望有一个文件结构, 看起来像这样: ```
  • + godot-headers - + simple + bin - libsimple.dll/so/dylib - libsimple.gdnlib - simple.gdns main.tscn project.godot + src - simple.c
    1. `godot-headers` Git克隆旁创建一个名为 `simple` 的新项目。这将创建 `simple` 文件夹和 `project.godot` 文件。然后在 `simple` 文件夹旁边手动创建 `src` 文件夹,并在 `simple` 文件夹中创建一个 `bin` 子文件夹。
    2. `simple.c` 文件包含什么. 现在, 在我们的例子中, 只做了一个没有头文件的C源文件, 以保持事情的简单. 一旦你开始编写更大的项目, 建议将你的项目分成多个文件. 但这不属于本教程的范围.
    3. 我们将一点一点地看源代码, 所以下面所有的部分应该都放在一个大文件里. 每一部分在我们添加它的时候进行解释.

    include #include const godot_gdnative_core_api_struct api = NULL;const godot_gdnative_ext_nativescript_api_struct nativescript_api = NULL;

    1. 上面的代码包括GDNative API结构头文件和一个标准头文件, 我们将进一步使用这个头文件进行字符串操作. 然后它定义了两个指向两个不同结构的指针.GDNative支持大量的函数, 用于回调到Godot的主可执行文件. 为了让你的模块能够访问这些函数,GDNative为你的应用程序提供了一个包含所有这些函数指针的结构.
    2. `核心``API结构提供, 但其他功能有自己的 ``GDNative结构`, 可通过扩展访问.
    3. 在我们的示例中, 我们访问其中一个扩展, 以获取对NativeScript特别需要的函数的访问权限.
    4. NativeScript的行为与Godot中的任何其他脚本一样. 由于NativeScript API的级别相当低, 因此它需要库比其他脚本系统(如GDScript)更详细地指定许多内容. 创建NativeScript实例时, 将调用库给定的构造函数. 当该实例被销毁时, 将执行给定的析构函数.
    void simple_constructor(godot_object p_instance, void p_method_data);void simple_destructor(godot_object p_instance, void p_method_data, void p_user_data);godot_variant simple_get_data(godot_object p_instance, void p_method_data, void p_user_data, int p_num_args, godot_variant *p_args);
    1. `get_data` 的方法.
    2. `godot_` (你可以在后面修改), 其后是它们的名字. `gdnative_init` 是一个初始化动态库的函数.Godot会给它一个指向结构的指针, 该结构包含各种我们可能觉得有用的信息, 其中包括指向API结构的指针.
    3. 对于任何其他API结构, 我们需要遍历扩展数组并检查扩展的类型.
    void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options p_options) { api = p_options->api_struct; // Now find our extensions. for (int i = 0; i < api->num_extensions; i++) { switch (api->extensions[i]->type) { case GDNATIVE_EXT_NATIVESCRIPT: { nativescript_api = (godot_gdnative_ext_nativescript_api_struct )api->extensions[i]; }; break; default: break; } }}
    1. `gdnative_terminate` , 在卸载库之前调用它. 当没有任何对象使用它时,Godot将卸载库. 在这里, 您可以进行任何需要清理的清理工作. 对于我们的示例, 我们只是要清除我们的API指针.
    void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *p_options) { api = NULL; nativescript_api = NULL;}
    1. `nativescript_init` , 这是我们今天需要的最重要的函数. 这个函数将被Godot调用, 作为加载GDNative库的一部分, 并将我们提供的对象反馈给引擎.
    void GDN_EXPORT godot_nativescript_init(void *p_handle) { godot_instance_create_func create = { NULL, NULL, NULL }; create.create_func = &simple_constructor; godot_instance_destroy_func destroy = { NULL, NULL, NULL }; destroy.destroy_func = &simple_destructor; nativescript_api->godot_nativescript_register_class(p_handle, “SIMPLE”, “Reference”, create, destroy); godot_instance_method get_data = { NULL, NULL, NULL }; get_data.method = &simple_get_data; godot_method_attributes attributes = { GODOT_METHOD_RPC_MODE_DISABLED }; nativescript_api->godot_nativescript_register_method(p_handle, “SIMPLE”, “get_data”, attributes, get_data);}
    1. `nativescript_register_class` 来告诉引擎哪些类是可以实现的. 这里的第一个参数是句柄指针. 第二个是对象类的名称. 第三个是 ‘继承’ Godot中的对象类型;这不是真正的继承, 但也足够接近. 最后, 第四和第五个参数是对构造函数和析构函数的描述.
    2. `nativescript_register_method` 来告诉 Godot 我们的方法,本例中是我们的一个方法。在我们的例子中,就是 `get_data`。第一个参数仍然是句柄指针。第二个是我们要注册的对象类的名称。第三个是函数的名字,它将被 GDScript 所知。第四个是属性设置,参阅 `godot-headers/nativescript/godot_nativescript.h` 中的 `godot_method_rpc_mode` 枚举的相关值,第五个也是最后一个参数是调用方法时要调用的函数的描述。
    3. `instance_method` 包含函数本身的指针, 作为第一个字段. 这些结构中的另外两个字段是用于指定每个方法的用户数据. 第二个字段是 `method_data` , 在每次函数调用时作为 `p_method_data` 参数传递. 这对于在可能的多个不同的脚本类的不同方法上重复使用一个函数是很有用的. 如果 `method_data` 值是一个需要释放的内存指针, 第三个 `free_func` 字段可以包含一个指向释放该内存的函数的指针. 当脚本本身(不是实例!)被卸载时(所以通常是在库卸载时), 这个释放函数会被调用.
    4. 现在, 是时候开始处理我们对象的功能了. 首先, 我们定义一个结构, 用于存储GDNative类实例的成员数据.
    typedef struct user_data_struct { char data[256];} user_data_struct;
    1. 然后, 我们定义我们的构造函数. 我们在构造函数中所做的就是为结构分配内存并用一些数据填充它. 请注意, 我们使用Godot的内存函数, 以便跟踪内存, 然后将指针返回到我们的新结构. 如果实例化多个对象, 则此指针将充当我们的实例标识符.
    2. `p_user_data` 的参数传递给与我们的对象相关的任何函数, 并且可以用于标识我们的实例并访问其成员数据.
    void simple_constructor(godot_object p_instance, void p_method_data) { user_data_struct user_data = api->godot_alloc(sizeof(user_data_struct)); strcpy(user_data->data, “World from GDNative!”); return user_data;}
    1. Godot完成我们的对象时, 我们会调用析构函数, 并释放实例的成员数据.
    void simple_destructor(godot_object p_instance, void p_method_data, void *p_user_data) { api->godot_free(p_user_data);}
    1. `get_data` 函数. 数据总是以变量的形式发送和返回, 所以为了返回我们的数据, 也就是一个字符串, 我们首先需要把C语言的字符串转换成Godot的字符串对象, 然后把这个字符串对象复制到我们要返回的变量中.
    godot_variant simple_get_data(godot_object p_instance, void p_method_data, void p_user_data, int p_num_args, godot_variant **p_args) { godot_string data; godot_variant ret; user_data_struct user_data = (user_data_struct *)p_user_data; api->godot_string_new(&data); api->godot_string_parse_utf8(&data, user_data->data); api->godot_variant_new_string(&ret, &data); api->godot_string_destroy(&data); return ret;}
    1. `godot_TYPENAME_destroy` . 使用String创建Variant时, 它会引用String. 这意味着可以 `销毁` 原始字符串以减少引用计数. 如果没有发生这种情况,String内存将泄漏, 因为ref-count永远不会为零, 并且内存永远不会被释放. 返回的变体会被Godot自动销毁.
    2. 注解
    3. `godot_TYPENAME_destroy`. 创建Variant后, 将调用C++中的String类析构函数, C中也是如此.
    4. 我们返回的变体由Godot自动销毁.
    5. 这就是我们模块的完整源代码.
    6. ## 编译
    7. 我们现在需要编译我们的源代码. 如前所述, 我们在GitHub上的示例项目包含一个SCons配置, 它为你做了所有的艰苦工作, 但在我们的教程中, 我们将直接调用编译器.
    8. `src` 文件夹中打开一个终端会话, 从那里执行命令. 确保在你继续之前创建 `bin` 文件夹.
    9. Linux上:
    gcc -std=c11 -fPIC -c -I../godot-headers simple.c -o simple.ogcc -rdynamic -shared simple.o -o ../simple/bin/libsimple.so
    1. macOS上:
    clang -std=c11 -fPIC -c -I../godot-headers simple.c -o simple.osclang -dynamiclib simple.os -o ../simple/bin/libsimple.dylib
    1. Windows上:
    cl /Fosimple.obj /c simple.c /nologo -EHsc -DNDEBUG /MD /I. /I..\godot-headerslink /nologo /dll /out:..\simple\bin\libsimple.dll /implib:..\simple\bin\libsimple.lib simple.obj
    1. 注解
    2. `libsimple.lib` 库. 这是一个库, 你可以将其编译到项目中, 以提供对DLL的访问. 我们得到的是一个副产品, 我们不需要它:)当把你的游戏导出发布时, 这个文件将被忽略.
    3. ## 创建GDNativeLibrary (`.gdnlib`)文件
    4. GDNativeLibrary 资源, 扩展名为 `.gdnlib` , 与动态库放在一起. 这个文件告诉Godot哪些动态库是模块的一部分, 需要在每个平台上加载.
    5. 我们可以使用Godot来生成这个文件, 所以在编辑器中打开 “简单” 项目.
    6. 首先单击属性面板中的创建资源按钮:
    7. `GDNativeLibrary`:
    8. 你应该看到一个上下文编辑器出现在底部面板中. 使用右下角的 “展开底部面板” 按钮将其展开到全高:
    9. ### 常规属性
    10. 在检查器中, 你有各种属性来控制加载库.
    11. *Load Once* , 我们的库只被加载一次, 每个使用我们库的单独脚本将使用相同的数据. 你在全局定义的任何变量都可以从你创建的对象的任何实例中访问. 如果禁用 *Load Once* , 那么每次脚本访问库的时候都会有一份新的库被加载到内存中.
    12. *Singleton* 被启用, 我们的库就会自动加载, 并调用一个叫做 `godot_gdnative_singleton` 的函数. 我们将把这个问题留给另一个教程.
    13. *符号前缀* 是我们核心函数的前缀, 比如前面看到的 `godot_` 中的 `godot_nativescript_init` . 如果你使用多个希望静态链接的GDNative库, 你将不得不使用不同的前缀. 这又是一个需要在单独的教程中深入探讨的问题, 目前只需要在部署到iOS时使用, 因为这个平台不喜欢动态库.
    14. *Reloadable* 定义了当编辑器失去和获得焦点时, 是否应该重新加载库, 通常是为了从外部对库的任何变化中获取新的或修改的符号.
    15. ### 平台库
    16. GDNativeLibrary编辑器插件可以让你为你所要支持的每个平台和架构配置两件事.
    17. *动态库* 一栏(保存文件中的 `entry` 部分)告诉我们每个平台和特征组合需要加载哪个动态库. 这也告知导出器在向特定平台导出时需要导出哪些文件.
    18. *Dependencies* 列(也称 `依赖` 部分)告诉Godot为了让库工作, 每个平台还需要导出哪些文件. 如果您的GDNative模块使用另一个DLL来实现第三方库的功能, 这就是您列出该DLL的地方.
    19. 在我们的例子中, 我们只构建了Linux, macOS和/或Windows的库, 所以你可以通过点击文件夹按钮在相关字段中链接它们. 如果你建立了所有三个库, 你应该有这样的东西:
    20. ### 保存资源
    21. `bin/libsimple.gdnlib`:
    22. 该文件以基于文本的格式保存, 其内容应类似于以下内容:
    [general]singleton=falseloadonce=truesymbol_prefix=”godot“reloadable=true[entry]OSX.64=”res://bin/libsimple.dylib”OSX.32=”res://bin/libsimple.dylib”Windows.64=”res://bin/libsimple.dll”X11.64=”res://bin/libsimple.so”[dependencies]OSX.64=[ ]OSX.32=[ ]Windows.64=[ ]X11.64=[ ]
    1. ## 创建NativeScript (`.gdns`)文件
    2. `.gdnlib` 文件,我们已经告诉 Godot 如何加载库,现在需要告诉它关于我们的“SIMPLE”对象类。通过创建一个 NativeScript 资源文件,扩展名为 `.gdns`
    3. `NativeScript`:
    4. *Class Name* 中,输入 SIMPLE ,这是在调用 `godot_nativescript_register_class` 时在C源码中声明的对象类名称。通过点击 *Library* 和选择 *Load* 来选择我们的 `.gdnlib` 文件:
    5. 注解
    6. *类名* 必须与注册时在 `godot_nativescript_init` 中给出的拼写相同.
    7. `bin/simple.gdns` :
    8. `main` . 然后添加一个按钮和一个标签作为子节点. 把它们放在屏幕上合适的地方, 并给你的按钮起个名字.
    9. 选择控制节点, 给它附加一个脚本:
    10. `pressed` 信号与你的脚本连接起来:
    11. `main.tscn` .
    12. `main.gd` 代码:
    extends Control# load the Simple libraryonready var data = preload(“res://bin/simple.gdns”).new()func _on_Button_pressed(): $Label.text = “Data = “ + data.get_data() `` main.tscn` 文件, 然后就可以了: