GDNative C++ 示例

前言

本教程建立在 GDNative C example 中给出的信息之上, 因此我们强烈建议您先阅读.

GDNative的C++绑定构建在NativeScript GDNative API之上, 并提供了一种使用C++在Godot中 “扩展” 节点的更好方法. 这相当于在GDScript中编写脚本, 而是在C++中编写脚本.

你可以在 GitHub 上下载我们将在本教程中创建的完整例子。

设置项目

您需要一些先决条件:

  • Godot 3.x 可执行文件,

  • C++ 编译器,

  • SCons 作为构建工具,

  • godot-cpp 仓库的副本。

另请参阅《编译》,因为构建工具与从源代码编译 Godot 所需的构建工具相同。

您可以从 GitHub 下载这些仓库,或让 Git 为您完成这些工作。请注意,这些仓库现在对于不同版本的 Godot 具有不同的分支。为早期版本的 Godot 编写的 GDNative 模块可以在较新版本中运行(除了 3.0 和 3.1 之间的 ARVR 接口的一次重大更改),但反之则不行,因此请确保下载正确的分支。还要注意,您用于生成 api.json 的 Godot 版本将成为最低版本。

备注

GDExtension 已经被合并到 godot-cpp 的 master 分支中,但它只与即将推出的 Godot 4.0 兼容。因此,您需要使用 GDNative ,需使用 godot-cpp 的 3.x 分支,并按照此示例进行操作。

本教程只会涉及 Godot 3.x 中的 GDNative,不会涉及 Godot 4.0 中的 GDExtension。

如果您使用 Git 对项目进行版本控制,最好将它们添加为 Git 子模块:

  1. mkdir gdnative_cpp_example
  2. cd gdnative_cpp_example
  3. git init
  4. git submodule add -b 3.x https://github.com/godotengine/godot-cpp
  5. cd godot-cpp
  6. git submodule update --init

如果您决定只下载仓库或将它们克隆到项目文件夹中,请确保文件夹布局与此处描述的相同,因为我们将在此处展示的代码假定项目遵循此布局。

确保克隆递归以拉入两个仓库:

  1. mkdir gdnative_cpp_example
  2. cd gdnative_cpp_example
  3. git clone --recursive -b 3.x https://github.com/godotengine/godot-cpp

备注

godot-cpp 现在包含 godot-headers 作为嵌套的子模块,如果你手动下载它们,请确保将 godot-headers 放在 godot-cpp 文件夹内。

这样做不是必须的,但我们发现这样最容易管理。如果您决定只下载仓库或只是将它们克隆到您的文件夹中,请确保文件夹布局与我们在此处设置的相同。我们接下来将展示的代码,很多都假定项目具有这样的布局。

如果从介绍中指定的链接克隆示例, 子模块不会自动初始化. 您需要执行以下命令:

  1. cd gdnative_cpp_example
  2. git submodule update --init --recursive

这会将这两个仓库克隆到您的项目文件夹中。

构建 C++ 绑定

现在我们已经下载了我们的先决条件, 现在是构建C++绑定的时候了.

仓库包含当前 Godot 版本的元数据副本,但如果您需要为较新版本的 Godot 构建这些绑定,只需调用 Godot 可执行文件:

  1. godot --gdnative-generate-json-api api.json

将生成的 api.json 文件放在项目文件夹中, 并将 use_custom_api_file = yes custom_api_file = .. / api.json 添加到下面的scons命令中.

要生成和编译绑定, 使用这个命令(根据你的操作系统, 用 windows , linuxosx 代替 <platform> ):

为了加快编译速度, 在SCons命令行的末尾添加 -jN, 其中 N 是你系统中的CPU线程数. 下面的例子使用了4个线程.

  1. cd godot-cpp
  2. scons platform=<platform> generate_bindings=yes -j4
  3. cd ..

这一步将需要一段时间. 完成后, 您应该有一个静态库, 可以编译到您的项目中, 存储在 godot-cpp / bin / 中.

备注

您可能需要在 Windows 或 Linux 的命令行中添加 bits=64

创建一个简单的插件

现在是构建实际插件的时候了. 我们首先创建一个空的Godot项目, 我们将在其中放置一些文件.

打开Godot并创建一个新项目. 对于这个示例, 我们将它放在我们的GDNative模块的文件夹结构中名为 demo 的文件夹中.

在我们的演示项目中, 我们将创建一个包含名为 “Main” 的节点的场景, 我们将其保存为 main.tscn . 我们稍后再回过头来看看.

回到顶级GDNative模块文件夹, 我们还将创建一个名为 src 的子文件夹, 我们将在其中放置源文件.

您现在应该在您的GDNative模块中有 demo , godot-cpp, godot_headerssrc 目录.

src 文件夹中, 我们将首先为我们将要创建的GDNative节点创建头文件. 我们将它命名为 gdexample.h :

  1. #ifndef GDEXAMPLE_H
  2. #define GDEXAMPLE_H
  3. #include <Godot.hpp>
  4. #include <Sprite.hpp>
  5. namespace godot {
  6. class GDExample : public Sprite {
  7. GODOT_CLASS(GDExample, Sprite)
  8. private:
  9. float time_passed;
  10. public:
  11. static void _register_methods();
  12. GDExample();
  13. ~GDExample();
  14. void _init(); // our initializer called by Godot
  15. void _process(float delta);
  16. };
  17. }
  18. #endif

以上有一些注意事项. 我们包括 Godot.hpp , 其中包含我们所有的基本定义. 之后, 我们包含 Sprite.hpp , 它包含对Sprite类的绑定. 我们将在我们的模块中扩展这个类.

我们使用命名空间 godot , 因为GDNative中的所有内容都在此命名空间中定义.

然后我们有了我们的类定义, 它通过容器类从我们的Sprite继承. 我们稍后会看到一些副作用. 这也是NativeScript 1.1中将要改进的主要部分. GODOT_CLASS 宏为我们设置了一些内部事物.

之后, 我们声明一个名为 time_passed 的成员变量.

在下一个块中我们定义了我们的方法, 我们显然已经定义了构造函数和析构函数, 但是还有其他两个函数可能看起来很熟悉.

第一个是 _register_methods , 这是一个静态函数,Godot将调用它来找出可以在我们的NativeScript上调用哪些方法以及它暴露的属性. 第二个是我们的 _process 函数, 它与您在GDScript中习惯的 _process 函数完全相同. 第三个是我们的 _init 函数, 它是在Godot正确设置我们的对象之后调用的. 即使您没有在其中放置任何代码, 它也必须存在.

所以, 让我们通过创建 gdexample.cpp 文件来实现我们的函数:

  1. #include "gdexample.h"
  2. using namespace godot;
  3. void GDExample::_register_methods() {
  4. register_method("_process", &GDExample::_process);
  5. }
  6. GDExample::GDExample() {
  7. }
  8. GDExample::~GDExample() {
  9. // add your cleanup here
  10. }
  11. void GDExample::_init() {
  12. // initialize any variables here
  13. time_passed = 0.0;
  14. }
  15. void GDExample::_process(float delta) {
  16. time_passed += delta;
  17. Vector2 new_position = Vector2(10.0 + (10.0 * sin(time_passed * 2.0)), 10.0 + (10.0 * cos(time_passed * 1.5)));
  18. set_position(new_position);
  19. }

这个应该是直截了当的. 我们正在实现我们在头文件中定义的每个类的方法. 注意 register_method 调用 必须 公开 _process 方法, 否则Godot将无法使用它. 但是, 我们不必告诉Godot我们的构造函数, 析构函数和 _init 函数.

另一个注意的方法是我们的 _process 函数,它的作用就是记录已经过了多少时间,并使用正弦和余弦函数计算我们的精灵的新位置。需要注意的是调用 owner->set_position 来调用我们的 Sprite 的一个内置方法。这是因为我们的类是一个容器类;owner 指向我们脚本所涉及的实际 Sprite 节点。

还有一个我们需要的C++文件; 我们将它命名为 gdlibrary.cpp . 我们的GDNative插件可以包含多个NativeScripts, 每个都有自己的头文件和源文件, 就像我们在上面实现了 GDExample 一样. 我们现在需要的是一小段代码, 告诉Godot我们的GDNative插件中的所有NativeScripts.

  1. #include "gdexample.h"
  2. extern "C" void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *o) {
  3. godot::Godot::gdnative_init(o);
  4. }
  5. extern "C" void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *o) {
  6. godot::Godot::gdnative_terminate(o);
  7. }
  8. extern "C" void GDN_EXPORT godot_nativescript_init(void *handle) {
  9. godot::Godot::nativescript_init(handle);
  10. godot::register_class<godot::GDExample>();
  11. }

请注意, 我们这里没有使用 godot 命名空间, 因为这里实现的三个函数需要在没有命名空间的情况下定义.

当Godot加载我们的插件并卸载它时, 分别调用 godot_gdnative_initgodot_gdnative_terminate 函数. 我们在这里所做的只是解析我们的绑定模块中的函数来初始化它们, 但您可能需要根据需要设置更多内容.

重要的功能是第三个函数叫做 godot_nativescript_init . 我们首先在我们的绑定库中调用一个函数来执行它常用的东西. 之后, 我们为库中的每个类调用函数 register_class .

编译插件

手工编写 SCons 用于构建的 SConstruct 文件并不容易。出于这个示例的目的,只需使用我们已经准备好的这个硬编码的 SConstruct 文件。我们将在后续教程中介绍如何使用这些构建文件的更可定制的详细示例。

备注

这个 SConstruct 文件被编写为与最新的godot-cpp master分支一起使用, 您可能需要使用旧版本进行小的更改, 或者参考Godot 3.0文档中的 SConstruct 文件.

一旦你下载了 SConstruct 文件,把它放在你的GDNative模块文件夹中,除了 godot-cppgodot-headersdemo 之外,然后运行:

  1. scons platform=<platform>

您现在应该能够在 demo / bin / <platform> 中找到该模块.

备注

在这里, 我们编译了godot-cpp和我们的gdexample库作为调试版本. 对于优化的构建, 您应该使用 target = release 开关编译它们.

使用 GDNative 模块

在我们跳回Godot之前, 我们需要在 demo / bin / 中再创建两个文件. 两者都可以使用Godot编辑器创建, 但直接创建它们可能会更快.

第一个是一个文件, 让Godot知道应该为每个平台加载什么动态库, 并称为 gdexample.gdnlib .

  1. [general]
  2. singleton=false
  3. load_once=true
  4. symbol_prefix="godot_"
  5. reloadable=false
  6. [entry]
  7. X11.64="res://bin/x11/libgdexample.so"
  8. Windows.64="res://bin/win64/libgdexample.dll"
  9. OSX.64="res://bin/osx/libgdexample.dylib"
  10. [dependencies]
  11. X11.64=[]
  12. Windows.64=[]
  13. OSX.64=[]

该文件包含一个 “general” 部分, 用于控制模块的加载方式. 它还包含一个前缀部分, 现在应该留在 godot_ . 如果更改此设置, 则需要重命名用作入口点的各种函数. 这是为iPhone平台添加的, 因为它不允许部署动态库, 但GDNative模块是静态链接的.

entry 部分是重要的一点: 它告诉Godot每个支持平台的项目文件系统中动态库的位置. 导出项目时, 这也将导致 导出该文件, 这意味着数据包不会包含与目标平台不兼容的库.

最后, dependencies 部分允许您命名应包含的其他动态库. 当您的GDNative插件实现其他人的库并要求您为项目提供第三方动态库时, 这一点非常重要.

如果您双击Godot中的 gdexample.gdnlib 文件, 您会看到还有更多的选项要设置:

../../../_images/gdnative_library.png

我们需要创建的第二个文件是我们添加到插件中的每个NativeScript使用的文件. 我们将它命名为 gdexample.gdns 用于我们的gdexample NativeScript.

  1. [gd_resource type="NativeScript" load_steps=2 format=2]
  2. [ext_resource path="res://bin/gdexample.gdnlib" type="GDNativeLibrary" id=1]
  3. [resource]
  4. resource_name = "gdexample"
  5. class_name = "GDExample"
  6. library = ExtResource( 1 )

这是标准的Godot资源; 您可以直接在场景中创建它, 但将其保存到文件可以更容易地在其他地方重用它. 这个资源指向我们的gdnlib文件, 因此Godot可以知道哪个动态库包含我们的NativeScript. 它还定义了 class_name , 它标识了我们想要使用的插件中的NativeScript.

是时候跳回Godot了. 我们在开始时加载我们创建的主场景, 现在为场景添加一个Sprite:

../../../_images/gdnative_cpp_nodes.png

我们要将Godot徽标指定给这个精灵作为我们的纹理, 禁用 centered 属性并将我们的 gdexample.gdns 文件拖到精灵的 script 属性中:

../../../_images/gdnative_cpp_sprite.png

我们终于准备好运行这个项目了:

../../../_images/gdnative_cpp_animated.gif

添加属性

GDScript允许您使用 export 关键字向脚本添加属性. 在GDNative中, 您必须注册属性, 有两种方法可以执行此操作. 您可以直接绑定到成员, 也可以使用setter和getter函数.

备注

还有第三种选择, 就像在GDScript中一样, 您可以直接实现一个对象的 _get_property_list , _get_set 方法, 但这远远超出了本教程的范围.

我们将从直接绑定开始检查两者. 让我们添加一个允许我们控制波浪幅度的属性.

在我们的 gdexample.h 文件中, 我们只需添加一个成员变量, 如下所示:

  1. ...
  2. private:
  3. float time_passed;
  4. float amplitude;
  5. ...

在我们的 gdexample.cpp 文件中, 我们需要进行一些更改, 我们只会显示我们最终更改的方法, 不要删除我们省略的行:

  1. void GDExample::_register_methods() {
  2. register_method("_process", &GDExample::_process);
  3. register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
  4. }
  5. void GDExample::_init() {
  6. // initialize any variables here
  7. time_passed = 0.0;
  8. amplitude = 10.0;
  9. }
  10. void GDExample::_process(float delta) {
  11. time_passed += delta;
  12. Vector2 new_position = Vector2(
  13. amplitude + (amplitude * sin(time_passed * 2.0)),
  14. amplitude + (amplitude * cos(time_passed * 1.5))
  15. );
  16. set_position(new_position);
  17. }

使用这些更改编译模块后,就会看到界面上加入了一个属性。您现在可以更改此属性,当您运行项目时,您将看到我们的 Godot 图标沿着更大的数字移动。

备注

gdexample.gdnlib 文件中的 reloadable 属性必须设定为 true,Godot 编辑器才能自动获取到新添加的属性。

但是,使用该设置时要特别小心,特别是在使用工具类时,因为编辑器可能会持有对象,然后将脚本实例附加到对象上,这些对象由 GDNative 库管理。

让我们做同样的事情但是为了我们动画的速度并使用 setter 和 getter 函数。我们的 gdexample.h 头文件再次只需要几行代码:

  1. ...
  2. float amplitude;
  3. float speed;
  4. ...
  5. void _process(float delta);
  6. void set_speed(float p_speed);
  7. float get_speed();
  8. ...

这需要对我们的 gdexample.cpp 文件进行一些更改, 同样我们只显示已更改的方法, 所以不要删除我们忽略的任何内容:

  1. void GDExample::_register_methods() {
  2. register_method("_process", &GDExample::_process);
  3. register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
  4. register_property<GDExample, float>("speed", &GDExample::set_speed, &GDExample::get_speed, 1.0);
  5. }
  6. void GDExample::_init() {
  7. // initialize any variables here
  8. time_passed = 0.0;
  9. amplitude = 10.0;
  10. speed = 1.0;
  11. }
  12. void GDExample::_process(float delta) {
  13. time_passed += speed * delta;
  14. Vector2 new_position = Vector2(
  15. amplitude + (amplitude * sin(time_passed * 2.0)),
  16. amplitude + (amplitude * cos(time_passed * 1.5))
  17. );
  18. set_position(new_position);
  19. }
  20. void GDExample::set_speed(float p_speed) {
  21. speed = p_speed;
  22. }
  23. float GDExample::get_speed() {
  24. return speed;
  25. }

编译完项目后,我们就会看到另一个名为 speed 的属性。更改其值将使动画更快或更慢。

在这个例子中,使用 setter 和 getter 并没有明显的优势。如果你想要对变量的赋值作出反应,那么使用 setter 就很合适。如果你必须要做类似的事情,绑定变量就足够了。

在需要根据对象状态做出其他选择的更复杂场景中,getter和setter变得更加有用.

备注

为简单起见,我们在 register_property<class, type> 方法调用中省略了可选的参数。这些参数有 rpc_modeusagehinthint_string。这些可用于进一步配置属性在 Godot 方面的显示和设置方式。

现代C++编译器能够推断出类和变量类型, 并允许您省略 register_property 方法的 <GDExample,float> 部分. 然而, 我们在这方面的经验好坏参半.

信号

最后但同样重要的是, 信号也完全适用于GDNative. 让模块对另一个对象发出的信号作出反应, 需要在该对象上调用 connect . 我们想不出一个摆动Godot图标的好示例, 我们需要展示一个更完整的示例.

这是必需的语法:

  1. some_other_node->connect("the_signal", this, "my_method");

请注意, 如果您之前在 _register_methods 方法中注册了它, 则只能调用 my_method .

让对象发出信号更为常见。对于我们摇摆不定的 Godot 图标,我们会做一些愚蠢的事情来展示它是如何工作的。每过一秒钟我们就会发出一个信号并传递新的位置。

在我们的 gdexample.h 头文件中,我们需要定义一个新成员 time_emit

  1. ...
  2. float time_passed;
  3. float time_emit;
  4. float amplitude;
  5. ...

gdexample.cpp 这次的修改有点复杂。首先,你需要在我们的 _init 方法或构造函数中设置 time_emit = 0.0。另外两个修改我们将逐一查看。

在我们的 _register_methods 方法中,我们需要声明我们的信号。按如下方式实现:

  1. void GDExample::_register_methods() {
  2. register_method("_process", &GDExample::_process);
  3. register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
  4. register_property<GDExample, float>("speed", &GDExample::set_speed, &GDExample::get_speed, 1.0);
  5. register_signal<GDExample>((char *)"position_changed", "node", GODOT_VARIANT_TYPE_OBJECT, "new_pos", GODOT_VARIANT_TYPE_VECTOR2);
  6. }

我们这里调用的 register_signal 方法,第一个参数是信号的名称,后续是两两一组的值,用于指定与该信号一同发送的参数名及对应的类型。

接下来我们需要修改我们的 _process 方法:

  1. void GDExample::_process(float delta) {
  2. time_passed += speed * delta;
  3. Vector2 new_position = Vector2(
  4. amplitude + (amplitude * sin(time_passed * 2.0)),
  5. amplitude + (amplitude * cos(time_passed * 1.5))
  6. );
  7. set_position(new_position);
  8. time_emit += delta;
  9. if (time_emit > 1.0) {
  10. emit_signal("position_changed", this, new_position);
  11. time_emit = 0.0;
  12. }
  13. }

经过一秒钟后, 我们发出信号并重置我们的计数器。我们可以将参数值直接添加给 emit_signal

完成 GDNative 库的编译后,我们可以进入 Godot 并选择我们的精灵节点。在节点面板上,我们可以找到我们新建的信号,并通过点按连接按钮或双击信号将其链接起来。我们在主节点上添加了一个脚本并实现了这样的信号:

  1. extends Node
  2. func _on_Sprite_position_changed(node, new_pos):
  3. print("The position of " + node.name + " is now " + str(new_pos))

每一秒,我们都会将我们的位置输出到控制台。

下一步

以上只是一个简单的示例, 但我们希望它向您展示基础知识. 您可以在此示例的基础上构建完整的脚本, 以使用C++控制Godot中的节点.

要在 Godot 编辑器打开时编辑并重新编译插件,请在完成库的构建后重新运行项目。