GDNative C示例

介绍

本教程将向您介绍创建GDNative模块所需的最低限度。这应该是你进入GDNative世界的起点。理解本教程的内容将有助于您理解在此之后将要发生的一切。

在开始之前,您可以将源代码下载到下面描述的示例对象 GDNative-demos repository .

这个示例项目还包含一个sconstruct文件,它使编译变得更加容易,但是在本教程中,我们将手工操作来理解这个过程。

GDNative 可以使用诸如 PluginScriptARVRInterfaceGDNative . 在本教程中,我们将学习如何创建 NativeScript 模块。NATScript允许您以类似的方式编写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应用商店安装Xcode。在Windows上,您可以使用Visual Studio 2015或更高版本,或MingW-W64。

  3. 的Git克隆 godot_headers repository :这些是Godot的公共API的C头文件,这些文件公开给了gdnative。

对于后者,我们建议您为此gd本机示例项目创建一个专用文件夹,打开该文件夹中的一个终端并执行:

git clone https://github.com/GodotNativeTools/godot_headers

这将把所需的文件下载到该文件夹中。

小技巧

如果您计划将Git用于GDNative项目,还可以添加 godot_headers 作为Git子模块。

注解

这个 godot_headers 存储库有不同的分支。随着Godot的发展,GDNative也在发展。当我们试图保持版本之间的兼容性时,您应该始终根据与godot稳定分支(例如 3.1 )以及理想的实际释放(例如 3.1.1-stable )你用的。基于旧版本的godot头构建的gdantive模块 may 使用较新版本的引擎,但不能反过来使用。

这个 master 分公司 godot_headers 存储库与 master godot的分支,因此包含GDNative类和结构定义,这些定义将与最新的开发构建一起使用。

如果您想为稳定版本的godot编写一个gdantive模块,请查看可用的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 .

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

我们的C源

让我们从编写主代码开始。最后,我们希望最终得到一个文件结构,它沿着这些行进行查找:

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

打开Godot并创建一个名为“Simple”的新项目 godot_headers Git克隆。这将创建 simple 文件夹和 project.godot 文件。然后手动创建 binsrc 此文件夹中的子文件夹。

我们先看看 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;

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

为了使这个实现模块化并易于扩展,核心函数可以通过“核心”API结构直接使用,但是其他函数有自己的“gdnive结构”,可以通过扩展来访问。

在我们的示例中,我们访问其中一个扩展来访问本机脚本所需的特定函数。

本地脚本的行为与godot中的任何其他脚本类似。由于NativeScript API的级别较低,因此它要求库比其他脚本系统(如gdscript)更详细地指定许多内容。创建本机脚本实例时,将调用库给定的构造函数。当该实例被销毁时,将执行给定的析构函数。

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调用,作为加载gdantive库的一部分,并将我们提供的对象传回引擎。

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 对于我们班的每个方法。在我们的例子中,这只是 get_data . 我们的第一个参数又是句柄指针。第二个是我们正在注册的对象类的名称。第三个是gdscript所知道的函数名。第四个是我们的属性设置(参见 godot_method_rpc_mode 枚举 godot_headers/nativescript/godot_nativescript.h 对于可能的值)。第五个也是最后一个参数是当方法被调用时要调用哪个函数的描述。

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

现在,是时候开始研究对象的函数了。首先,我们定义了一个结构,用于存储gdantive类实例的成员数据。

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_TYPENAME_destroy . 当使用字符串创建变量时,它引用该字符串。这意味着原始字符串可以“销毁”以减少引用计数。如果没有发生这种情况,字符串内存将泄漏,因为引用计数永远不会为零,内存也永远不会释放。返回的变体会被godot自动销毁。

注解

在更复杂的操作中,可能会混淆需要解除分配哪些值,哪些不需要解除分配。一般规则:呼叫 godot_TYPENAME_destroy 当调用C++析构函数时。在创建了变量之后,将在C++中调用字符串析构函数,因此在C.也一样。

我们返回的变体会被Godot自动销毁。

这就是我们模块的全部源代码。

编译

我们现在需要编译源代码。如前所述,我们在Github上的示例项目包含一个scons配置,它可以为您完成所有的工作,但是对于我们的教程,我们将直接调用编译器。

假设您坚持以上建议的文件夹结构,则最好在 src 文件夹并从中执行命令。确保创建 bin 文件夹。

Linux上:

gcc -std=c11 -fPIC -c -I../../godot_headers simple.c -o simple.os
gcc -shared simple.os -o ../bin/libsimple.so

在MacOS上:

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

在Windows上:

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

注解

在Windows版本中,您还可以使用 libsimple.lib 类库。这是一个库,您可以编译成一个项目来提供对DLL的访问。我们将其作为副产品获得,我们不需要它:)当导出游戏以发布时,此文件将被忽略。

小技巧

如果添加空白 .gdignore 文件发送至 src 文件夹中,godot不会尝试导入编译器生成的文件。这在Windows上是必需的被编译对象有 .obj 扩展,这也是引擎支持的三维模型格式。

创建gdnativelibrary (.gdnlib 文件

通过编译模块,我们现在需要创建一个 GDNativeLibrary 资源 .gdnlib 我们把它放在动态库旁边。这个文件告诉Godot哪些动态库是模块的一部分,需要在每个平台上加载。

我们可以使用godot来生成这个文件,所以在编辑器中打开“简单”项目。

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

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

然后选择 GDNativeLibrary

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

您应该会在底部面板中看到一个上下文编辑器。使用右下角的“展开底部面板”按钮将其展开到全高:

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

常规属性

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

如果 加载一次 启用后,库只加载一次,使用库的每个脚本将使用相同的数据。全局定义的任何变量都可以从创建的对象的任何实例访问。如果 加载一次 如果禁用,则每次脚本访问库时都会将库的新副本加载到内存中。

如果 单子 如果启用,则会自动加载库并调用 godot_singleton_init 被调用。我们将把它留给另一个教程。

这个 符号前缀 是我们核心功能的前缀,例如 godot_ 在里面 godot_nativescript_init 见前面。如果您使用希望静态链接的多个gdantive库,则必须使用不同的前缀。在单独的教程中,这也是一个需要深入研究的主题,因为这个平台不喜欢动态库,所以此时只需要部署到iOS。

可重新加载 定义当编辑器失去焦点并获得焦点时是否应重新加载库,通常是从外部对库所做的任何更改中提取新的或修改过的符号。

平台库

gdnativelibrary编辑器插件允许您为要支持的每个平台和体系结构配置两件事情。

这个 动态库 柱 (entry 保存文件中的部分)告诉我们每个平台和功能组合必须加载哪个动态库。这还将通知导出程序在导出到特定平台时需要导出哪些文件。

这个 依赖关系 列(也 dependencies 部分)告诉Godot为使我们的库工作,需要为每个平台导出哪些其他文件。假设您的gdantive模块使用另一个dll来实现来自第三方库的功能,这就是您列出该dll的地方。

对于我们的示例,我们只为Linux、MacOS和/或Windows构建库,因此您可以通过单击文件夹按钮在相关字段中链接它们。如果您构建了这三个库,那么您应该具有如下功能:

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

保存资源

然后我们可以将gdnativelibrary资源另存为 bin/libsimple.gdnlib 使用Inspector中的“保存”按钮:

../../../_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=[  ]

创建本机脚本 (.gdns 文件

和我们一起 .gdnlib 文件我们已经告诉了Godot如何加载我们的库,现在我们需要告诉它关于我们的“简单”对象类。我们通过创建 NativeScript 资源文件 .gdns 延伸。

与gdnativelibrary资源相同,单击按钮在检查器中创建一个新资源并选择 NativeScript

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

检查员将显示一些我们需要填充的属性。AS 类名 我们输入“simple”,这是我们在调用时在C源代码中声明的对象类名。 godot_nativescript_register_class . 我们还需要选择 .gdnlib 单击文件 类库 和选择 Load

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

最后单击保存图标并将其另存为 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()

在这之后,我们的项目应该可以工作了。当你第一次运行它的时候,戈多会问你你的主要场景是什么,然后你选择你的 main.tscn 文件和预存:

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