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 C++ example¶
前言¶
The C++ bindings for GDExtension are built on top of the C GDExtension API and provide a nicer way to "extend" nodes and other built-in classes in Godot using C++. This new system allows the extension of Godot to nearly the same level as statically linked C++ modules.
You can download the included example in the test folder of the godot-cpp repository on GitHub.
设置项目¶
您需要一些先决条件:
a Godot 4 executable,
C++ 编译器,
SCons 作为构建工具,
godot-cpp 仓库的副本。
另请参阅《编译》,因为构建工具与从源代码编译 Godot 所需的构建工具相同。
You can download the godot-cpp repository from GitHub or let Git do the work for you. Note that this repository has different branches for different versions of Godot. GDExtensions will not work in older versions of Godot (only Godot 4 and up) and vice versa, so make sure you download the correct branch.
备注
To use GDExtension
you need to use the godot-cpp branch that matches the version of Godot that you are
targeting. For example, if you're targeting Godot 4.1, use the 4.1
branch,
which is what is shown through out this tutorial.
The master
branch is the development branch which is updated regularly
to work with Godot's master
branch.
警告
Our long-term goal is that GDExtensions targeting an earlier version of Godot will work in later minor versions, but not vice-versa. For example, a GDExtension targeting Godot 4.2 should work just fine in Godot 4.3, but one targeting Godot 4.3 won't work in Godot 4.2.
However, GDExtension is currently experimental, which means that we may break compatibility in order to fix major bugs or include critical features. For example, GDExtensions created for Godot 4.0 aren't compatible with Godot 4.1 (see Updating your GDExtension for 4.1).
If you are versioning your project using Git, it is recommended to add it as a Git submodule:
mkdir gdextension_cpp_example
cd gdextension_cpp_example
git init
git submodule add -b 4.1 https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init
Alternatively, you can also clone it to the project folder:
mkdir gdextension_cpp_example
cd gdextension_cpp_example
git clone -b 4.1 https://github.com/godotengine/godot-cpp
备注
If you decide to download the repository or clone it into your folder, make sure to keep the folder layout the same as we've setup here. Much of the code we'll be showcasing here assumes the project has this layout.
如果从介绍中指定的链接克隆示例, 子模块不会自动初始化. 您需要执行以下命令:
cd gdextension_cpp_example
git submodule update --init
This will initialize the repository in your project folder.
构建 C++ 绑定¶
现在我们已经下载了我们的先决条件, 现在是构建C++绑定的时候了.
仓库包含当前 Godot 版本的元数据副本,但如果您需要为较新版本的 Godot 构建这些绑定,只需调用 Godot 可执行文件:
godot --dump-extension-api extension_api.json
Place the resulting extension_api.json
file in the project folder and add
custom_api_file=<PATH_TO_FILE>
to the scons command
below.
To generate and compile the bindings, use this command (replacing <platform>
with windows
, linux
or macos
depending on your OS):
To speed up compilation, add -jN
at the end of the SCons command line where N
is the number of CPU threads you have on your system. The example below uses 4 threads.
cd godot-cpp
scons platform=<platform> -j4 custom_api_file=<PATH_TO_FILE>
cd ..
这一步将需要一段时间. 完成后, 您应该有一个静态库, 可以编译到您的项目中, 存储在 godot-cpp / bin /
中.
备注
您可能需要在 Windows 或 Linux 的命令行中添加 bits=64
。
创建一个简单的插件¶
现在是构建实际插件的时候了. 我们首先创建一个空的Godot项目, 我们将在其中放置一些文件.
Open Godot and create a new project. For this example, we will place it in a
folder called demo
inside our GDExtension's folder structure.
在我们的演示项目中, 我们将创建一个包含名为 "Main" 的节点的场景, 我们将其保存为 main.tscn
. 我们稍后再回过头来看看.
Back in the top-level GDExtension module folder, we're also going to create a
subfolder called src
in which we'll place our source files.
You should now have demo
, godot-cpp
, and src
directories in your GDExtension module.
Your folder structure should now look like this:
gdextension_cpp_example/
|
+--demo/ # game example/demo to test the extension
|
+--godot-cpp/ # C++ bindings
|
+--src/ # source code of the extension we are building
In the src
folder, we'll start with creating our header file for the
GDExtension node we'll be creating. We will name it gdexample.h
:
#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H
#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);
};
}
#endif
There are a few things of note to the above. We include sprite2d.hpp
which
contains bindings to the Sprite2D class. We'll be extending this class in our
module.
We're using the namespace godot
, since everything in GDExtension is defined
within this namespace.
Then we have our class definition, which inherits from our Sprite2D through a
container class. We'll see a few side effects of this later on. The
GDCLASS
macro sets up a few internal things for us.
之后, 我们声明一个名为 time_passed
的成员变量.
In the next block we're defining our methods, we have our constructor and destructor defined, but there are two other functions that will likely look familiar to some, and one new method.
The first is _bind_methods
, which is a static function that Godot will
call to find out which methods can be called and which properties it exposes.
The second is our _process
function, which will work exactly the same
as the _process
function you're used to in GDScript.
所以, 让我们通过创建 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);
}
This one should be straightforward. We're implementing each method of our class that we defined in our header file.
Note our _process
function, which keeps track of how much time has passed
and calculates a new position for our sprite using a sine and cosine function.
There is one more C++ file we need; we'll name it register_types.cpp
. Our
GDExtension plugin can contain multiple classes, each with their own header
and source file like we've implemented GDExample
up above. What we need now
is a small bit of code that tells Godot about all the classes in our
GDExtension plugin.
#include "register_types.h"
#include "gdexample.h"
#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/godot.hpp>
using namespace godot;
void initialize_example_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
ClassDB::register_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 function register_class
for each of our classes in our library.
The important function is the third function called example_library_init
.
We first call a function in our bindings library that creates an initialization object.
This object registrates the initialization and termination functions of the GDExtension.
Furthermore, it sets the level of initialization (core, servers, scene, editor, level).
At last, we need the header file for the register_types.cpp
named
register_types.h
.
#ifndef GDEXAMPLE_REGISTER_TYPES_H
#define GDEXAMPLE_REGISTER_TYPES_H
void initialize_example_module();
void uninitialize_example_module();
#endif // GDEXAMPLE_REGISTER_TYPES_H
编译插件¶
手工编写 SCons 用于构建的 SConstruct
文件并不容易。出于这个示例的目的,只需使用我们已经准备好的这个硬编码的 SConstruct 文件
。我们将在后续教程中介绍如何使用这些构建文件的更可定制的详细示例。
备注
This SConstruct
file was written to be used with the latest godot-cpp
master, you may need to make small changes using it with older versions or
refer to the SConstruct
file in the Godot 4.0 documentation.
Once you've downloaded the SConstruct
file, place it in your GDExtension folder
structure alongside godot-cpp
, src
and demo
, then run:
scons platform=<platform>
您现在应该能够在 demo / bin / <platform>
中找到该模块.
备注
Here, we've compiled both godot-cpp and our gdexample library as debug
builds. For optimized builds, you should compile them using the
target=template_release
switch.
Using the GDExtension module¶
Before we jump back into Godot, we need to create one more file in
demo/bin/
.
This file lets Godot know what dynamic libraries should be
loaded for each platform and the entry function for the module. It is called gdexample.gdextension
.
[configuration]
entry_symbol = "example_library_init"
compatibility_minimum = "4.1"
[libraries]
macos.debug = "res://bin/libgdexample.macos.template_debug.framework"
macos.release = "res://bin/libgdexample.macos.template_release.framework"
windows.debug.x86_32 = "res://bin/libgdexample.windows.template_debug.x86_32.dll"
windows.release.x86_32 = "res://bin/libgdexample.windows.template_release.x86_32.dll"
windows.debug.x86_64 = "res://bin/libgdexample.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://bin/libgdexample.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "res://bin/libgdexample.linux.template_debug.x86_64.so"
linux.release.x86_64 = "res://bin/libgdexample.linux.template_release.x86_64.so"
linux.debug.arm64 = "res://bin/libgdexample.linux.template_debug.arm64.so"
linux.release.arm64 = "res://bin/libgdexample.linux.template_release.arm64.so"
linux.debug.rv64 = "res://bin/libgdexample.linux.template_debug.rv64.so"
linux.release.rv64 = "res://bin/libgdexample.linux.template_release.rv64.so"
android.debug.x86_64 = "res://bin/libgdexample.android.template_debug.x86_64.so"
android.release.x86_64 = "res://bin/libgdexample.android.template_release.x86_64.so"
android.debug.arm64 = "res://bin/libgdexample.android.template_debug.arm64.so"
android.release.arm64 = "res://bin/libgdexample.android.template_release.arm64.so"
This file contains a configuration
section that controls the entry function of the module.
You should also set the minimum compatible Godot version with compatability_minimum
,
which prevents older version of Godot from trying to load your extension.
The libraries
section is the important bit: it tells Godot the location of the
dynamic library in the project's filesystem for each supported platform. It will
also result in just that file being exported when you export the project,
which means the data pack won't contain libraries that are incompatible with the
target platform.
Finally, the dependencies
section allows you to name additional dynamic
libraries that should be included as well. This is important when your GDExtension
plugin implements someone else's library and requires you to supply a
third-party dynamic library with your project.
Here is another overview to check the correct file structure:
gdextension_cpp_example/
|
+--demo/ # 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
Time to jump back into Godot. We load up the main scene we created way back in the beginning and now add a newly available GDExample node to the scene:
We're going to assign the Godot logo to this node as our texture, disable the
centered
property:
我们终于准备好运行这个项目了:
Custom editor icon¶
By default, Godot uses the node icon in the scene dock for GDExtension nodes. The custom icon can be
added via the gdextension
file. The node's icon is set by reference to its name and resource path
of an SVG file.
例如:
[icons]
GDExample = "res://icons/gd_example.svg"
The path should point to a 16 by 16 pixel SVG image. Read the guide for creating icons for more information.
添加属性¶
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);
ClassDB::add_property("GDExample", 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);
ClassDB::add_property("GDExample", 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", this, "my_method");
Note that you can only call my_method
if you've previously registered it in
your _bind_methods
method.
让对象发出信号更为常见。对于我们摇摆不定的 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() {
...
ClassDB::add_property("GDExample", 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")));
}
Here, our ADD_SIGNAL
macro can be a single call with a MethodInfo
argument.
MethodInfo
's first parameter will be the signal's name, and its remaining parameters
are PropertyInfo
types which describe the essentials of each of the method's parameters.
PropertyInfo
parameters are defined with the data type of the parameter, and then the name
that the parameter will have by default.
So here, we add a signal, with a MethodInfo
which names the signal "position_changed". The
PropertyInfo
parameters describe two esential 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++.