Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
新手入门
工作流概述
作为一种 GDExtension,godot-cpp 的使用比 GDScript 和 C# 更复杂。如果你决定使用它,以下是您的工作流程概述:
创建一个新的 godot-cpp 项目(从模板创建或如下所述从零开始)。
使用你喜欢的 IDE 在本地编写代码。
使用兼容范围内的最早 Godot 版本构建和测试你的代码。
为你计划支持的所有平台创建构建版本(例如使用 GitHub Actions)。
可选:发布至 Godot 资产库。
示例项目
对于你的第一个godot-cpp项目,建议先阅读本指南了解相关技术。完成后可使用godot-cpp模板,它提供更完整的功能支持(例如 GitHub Actions 流水线和实用 SConstruct 样板代码)。该模板本身并未进行非常详细的说明,这就是我们建议你先完成本指南的原因。
设置项目
你需要具备以下几个前置条件:
一份 Godot 4 可执行文件。
一个 C++ 编译器。
使用 SCons 作为构建工具。
一份 godot-cpp 仓库的副本。
另请参阅配置 IDE和编译,因为这些构建工具与你从源码编译 Godot 所需的构建工具完全相同。
你可以从 GitHub 下载 godot-cpp 仓库,或者让 Git 为你完成这项工作。请注意,这个仓库为不同版本的 Godot 提供了不同的分支。GDExtensions 仅支持 Godot 的新版本(Godot 4 及更高版本),反之亦然,因此请确保你下载的是正确的分支。
备注
使用 GDExtension 时,需选择与目标 Godot 版本匹配的 godot-cpp 分支。例如,若目标是 Godot 4.1,那么应该使用 4.1 分支。本教程中统一使用 4.x 这一版本号,实际操作时请替换为你的具体目标版本。
master 分支是开发分支,它会定期更新以与 Godot 的 master 分支保持兼容。
警告
我们的长期目标是,确保旧版 GDExtension 能在后续版本中持续兼容,反之则不可行。例如,一个面向 Godot 4.2 的 GDExtension 应当在 Godot 4.3 中正常运作,但面向 Godot 4.3 的则不能在 Godot 4.2 中运作。
有一个例外:针对 Godot 4.0 的扩展将无法在 Godot 4.1 及更高版本中运行(参见将 GDExtension 更新到 4.1)。
如果你使用 Git 对项目进行版本控制,最好将其添加为 Git 子模块:
mkdir gdextension_cpp_example
cd gdextension_cpp_example
git init
git submodule add -b 4.x https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init
也可以直接将该项目克隆到项目文件夹内:
mkdir gdextension_cpp_example
cd gdextension_cpp_example
git clone -b 4.x https://github.com/godotengine/godot-cpp
备注
如果你决定下载仓库或将其克隆到你的文件夹中,请确保与我们这里所设置的文件夹结构相同,我们假定将在此展示的许多代码都基于这种项目结构。
如果从介绍中指定的链接克隆示例, 子模块不会自动初始化. 你需要执行以下命令:
cd gdextension_cpp_example
git submodule update --init
这会将该仓库克隆到你的项目文件夹中。
创建一个简单的插件
现在来构建一个插件。我们首先创建一个空的 Godot 项目,并在其中放入一些文件。
打开 Godot 并创建一个新项目。对于该示例,我们将其放在我们的 GDExtension 的文件夹结构中名为 project 的文件夹中。
在我们的项目中,我们将创建一个包含名为 "Main" 的节点的场景,我们将其保存为 main.tscn ,稍后我们再回到这个场景。
回到 GDExtension 模块根目录,创建一个名为 src 的子文件夹,用来放置我们的源代码文件。
在你的 GDExtension 模块中,你现在应该有 project 、 godot-cpp 和 src 这三个目录。
你的文件结构应如下所示:
gdextension_cpp_example/
|
+--project/ # game example/demo to test the extension
|
+--godot-cpp/ # C++ bindings
|
+--src/ # source code of the extension we are building
在 src 文件夹中,我们将首先为我们将要创建的 GDExtension 节点创建头文件,将其命名为 gdexample.h :
#pragma once
#include <godot_cpp/classes/sprite2d.hpp>
namespace godot {
class GDExample : public Sprite2D {
GDCLASS(GDExample, Sprite2D)
private:
double time_passed;
protected:
static void _bind_methods();
public:
GDExample();
~GDExample();
void _process(double delta) override;
};
} // namespace godot
上面的代码有些需要注意的地方:我们引入了 sprite2d.hpp,其中包含 Sprite2D 类的绑定。我们将在我们的模块中扩展这个类。
我们使用命名空间 godot,因为 GDExtension 中的所有内容都在此命名空间中定义。
然后是类定义,它通过一个容器类继承 Sprite2D。我们稍后会看到这样做的副作用。GDCLASS 宏为我们设置了一些内部内容。
之后, 我们声明一个名为 time_passed 的成员变量.
在接下来的代码块中,我们已经定义了构造函数和析构函数,但还有另外两个函数可能会让一些人感到熟悉,以及一个全新的方法。
第一个是 _bind_methods,这是一个静态函数,Godot 会调用它来了解可以调用哪些方法以及它暴露了哪些属性。第二个是我们的 _process 函数,它的工作方式与你在 GDScript 中熟悉的 _process 函数完全相同。
接下来,让我们通过创建 gdexample.cpp 文件来实现我们的函数:
#include "gdexample.h"
#include <godot_cpp/core/class_db.hpp>
using namespace godot;
void GDExample::_bind_methods() {
}
GDExample::GDExample() {
// Initialize any variables here.
time_passed = 0.0;
}
GDExample::~GDExample() {
// Add your cleanup here.
}
void GDExample::_process(double 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);
}
这一步应该非常直截了当。我们在实现头文件中定义的类中的每个方法。
注意 _process 函数,它用于记录经过的时间,并利用正弦和余弦函数计算精灵的新位置。
我们的 GDExtension 插件可以包含多个类,每个都有各自的头文件和源文件(正如我们上面实现的 GDExample)。在此,我们还需要新增一个名为 register_types.cpp 的 C++ 文件,用来向 Godot 注册我们 GDExtension 插件中的所有类。
#include "register_types.h"
#include "gdexample.h"
#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>
using namespace godot;
void initialize_example_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
GDREGISTER_CLASS(GDExample);
}
void uninitialize_example_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
}
extern "C" {
// Initialization.
GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);
init_obj.register_initializer(initialize_example_module);
init_obj.register_terminator(uninitialize_example_module);
init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);
return init_obj.init();
}
}
initialize_example_module 和 uninitialize_example_module 函数分别在 Godot 加载我们的插件和卸载插件时被调用。我们在这里做的只是遍历绑定模块中的函数并初始化它们,但根据需要,你可能需要设置更多内容。我们为库中的每个类都调用 GDREGISTER_CLASS 宏。
备注
你可以在 Object 类 中找到关于 GDREGISTER_CLASS(及其替代方法)的相关信息。
核心在于第三个函数 godot_nativescript_init。我们首先调用绑定库中的一个函数来创建初始化对象,该对象负责注册 GDExtension 的初始化与终止函数,同时设置初始化层级(核心层、服务器层、场景层、编辑器层)。
最后,我们需要为 register_types.cpp 创建一个头文件,命名为 register_types.h。
#pragma once
#include <godot_cpp/core/class_db.hpp>
using namespace godot;
void initialize_example_module(ModuleInitializationLevel p_level);
void uninitialize_example_module(ModuleInitializationLevel p_level);
编译插件
为了编译项目,我们需要通过一个 SConstruct 文件来定义 SCons 的编译规则,该文件需引用 godot-cpp 目录中的同名文件。从零编写构建文件不在本教程范围内。你可以直接下载我们准备的 SConstruct 文件。我们将在后续教程中深入讲解如何自定义这些构建文件。
备注
这个 SConstruct 文件是针对最新的 godot-cpp 主分支编写的,用于更早版本的话可能需要略微进行一些修改,也可以参考 Godot 4.x 文档中的 SConstruct 文件。
下载好 SConstruct 文件后,将其放在你的 GDExtension 目录中和 godot-cpp、src 和 demo 平级的位置,然后运行:
scons platform=<platform>
如果你正在当前使用的平台上进行编译,可以省略 platform 这个选项。可用的 platform 选项列表取决于你当前配置了哪些平台的依赖项(可以输入 platform=list 来查看所有可用的平台)。更多详情请参阅 构建系统介绍 。
现在,你应该可以在 project/bin/ 目录下找到编译好的库文件了。
备注
在这里,我们把 godot-cpp 和我们的 gdexample 库都编译成了调试(debug)版本,这也是默认的编译方式。如果想要生成优化后的正式版本,你应该使用 target=template_release 这个参数来进行编译。
使用 GDExtension 模块
回到 Godot 编辑器之前,我们还需要在 project/bin/ 中再创建一个文件。
该文件用于告知 Godot 不同平台应该加载哪些动态库,并指定模块的入口函数。其文件名为 gdexample.gdextension。
[configuration]
entry_symbol = "example_library_init"
compatibility_minimum = "4.1"
reloadable = true
[libraries]
macos.debug = "./libgdexample.macos.template_debug.dylib"
macos.release = "./libgdexample.macos.template_release.dylib"
windows.debug.x86_32 = "./gdexample.windows.template_debug.x86_32.dll"
windows.release.x86_32 = "./gdexample.windows.template_release.x86_32.dll"
windows.debug.x86_64 = "./gdexample.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "./gdexample.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "./libgdexample.linux.template_debug.x86_64.so"
linux.release.x86_64 = "./libgdexample.linux.template_release.x86_64.so"
linux.debug.arm64 = "./libgdexample.linux.template_debug.arm64.so"
linux.release.arm64 = "./libgdexample.linux.template_release.arm64.so"
linux.debug.rv64 = "./libgdexample.linux.template_debug.rv64.so"
linux.release.rv64 = "./libgdexample.linux.template_release.rv64.so"
该文件包含一个 configuration 部分,用于控制模块的入口函数。你还应该用 compatability_minimum 设置兼容的最低 Godot 版本,以防止更旧版的 Godot 试图加载你的扩展。reloadable 标志用来启用扩展的自动重载功能,这将使编辑器在每次重新编译你的扩展时自动重新加载,而无需重启编辑器——此功能仅在调试模式(默认)下编译你的扩展时才有效。
libraries 部分很重要:它的作用是告诉 Godot 各个支持的平台对应的动态库在项目文件系统中的位置。因此导出项目时,也只会导出对应的文件,也就是说,数据包中不会包含与目标平台不兼容的库文件。
你可以在 .gdextension 文件 中了解更多关于 .gdextension 文件的信息。
以下是另一个检查正确文件结构的概述:
gdextension_cpp_example/
|
+--project/ # game example/demo to test the extension
| |
| +--main.tscn
| |
| +--bin/
| |
| +--gdexample.gdextension
|
+--godot-cpp/ # C++ bindings
|
+--src/ # source code of the extension we are building
| |
| +--register_types.cpp
| +--register_types.h
| +--gdexample.cpp
| +--gdexample.h
现在让我们回到 Godot。我们载入最初创建的主场景,并向场景中添加一个新出现的 GDExample 节点:
我们将把 Godot 图标设置为此节点的纹理,并禁用 centered(居中)属性:
我们终于准备好运行这个项目了:
添加属性
GDScript 允许你使用 export 关键字为你的脚本添加可编辑属性。而在 GDExtension 中,你需要为这些属性注册 getter 和 setter 函数,或者直接实现对象的 _get_property_list 、 _get 和 _set 方法(但这超出了本教程的范围)。
现在我们来添加一个属性,以便能够控制波动的振幅。
在我们的 gdexample.h 文件中,我们需要添加一个成员变量以及相应的 getter/setter 函数:
...
private:
double time_passed;
double amplitude;
public:
void set_amplitude(const double p_amplitude);
double get_amplitude() const;
...
在我们的 gdexample.cpp 文件中,我们需要进行一些更改,以下列出的只有被更改的方法,未列出的部分请勿删除。
void GDExample::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_amplitude"), &GDExample::get_amplitude);
ClassDB::bind_method(D_METHOD("set_amplitude", "p_amplitude"), &GDExample::set_amplitude);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "amplitude"), "set_amplitude", "get_amplitude");
}
GDExample::GDExample() {
// Initialize any variables here.
time_passed = 0.0;
amplitude = 10.0;
}
void GDExample::_process(double 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::set_amplitude(const double p_amplitude) {
amplitude = p_amplitude;
}
double GDExample::get_amplitude() const {
return amplitude;
}
在完成这些修改并再次编译后,就会看到界面上出现了一个新属性。现在你可以更改此属性,然后运行项目,你将看到我们的 Godot 图标沿着更大的数字移动。
让我们做同样的事情但是为了我们动画的速度并使用 setter 和 getter 函数。我们的 gdexample.h 头文件再次只需要几行代码:
...
double amplitude;
double speed;
...
void _process(double delta) override;
void set_speed(const double p_speed);
double get_speed() const;
...
这需要对我们的 gdexample.cpp 文件进行一些更改, 同样我们只显示已更改的方法, 所以不要删除我们忽略的任何内容:
void GDExample::_bind_methods() {
...
ClassDB::bind_method(D_METHOD("get_speed"), &GDExample::get_speed);
ClassDB::bind_method(D_METHOD("set_speed", "p_speed"), &GDExample::set_speed);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,20,0.01"), "set_speed", "get_speed");
}
GDExample::GDExample() {
time_passed = 0.0;
amplitude = 10.0;
speed = 1.0;
}
void GDExample::_process(double 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(const double p_speed) {
speed = p_speed;
}
double GDExample::get_speed() const {
return speed;
}
现在,当项目编译后,我们会看到另一个属性叫做 speed。改变它的值将使动画变得更快或更慢。此外,我们还添加了一个属性 range,它描述了值的范围。前两个参数是最小值和最大值,第三个是步进值。
备注
为简单起见,我们只使用了属性方法中的 hint_range。其实还有很多其他选项可供选择。这些选项可以进一步配置属性在 Godot 端的显示和设置方式。你可以在此处找到关于属性提示(property hints)的更多信息 @GlobalScope 。
信号
最后但同样重要的是,信号在 GDExtension 中也能完全正常工作。要让你的扩展(extension)响应另一个对象发出的信号,需要你在该对象上调用 connect。对于我们这个摇摆的 Godot 图标来说,暂时找不到合适的示例来展示,实际上我们需要一个更完整的示例。
这是必需的语法:
some_other_node->connect("the_signal", Callable(this, "my_method"));
为了将我们某个节点的信号 the_signal 与我们的方法 my_method 连接起来,我们需要在 connect 方法中提供信号的名称和一个 Callable。Callable 包含关于可调用方法的对象的信息。在我们的例子中,它将当前对象实例 this 与对象的 my_method 方法关联起来。然后,connect 方法会将这个连接添加到 the_signal 的观察者中。每当 the_signal 被触发时,Godot 就知道该调用哪个对象中的哪个方法。
请注意,只有在 _bind_methods 方法中注册之后才能调用 my_method。否则 Godot 无法得知 my_method 的存在。
想要进一步了解 Callable 请参考 Callable。
让对象发出信号是一种更常见的做法。对于我们的 Godot 图标摆动示例,我们将用一个简单的例子来说明其工作原理。我们计划每秒发出一个信号,并同时传递新的位置信息。
在我们的 gdexample.h 头文件中,我们需要定义一个新成员 time_emit:
...
double time_passed;
double time_emit;
double amplitude;
...
gdexample.cpp 这次的修改有点复杂。首先,你需要在我们的 _init 方法或构造函数中设置 time_emit = 0.0。另外两个修改我们将逐一查看。
在我们的 _bind_methods 方法中,我们需要声明我们的信号。按如下方式实现:
void GDExample::_bind_methods() {
...
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,20,0.01"), "set_speed", "get_speed");
ADD_SIGNAL(MethodInfo("position_changed", PropertyInfo(Variant::OBJECT, "node"), PropertyInfo(Variant::VECTOR2, "new_pos")));
}
在这里,我们的 ADD_SIGNAL 宏可以通过一个包含 MethodInfo 参数的单次调用来实现。MethodInfo 的第一个参数是信号的名称,剩下的参数是 PropertyInfo 类型,描述方法每个参数的基本信息。PropertyInfo 参数通过定义参数的数据类型,以及参数的默认名称来进行说明。
因此,在这里我们添加了一个信号,并使用 MethodInfo 来命名信号为 "position_changed"。PropertyInfo 参数描述了两个必需的参数,一个是 Object 类型,另一个是 Vector2 类型,分别命名为 "node" 和 "new_pos"。
接下来我们需要修改我们的 _process 方法:
void GDExample::_process(double 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;
}
}
经过一秒钟后, 我们发出信号并重置我们的计数器。我们可以将参数值直接添加给 emit_signal。
一旦 GDExtension 库编译完成,我们可以进入 Godot 并选择我们的精灵节点。在节点面板中,我们可以找到我们新建的信号,并通过点击连接按钮或双击信号来进行连接。我们在主节点上添加了一个脚本并实现了这样的信号:
extends Node
func _on_Sprite2D_position_changed(node, new_pos):
print("The position of " + node.get_class() + " is now " + str(new_pos))
每一秒,我们都会将我们的位置输出到控制台。
下一步
我们希望上面的示例能帮助你理解基本概念。你可以在此基础上扩展,创建完整的脚本,使用 C++ 控制 Godot 中的节点!
与其基于上面的示例来搭建项目,我们建议现在重新开始,通过克隆 godot-cpp-template 仓库,并以此为基础来创建项目。该模板对功能支持更完善,例如包括 GitHub 构建工作流以及额外有用的 SConstruct 模板代码。