GDNative C++示例

简介

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

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

Godot 3.1看到了NativeScript 1.1新增功能的引入, 使GDNative团队能够构建更好的C++绑定库. 这些变化现已合并到主分支中, 并将成为我们前进的方式. 如果您想编写一个也支持Godot 3.0的C++ GDNative插件, 您需要使用3.0分支和NativeScript 1.0语法. 我们将在这篇文章中并排展示它们.

你可以在 on GitHub <https://github.com/BastiaanOlij/gdnative_cpp_example> __上下载我们将在本教程中创建的完整例子.

设置项目

您需要一些先决条件:

  • Godot 3.x可执行文件,

  • 一个C++编译器,

  • SCons作为构建工具,

  • godot-cpp repository 的副本.

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

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

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

mkdir gdnative_cpp_example
cd gdnative_cpp_example
git init
git submodule add https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init
mkdir gdnative_cpp_example
cd gdnative_cpp_example
git init
git submodule add -b 3.0 https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init

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

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

mkdir gdnative_cpp_example
cd gdnative_cpp_example
git clone --recursive https://github.com/godotengine/godot-cpp
mkdir gdnative_cpp_example
cd gdnative_cpp_example
git clone --recursive -b 3.0 https://github.com/godotengine/godot-cpp

注解

godot-cpp 现在包含 godot_headers 作为嵌套子模块, 如果您手动下载它们, 请确保将 godot_headers 放在 godot-cpp 文件夹中.

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

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

cd gdnative_cpp_example
git submodule update --init --recursive

这会将这两个存储库克隆到您的项目文件夹中.

构建C++绑定

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

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

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个线程.

cd godot-cpp
scons platform=<platform> generate_bindings=yes -j4
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 :

#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H

#include <Godot.hpp>
#include <Sprite.hpp>

namespace godot {

class GDExample : public Sprite {
    GODOT_CLASS(GDExample, Sprite)

private:
    float time_passed;

public:
    static void _register_methods();

    GDExample();
    ~GDExample();

    void _init(); // our initializer called by Godot

    void _process(float delta);
};

}

#endif
#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H

#include <Godot.hpp>
#include <Sprite.hpp>

namespace godot {

class GDExample : public godot::GodotScript<Sprite> {
    GODOT_CLASS(GDExample)

private:
    float time_passed;

public:
    static void _register_methods();

    GDExample();
    ~GDExample();

    void _process(float delta);
};

}

#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 文件来实现我们的函数:

#include "gdexample.h"

using namespace godot;

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
}

GDExample::GDExample() {
}

GDExample::~GDExample() {
    // add your cleanup here
}

void GDExample::_init() {
    // initialize any variables here
    time_passed = 0.0;
}

void GDExample::_process(float delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(10.0 + (10.0 * sin(time_passed * 2.0)), 10.0 + (10.0 * cos(time_passed * 1.5)));

    set_position(new_position);
}
#include "gdexample.h"

using namespace godot;

void GDExample::_register_methods() {
    register_method((char *)"_process", &GDExample::_process);
}

GDExample::GDExample() {
    // Initialize any variables here
    time_passed = 0.0;
}

GDExample::~GDExample() {
    // Add your cleanup procedure here
}

void GDExample::_process(float delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(10.0 + (10.0 * sin(time_passed * 2.0)), 10.0 + (10.0 * cos(time_passed * 1.5)));

    owner->set_position(new_position);
}

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

另一种注意方法是我们的 _process 函数, 它只是跟踪已经过了多少时间, 并使用简单的正弦和余弦函数计算我们的精灵的新位置. 突出的是调用 owner-> set_position 来调用我们的Sprite的一个内置方法. 这是因为我们的类是一个容器类; owner 指向我们脚本所涉及的实际Sprite节点. 在即将发布的NativeScript 1.1中, 可以在我们的类上直接调用 set_position .

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

#include "gdexample.h"

extern "C" void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *o) {
    godot::Godot::gdnative_init(o);
}

extern "C" void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *o) {
    godot::Godot::gdnative_terminate(o);
}

extern "C" void GDN_EXPORT godot_nativescript_init(void *handle) {
    godot::Godot::nativescript_init(handle);

    godot::register_class<godot::GDExample>();
}

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

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

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

编译插件

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

注解

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

一旦您下载了 SConstruct 文件, 将其放在 godot-cpp , godot_headersdemo 旁边的GDNative模块文件夹中, 然后运行:

scons platform=<platform>

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

注解

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

使用GDNative模块

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

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

[general]

singleton=false
load_once=true
symbol_prefix="godot_"
reloadable=false

[entry]

X11.64="res://bin/x11/libgdexample.so"
Windows.64="res://bin/win64/libgdexample.dll"
OSX.64="res://bin/osx/libgdexample.dylib"

[dependencies]

X11.64=[]
Windows.64=[]
OSX.64=[]

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

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

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

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

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

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

[gd_resource type="NativeScript" load_steps=2 format=2]

[ext_resource path="res://bin/gdexample.gdnlib" type="GDNativeLibrary" id=1]

[resource]

resource_name = "gdexample"
class_name = "GDExample"
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 文件中, 我们只需添加一个成员变量, 如下所示:

...
private:
    float time_passed;
    float amplitude;
...

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

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
}

void GDExample::_init() {
    // initialize any variables here
    time_passed = 0.0;
    amplitude = 10.0;
}

void GDExample::_process(float delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);
}
void GDExample::_register_methods() {
    register_method((char *)"_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
}

GDExample::GDExample() {
    // initialize any variables here
    time_passed = 0.0;
    amplitude = 10.0;
}

void GDExample::_process(float delta) {
    time_passed += delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    owner->set_position(new_position);
}

一旦您使用这些更改编译模块, 您将看到已将属性添加到我们的界面. 您现在可以更改此属性, 当您运行项目时, 您将看到我们的Godot图标沿着更大的数字移动.

注解

"gdexample.gdnlib" 文件中的 "可重新读取 "(reloadable)属性必须设定为" 真",Godot编辑器才能自动获取到新添加的属性.

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

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

...
    float amplitude;
    float speed;
...
    void _process(float delta);
    void set_speed(float p_speed);
    float get_speed();
...

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

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
    register_property<GDExample, float>("speed", &GDExample::set_speed, &GDExample::get_speed, 1.0);
}

void GDExample::_init() {
    // initialize any variables here
    time_passed = 0.0;
    amplitude = 10.0;
    speed = 1.0;
}

void GDExample::_process(float delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);
}

void GDExample::set_speed(float p_speed) {
    speed = p_speed;
}

float GDExample::get_speed() {
    return speed;
}
void GDExample::_register_methods() {
    register_method((char *)"_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
    register_property<GDExample, float>("speed", &GDExample::set_speed, &GDExample::get_speed, 1.0);
}

GDExample::GDExample() {
    // initialize any variables here
    time_passed = 0.0;
    amplitude = 10.0;
    speed = 1.0;
}

void GDExample::_process(float delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    owner->set_position(new_position);
}

void GDExample::set_speed(float p_speed) {
    speed = p_speed;
}

float GDExample::get_speed() {
    return speed;
}

现在, 当编译项目时, 我们将看到另一个名为speed的属性. 更改其值将使动画更快或更慢.

对于这个示例, 使用setter和getter没有明显的优势. 这只是更多的代码编写. 对于一个简单的示例, 如果您想对改变的变量做出反应, 那么设置器可能有充分的理由, 但在很多情况下只需绑定变量就足够了.

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

注解

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

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

信号

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

但这是必需的语法:

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

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

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

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

...
    float time_passed;
    float time_emit;
    float amplitude;
...

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

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

void GDExample::_register_methods() {
    register_method("_process", &GDExample::_process);
    register_property<GDExample, float>("amplitude", &GDExample::amplitude, 10.0);
    register_property<GDExample, float>("speed", &GDExample::set_speed, &GDExample::get_speed, 1.0);

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

    Dictionary args;
    args[Variant("node")] = Variant(Variant::OBJECT);
    args[Variant("new_pos")] = Variant(Variant::VECTOR2);
    register_signal<GDExample>((char *)"position_changed", args);
}

在这里我们看到了最新版本的godot-cpp的一个很好的改进, 其中我们的 register_signal 方法可以是一个单独的调用, 首先取信号名称, 然后有一对值来指定参数名称和我们将与这个信号一起发送的参数类型.

对于NativeScript 1.0, 我们首先构建一个字典, 在其中我们告诉Godot我们将传递给信号的参数类型, 然后注册它.

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

void GDExample::_process(float delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    set_position(new_position);

    time_emit += delta;
    if (time_emit > 1.0) {
        emit_signal("position_changed", this, new_position);

        time_emit = 0.0;
    }
}
void GDExample::_process(float delta) {
    time_passed += speed * delta;

    Vector2 new_position = Vector2(
        amplitude + (amplitude * sin(time_passed * 2.0)),
        amplitude + (amplitude * cos(time_passed * 1.5))
    );

    owner->set_position(new_position);

    time_emit += delta;
    if (time_emit > 1.0) {
        Array args;
        args.push_back(Variant(owner));
        args.push_back(Variant(new_position));
        owner->emit_signal("position_changed", args);

        time_emit = 0.0;
    }
}

经过一秒钟后, 我们发出信号并重置我们的计数器. 再次在新版本的godot-cpp中, 我们可以将参数值直接添加到 emit_signal . 在NativeScript 1.0中我们首先构建一个值数组, 然后调用 emit_signal .

编译完成后, 我们可以进入Godot并选择我们的精灵节点. 在我们的 Node 选项卡上, 我们找到了我们的新信号并通过按连接将其链接起来. 我们在主节点上添加了一个脚本并实现了这样的信号:

extends Node

func _on_Sprite_position_changed(node, new_pos):
    print("The position of " + node.name + " is now " + str(new_pos))

每一秒我们只需将我们的位置输出到控制台.

原生脚本1.1与原生脚本1.0

到目前为止, 在上面的示例中, 旧语法和新语法之间似乎没有太大区别. 该类的定义略有不同, 我们不再使用 owner 成员来调用对象Godot一侧的方法. 很多改进都隐藏在引擎盖下.

此示例仅处理简单变量和简单方法. 特别是一旦开始传递对其他对象的引用, 或者当您开始调用需要更复杂参数的方法时,NativeScript 1.1确实开始显示它的好处.

下一步

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

在Godot编辑器保持打开状态时, 您应该能够编辑和重新编译插件; 在库完成构建后重新运行项目.