GDNative C 示例

前言

本教程将向你介绍创建GDNative模块的最低要求. 这应该是你进入GDNative世界的起点. 了解本教程的内容将有助于你理解之后的所有内容.

Before we begin, you can download the source code to the example object we describe below in the GDNative-demos repository.

这个例子项目还包含一个SConstruct文件, 使编译变得更容易一些, 但在本教程中, 我们将通过手工操作来了解这个过程.

GDNative 可用于创建多种类型的Godot附加功能, 使用的接口有 PluginScriptARVRInterfaceGDNative . 在本教程中, 我们将探讨如何创建一个 NativeScript 模块.NativeScript允许用C或C++写逻辑, 就像写GDScript文件一样. 我们将创建这个GDScript的C语言等效物:

extends Reference

var data

func _ready():
    data = "World from GDScript!"

func get_data():
    return data

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

先决条件

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

  1. 目标版本的Godot可执行文件.

  2. 一个C语言编译器. 在Linux上, 从你的软件包管理器安装 gccclang . 在macOS上, 你可以从Mac App Store上安装Xcode. 在Windows上, 你可以使用Visual Studio 2015或更高版本, 或者MinGW-w64.

  3. A Git clone of the godot-headers repository: these are the C headers for Godot's public API exposed to GDNative.

对于后者, 我们建议你为这个GDNative示例项目创建一个专用文件夹, 在该文件夹中打开终端并执行:

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类和结构定义,可以在最新的开发版本中运行。

如果你想为Godot的稳定版本编写GDNative模块,请查看可用的Git标签,用 git tags ,找到与你的引擎版本匹配的标签。在 godot-headers 仓库中,这些标签以 godot- 为前缀,所以你可以检查 godot-3.1.1-stable 标签,以用于Godot 3.1.1。在你的克隆版本库中,可以执行以下操作:

git checkout godot-3.1.1-stable

如果由于任何原因缺少了与稳定版相匹配的标签, 你可以回到与之相匹配的稳定分支(例如 3.1 ), 你也可以用 git checkout 3.1 来检查.

如果您使用您自己的影响GDNative的更改从源代码构建Godot, 您可以在 <godotsource>/modules/gdnative/include 中找到更新的类和结构定义

我们的C源

让我们从编写我们的主代码开始. 最终, 我们希望有一个文件结构, 看起来像这样:

+ <your development folder>
  + godot-headers
    - <lots of files here>
  + simple
    + bin
      - libsimple.dll/so/dylib
      - libsimple.gdnlib
      - simple.gdns
    main.tscn
    project.godot
  + src
    - simple.c

打开Godot,在你的 godot-headers Git克隆旁创建一个名为 simple 的新项目。这将创建 simple 文件夹和 project.godot 文件。然后在 simple 文件夹旁边手动创建 src 文件夹,并在 simple 文件夹中创建一个 bin 子文件夹。

我们先来看看我们的 simple.c 文件包含什么. 现在, 在我们的例子中, 只做了一个没有头文件的C源文件, 以保持事情的简单. 一旦你开始编写更大的项目, 建议将你的项目分成多个文件. 但这不属于本教程的范围.

我们将一点一点地看源代码, 所以下面所有的部分应该都放在一个大文件里. 每一部分在我们添加它的时候进行解释.

#include <gdnative_api_struct.gen.h>

#include <string.h>

const godot_gdnative_core_api_struct *api = NULL;
const godot_gdnative_ext_nativescript_api_struct *nativescript_api = NULL;

上面的代码包括GDNative API结构头文件和一个标准头文件, 我们将进一步使用这个头文件进行字符串操作. 然后它定义了两个指向两个不同结构的指针.GDNative支持大量的函数, 用于回调到Godot的主可执行文件. 为了让你的模块能够访问这些函数,GDNative为你的应用程序提供了一个包含所有这些函数指针的结构.

为了保持这种实现模块化和易于扩展,核心功能可直接通过 core API 结构提供,但其他功能有自己的 GDNative struct 结构体,可通过扩展访问。

在我们的示例中, 我们访问其中一个扩展, 以获取对NativeScript特别需要的函数的访问权限.

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);

这些是我们将为对象实现的函数的前置声明. 需要一个构造函数和析构函数. 此外, 该对象将有一个名为 get_data 的方法.

接下来是Godot在加载动态库时将调用的第一个入口点. 这些方法的前缀都是 godot_ (你可以在后面修改), 其后是它们的名字. gdnative_init 是一个初始化动态库的函数.Godot会给它一个指向结构的指针, 该结构包含各种我们可能觉得有用的信息, 其中包括指向API结构的指针.

对于任何其他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;
        }
    }
}

接下来是 gdnative_terminate , 在卸载库之前调用它. 当没有任何对象使用它时,Godot将卸载库. 在这里, 您可以进行任何需要清理的清理工作. 对于我们的示例, 我们只是要清除我们的API指针.

void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *p_options) {
    api = NULL;
    nativescript_api = NULL;
}

最后, 我们有 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);
}

我们首先通过调用 nativescript_register_class 来告诉引擎哪些类是可以实现的. 这里的第一个参数是句柄指针. 第二个是对象类的名称. 第三个是 '继承' 的Godot中的对象类型;这不是真正的继承, 但也足够接近. 最后, 第四和第五个参数是对构造函数和析构函数的描述.

然后我们通过调用 nativescript_register_method 来告诉 Godot 我们的方法,本例中是我们的一个方法。在我们的例子中,就是 get_data。第一个参数仍然是句柄指针。第二个是我们要注册的对象类的名称。第三个是函数的名字,它将被 GDScript 所知。第四个是属性设置,参阅 godot-headers/nativescript/godot_nativescript.h 中的 godot_method_rpc_mode 枚举的相关值,第五个也是最后一个参数是调用方法时要调用的函数的描述。

描述结构 instance_method 包含函数本身的指针, 作为第一个字段. 这些结构中的另外两个字段是用于指定每个方法的用户数据. 第二个字段是 method_data , 在每次函数调用时作为 p_method_data 参数传递. 这对于在可能的多个不同的脚本类的不同方法上重复使用一个函数是很有用的. 如果 method_data 值是一个需要释放的内存指针, 第三个 free_func 字段可以包含一个指向释放该内存的函数的指针. 当脚本本身(不是实例!)被卸载时(所以通常是在库卸载时), 这个释放函数会被调用.

现在, 是时候开始处理我们对象的功能了. 首先, 我们定义一个结构, 用于存储GDNative类实例的成员数据.

typedef struct user_data_struct {
    char data[256];
} user_data_struct;

然后, 我们定义我们的构造函数. 我们在构造函数中所做的就是为结构分配内存并用一些数据填充它. 请注意, 我们使用Godot的内存函数, 以便跟踪内存, 然后将指针返回到我们的新结构. 如果实例化多个对象, 则此指针将充当我们的实例标识符.

该指针将作为名为 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;
}

当Godot完成我们的对象时, 我们会调用析构函数, 并释放实例的成员数据.

void simple_destructor(godot_object *p_instance, void *p_method_data, void *p_user_data) {
    api->godot_free(p_user_data);
}

最后, 我们实现我们的 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;
}

字符串在 Godot 中进行堆分配,因此它们具有释放内存的析构函数。析构函数名为 godot_类型名称_destroy。使用 String 创建 Variant 时,它会引用这个 String。这意味着可以“销毁”原始 String 以减少引用计数。如果没有发生这种情况,String 的内存将泄漏,因为引用计数永远不会为零,内存永远不会被释放。返回的变体会被 Godot 自动销毁。

备注

在更复杂的操作中,跟踪哪个值需要重新分配以及哪个值不需要可能会造成混淆。一般来说:当将调用 C++ 析构函数时,请调用 godot_类型名称_destroy。创建 Variant 后,将调用 C++ 中的 String 类析构函数,在 C 中也是如此。

我们返回的变体由Godot自动销毁.

这就是我们模块的完整源代码.

编译

我们现在需要编译我们的源代码. 如前所述, 我们在GitHub上的示例项目包含一个SCons配置, 它为你做了所有的艰苦工作, 但在我们的教程中, 我们将直接调用编译器.

假设你坚持使用上面建议的文件夹结构, 最好在 src 文件夹中打开一个终端会话, 从那里执行命令. 确保在你继续之前创建 bin 文件夹.

在Linux上:

gcc -std=c11 -fPIC -c -I../godot-headers simple.c -o simple.o
gcc -rdynamic -shared simple.o -o ../simple/bin/libsimple.so

在macOS上:

clang -std=c11 -fPIC -c -I../godot-headers simple.c -o simple.os
clang -dynamiclib simple.os -o ../simple/bin/libsimple.dylib

在Windows上:

cl /Fosimple.obj /c simple.c /nologo -EHsc -DNDEBUG /MD /I. /I..\godot-headers
link /nologo /dll /out:..\simple\bin\libsimple.dll /implib:..\simple\bin\libsimple.lib simple.obj

备注

在Windows构建中, 你还会得到一个 libsimple.lib 库. 这是一个库, 你可以将其编译到项目中, 以提供对DLL的访问. 我们得到的是一个副产品, 我们不需要它:)当把你的游戏导出发布时, 这个文件将被忽略.

创建GDNativeLibrary (.gdnlib)文件

模块编译完成后, 需要创建一个相应的 GDNativeLibrary 资源, 扩展名为 .gdnlib , 与动态库放在一起. 这个文件告诉Godot哪些动态库是模块的一部分, 需要在每个平台上加载.

我们可以使用Godot来生成这个文件, 所以在编辑器中打开 "简单" 项目.

首先单击“检查器”中的创建资源按钮:

../../../_images/new_resource.gif

并选择 GDNativeLibrary:

../../../_images/gdnativelibrary_resource.png

你应该看到一个上下文编辑器出现在底部面板中. 使用右下角的 "展开底部面板" 按钮将其展开到全高:

../../../_images/gdnativelibrary_editor.png

常规属性

在检查器中, 你有各种属性来控制加载库.

如果启用 Load Once , 我们的库只被加载一次, 每个使用我们库的单独脚本将使用相同的数据. 你在全局定义的任何变量都可以从你创建的对象的任何实例中访问. 如果禁用 Load Once , 那么每次脚本访问库的时候都会有一份新的库被加载到内存中.

如果 Singleton 被启用, 我们的库就会自动加载, 并调用一个叫做 godot_gdnative_singleton 的函数. 我们将把这个问题留给另一个教程.

符号前缀 是我们核心函数的前缀, 比如前面看到的 godot_ 中的 godot_nativescript_init . 如果你使用多个希望静态链接的GDNative库, 你将不得不使用不同的前缀. 这又是一个需要在单独的教程中深入探讨的问题, 目前只需要在部署到iOS时使用, 因为这个平台不喜欢动态库.

Reloadable 定义了当编辑器失去和获得焦点时, 是否应该重新加载库, 通常是为了从外部对库的任何变化中获取新的或修改的符号.

平台库

GDNativeLibrary编辑器插件可以让你为你所要支持的每个平台和架构配置两件事.

动态库 一栏(保存文件中的 entry 部分)告诉我们每个平台和特征组合需要加载哪个动态库. 这也告知导出器在向特定平台导出时需要导出哪些文件.

Dependencies 列(也称 依赖 部分)告诉Godot为了让库工作, 每个平台还需要导出哪些文件. 如果您的GDNative模块使用另一个DLL来实现第三方库的功能, 这就是您列出该DLL的地方.

在我们的例子中, 我们只构建了Linux, macOS和/或Windows的库, 所以你可以通过点击文件夹按钮在相关字段中链接它们. 如果你建立了所有三个库, 你应该有这样的东西:

../../../_images/gdnativelibrary_editor_complete.png

保存资源

然后, 我们可以通过检查器中的保存按钮将GDNativeLibrary资源保存为 bin/libsimple.gdnlib:

../../../_images/gdnativelibrary_save.png

该文件以基于文本的格式保存, 其内容应类似于以下内容:

[general]

singleton=false
load_once=true
symbol_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=[  ]

创建NativeScript (.gdns)文件

通过 .gdnlib 文件,我们已经告诉 Godot 如何加载库,现在需要告诉它关于我们的“SIMPLE”对象类。通过创建一个 NativeScript 资源文件,扩展名为 .gdns

就像对GDNativeLibrary资源所做的那样, 点击按钮在检查器中创建一个新资源, 并选择 NativeScript:

../../../_images/nativescript_resource.png

检查器将显示一些我们需要填写的属性。在 Class Name 中,输入 "SIMPLE" ,这是在调用 godot_nativescript_register_class 时在C源码中声明的对象类名称。通过点击 Library 和选择 Load 来选择我们的 .gdnlib 文件:

../../../_images/nativescript_library.png

备注

类名 必须与注册时在 godot_nativescript_init 中给出的拼写相同.

最后点击保存图标并将其另存为 bin/simple.gdns :

../../../_images/save_gdns.gif

现在是时候建立我们的场景了. 在你的场景中添加一个控件节点作为根节点, 并命名为 main . 然后添加一个按钮和一个标签作为子节点. 把它们放在屏幕上合适的地方, 并给你的按钮起个名字.

../../../_images/c_main_scene_layout.png

选择控制节点, 给它附加一个脚本:

../../../_images/add_main_script.gif

接下来将按钮上的 pressed 信号与你的脚本连接起来:

../../../_images/connect_button_signal.gif

不要忘记保存你的场景, 把它称为 main.tscn .

现在我们可以实现 main.gd 代码:

extends Control

# load the Simple library
onready var data = preload("res://bin/simple.gdns").new()

func _on_Button_pressed():
    $Label.text = "Data = " + data.get_data()

做完这一切后, 我们的项目就可以工作了. 第一次运行时,Godot会询问主场景设置, 选择 main.tscn 文件, 然后就可以了:

../../../_images/c_sample_result.png