入门
工作流概述
作为一种 GDExtension,godot-cpp 的使用比 GDScript 和 C# 更复杂。如果你决定使用它,以下是您的工作流程概述:
创建一个新的 godot-cpp 项目(从模板创建或从零开始,如下所述)。
使用你喜欢的 IDE 在本地编写代码。
使用兼容范围内的最早 Godot 版本构建和测试你的代码。
Create builds for all platforms you want to support (e.g. using 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 及后续版本中运行(参见 为 Godot 4.1 更新您的 GDExtension)。
如果你使用 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 项目,并在其中放入一些文件。
Open Godot and create a new project. For this example, we will place it in a
folder called project inside our GDExtension's folder structure.
In our project, we'll create a scene containing a Node called "Main" and
we'll save it as main.tscn. We'll come back to that later.
回到 GDExtension 模块根目录,创建一个名为 src 的子文件夹,用来放置我们的源代码文件。
You should now have project, godot-cpp, and src
directories in your GDExtension module.
你的文件结构应如下所示:
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();
}
}
The initialize_example_module and uninitialize_example_module functions get
called respectively when Godot loads our plugin and when it unloads it. All
we're doing here is parse through the functions in our bindings module to
initialize them, but you might have to set up more things depending on your
needs. We call the GDREGISTER_CLASS macro for each of our classes in our library.
备注
You can find information about GDREGISTER_CLASS (and alternatives) at Object 类.
核心在于第三个函数 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 文件。
Once you've downloaded the SConstruct file, place it in your GDExtension folder
structure alongside godot-cpp, src, and project, then run:
scons platform=<platform>
You should now be able to find the module in project/bin/<platform>.
备注
这里我们将 godot-cpp 和 gdexample 库都编译为了调试构建。需要优化构建的话,编译时请使用 target=template_release 开关。
使用 GDExtension 模块
Before we jump back into Godot, we need to create one more file in
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 各个支持的平台对应的动态库在项目文件系统中的位置。因此导出项目时,也只会导出对应的文件,也就是说,数据包中不会包含与目标平台不兼容的库文件。
You can learn more about .gdextension files at .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 allows you to add properties to your script using the export
keyword. In GDExtension you have to register the properties with a getter and
setter function or directly implement the _get_property_list, _get and
_set methods of an object (but that goes far beyond the scope of this
tutorial).
Lets add a property that allows us to control the amplitude of our wave.
In our gdexample.h file we need to add a member variable and getter and setter
functions:
...
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;
}
Now when the project is compiled, we'll see another property called speed. Changing its value will make the animation go faster or slower. Furthermore, we added a property range which describes in which range the value can be. The first two arguments are the minimum and maximum value and the third is the step size.
备注
For simplicity, we've only used the hint_range of the property method. There are a lot more options to choose from. These can be used to further configure how properties are displayed and set on the Godot side.
信号
Last but not least, signals fully work in GDExtension as well. Having your extension
react to a signal given out by another object requires you to call connect
on that object. We can't think of a good example for our wobbling Godot icon, we
would need to showcase a far more complete example.
这是必需的语法:
some_other_node->connect("the_signal", Callable(this, "my_method"));
To connect our signal the_signal from some other node with our method
my_method, we need to provide the connect method with the name of the signal
and a Callable. The Callable holds information about an object on which a method
can be called. In our case, it associates our current object instance this with the
method my_method of the object. Then the connect method will add this to the
observers of the_signal. Whenever the_signal is now emitted, Godot knows which
method of which object it needs to call.
请注意,只有在 _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。另外两个修改我们将逐一查看。
In our _bind_methods method, we need to declare our signal. This is done
as follows:
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 参数通过定义参数的数据类型,以及参数的默认名称来进行说明。
So here, we add a signal, with a MethodInfo which names the signal "position_changed". The
PropertyInfo parameters describe two essential arguments, one of type Object, the other
of type Vector2, respectively named "node" and "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。
Once the GDExtension library is compiled, we can go into Godot and select our sprite node. In the Node dock, we can find our new signal and link it up by pressing the Connect button or double-clicking the signal. We've added a script on our main node and implemented our signal like this:
extends Node
func _on_Sprite2D_position_changed(node, new_pos):
print("The position of " + node.get_class() + " is now " + str(new_pos))
每一秒,我们都会将我们的位置输出到控制台。
下一步
We hope the above example showed you the basics. You can build upon this example to create full-fledged scripts to control nodes in Godot using C++!
Instead of basing your project off the above example setup, we recommend to restart now by cloning the
godot-cpp template, and base your project off of that.
It has better coverage of features, such as a GitHub build action and additional useful SConstruct boilerplate.