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语法. 我们将在这篇文章中并排展示它们. 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版本将成为最低版本.
注解
GDExtension 已经被合并到 godot-cpp 的
master分支中,但它只与即将推出的 Godot 4.0 兼容。因此,您需要使用 GDNative ,需使用 godot-cpp 的3.x分支,并按照此示例进行操作。 如果您使用Git对项目进行版本控制, 最好将它们添加为Git子模块: Godot Godot 3.0mkdir gdnative_cpp_examplecd gdnative_cpp_examplegit initgit submodule add -b 3.x https://github.com/godotengine/godot-cppcd godot-cppgit submodule update --init
如果您决定只下载存储库或将它们克隆到项目文件夹中, 请确保文件夹布局与此处描述的相同, 因为我们将在此处展示的代码假定项目遵循此布局. 确保克隆递归以拉入两个存储库: Godot Godot 3.0mkdir gdnative_cpp_examplecd gdnative_cpp_examplegit initgit submodule add -b 3.0 https://github.com/godotengine/godot-cppcd godot-cppgit submodule update --init
mkdir gdnative_cpp_examplecd gdnative_cpp_examplegit clone --recursive -b 3.x https://github.com/godotengine/godot-cpp
注解mkdir gdnative_cpp_examplecd gdnative_cpp_examplegit clone --recursive -b 3.0 https://github.com/godotengine/godot-cpp
godot-cpp现在包含godot-headers作为嵌套的子模块,如果你手动下载它们,请确保将godot-headers放在godot-cpp文件夹内。 您不必这样做, 但我们发现它最容易管理. 如果您决定只下载存储库或只是将它们克隆到您的文件夹中, 请确保文件夹布局与我们在此处设置的相同, 因为我们将在此处展示的代码假定项目具有此布局. 如果从介绍中指定的链接克隆示例, 子模块不会自动初始化. 您需要执行以下命令:
这会将这两个存储库克隆到您的项目文件夹中.cd gdnative_cpp_examplegit 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,linux或osx代替<platform>): 为了加快编译速度, 在SCons命令行的末尾添加 -jN, 其中 N 是你系统中的CPU线程数. 下面的例子使用了4个线程.cd godot-cppscons platform=<platform> generate_bindings=yes -j4cd ..
godot-cpp / bin /中. 在将来的某个时刻, 已编译的二进制文件将可用, 使此步骤可选. 注解bits=64. 我们仍在努力进行更好的自动检测.创建一个简单的插件
现在是构建实际插件的时候了. 我们首先创建一个空的Godot项目, 我们将在其中放置一些文件.demo的文件夹中.main.tscn. 我们稍后再回过头来看看.src的子文件夹, 我们将在其中放置源文件.demo,godot-cpp,godot_headers和src目录.src文件夹中, 我们将首先为我们将要创建的GDNative节点创建头文件. 我们将它命名为gdexample.h: NativeScript 1.1 NativeScript 1.0#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中的所有内容都在此命名空间中定义.GODOT_CLASS宏为我们设置了一些内部事物.time_passed的成员变量. 在下一个块中我们定义了我们的方法, 我们显然已经定义了构造函数和析构函数, 但是还有其他两个函数可能看起来很熟悉._register_methods, 这是一个静态函数,Godot将调用它来找出可以在我们的NativeScript上调用哪些方法以及它暴露的属性. 第二个是我们的_process函数, 它与您在GDScript中习惯的_process函数完全相同. 第三个是我们的_init函数, 它是在Godot正确设置我们的对象之后调用的. 即使您没有在其中放置任何代码, 它也必须存在.gdexample.cpp文件来实现我们的函数: NativeScript 1.1 NativeScript 1.0#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.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_gdnative_init和godot_gdnative_terminate函数. 我们在这里所做的只是解析我们的绑定模块中的函数来初始化它们, 但您可能需要根据需要设置更多内容.godot_nativescript_init. 我们首先在我们的绑定库中调用一个函数来执行它常用的东西. 之后, 我们为库中的每个类调用函数register_class.编译插件
SConstruct文件并不容易。出于这个示例的目的,只需使用我们已经准备好的这个硬编码的 SConstruct 文件。我们将在后续教程中介绍如何使用这些构建文件的更可定制的详细示例。 注解SConstruct文件被编写为与最新的godot-cpp master分支一起使用, 您可能需要使用旧版本进行小的更改, 或者参考Godot 3.0文档中的SConstruct文件.SConstruct文件,把它放在你的GDNative模块文件夹中,除了godot-cpp、godot-headers和demo之外,然后运行:scons platform=<platform>
demo / bin / <platform>中找到该模块. 注解target = release开关编译它们.使用GDNative模块
demo / bin /中再创建两个文件. 两者都可以使用Godot编辑器创建, 但直接创建它们可能会更快.gdexample.gdnlib.[general]singleton=falseload_once=truesymbol_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=[]
godot_. 如果更改此设置, 则需要重命名用作入口点的各种函数. 这是为iPhone平台添加的, 因为它不允许部署动态库, 但GDNative模块是静态链接的.entry部分是重要的一点: 它告诉Godot每个支持平台的项目文件系统中动态库的位置. 导出项目时, 这也将导致 仅 导出该文件, 这意味着数据包不会包含与目标平台不兼容的库.dependencies部分允许您命名应包含的其他动态库. 当您的GDNative插件实现其他人的库并要求您为项目提供第三方动态库时, 这一点非常重要.gdexample.gdnlib文件, 您会看到还有更多的选项要设置: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 )
class_name, 它标识了我们想要使用的插件中的NativeScript. 是时候跳回Godot了. 我们在开始时加载我们创建的主场景, 现在为场景添加一个Sprite:centered属性并将我们的gdexample.gdns文件拖到精灵的script属性中: 我们终于准备好运行这个项目了:添加属性
export关键字向脚本添加属性. 在GDNative中, 您必须注册属性, 有两种方法可以执行此操作. 您可以直接绑定到成员, 也可以使用setter和getter函数. 注解_get_property_list,_get和_set方法, 但这远远超出了本教程的范围. 我们将从直接绑定开始检查两者. 让我们添加一个允许我们控制波浪幅度的属性.gdexample.h文件中, 我们只需添加一个成员变量, 如下所示:...private: float time_passed; float amplitude;...
gdexample.cpp文件中, 我们需要进行一些更改, 我们只会显示我们最终更改的方法, 不要删除我们省略的行: NativeScript 1.1 NativeScript 1.0void 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);}
一旦您使用这些更改编译模块, 您将看到已将属性添加到我们的界面. 您现在可以更改此属性, 当您运行项目时, 您将看到我们的Godot图标沿着更大的数字移动. 注解 “gdexample.gdnlib” 文件中的 “可重新读取 “(reloadable)属性必须设定为” 真”,Godot编辑器才能自动获取到新添加的属性. 但是, 使用该设置时要特别小心, 特别是在使用工具类时, 因为编辑器可能会持有对象, 然后将脚本实例附加到对象上, 这些对象由GDNative库管理.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);}
gdexample.h头文件再次只需要几行代码:... float amplitude; float speed;... void _process(float delta); void set_speed(float p_speed); float get_speed();...
gdexample.cpp文件进行一些更改, 同样我们只显示已更改的方法, 所以不要删除我们忽略的任何内容: NativeScript 1.1 NativeScript 1.0void 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;}
现在, 当编译项目时, 我们将看到另一个名为speed的属性. 更改其值将使动画更快或更慢. 对于这个示例, 使用setter和getter没有明显的优势. 这只是更多的代码编写. 对于一个简单的示例, 如果您想对改变的变量做出反应, 那么设置器可能有充分的理由, 但在很多情况下只需绑定变量就足够了. 在需要根据对象状态做出其他选择的更复杂场景中,getter和setter变得更加有用. 注解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;}
rpc_mode,usage,hint和hint_string. 这些可用于进一步配置属性在Godot方面的显示和设置方式.register_property方法的<GDExample,float>部分. 然而, 我们在这方面的经验好坏参半.信号
connect. 我们想不出一个摆动Godot图标的好示例, 我们需要展示一个更完整的示例. 但这是必需的语法: NativeScript 1.1 NativeScript 1.0some_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方法中, 我们需要声明我们的信号, 我们按如下方式执行: NativeScript 1.1 NativeScript 1.0void 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);}
register_signal方法可以是一个单独的调用, 首先取信号名称, 然后有一对值来指定参数名称和我们将与这个信号一起发送的参数类型. 对于NativeScript 1.0, 我们首先构建一个字典, 在其中我们告诉Godot我们将传递给信号的参数类型, 然后注册它._process方法: NativeScript 1.1 NativeScript 1.0void 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; }}
emit_signal. 在NativeScript 1.0中我们首先构建一个值数组, 然后调用emit_signal.Node选项卡上, 我们找到了我们的新信号并通过按连接将其链接起来. 我们在主节点上添加了一个脚本并实现了这样的信号:
每一秒我们只需将我们的位置输出到控制台.extends Nodefunc _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编辑器保持打开状态时, 您应该能够编辑和重新编译插件; 在库完成构建后重新运行项目.
