自定义 C++ 模块
模块
Godot 允许通过模块化的方式对引擎进行扩展. 你可以创建新的模块, 然后启用/禁用它. 这允许在每个级别添加新的引擎功能, 而无需修改内核, 可以将其拆分以供在不同模块中使用和重用.
Modules are located in the modules/ subdirectory of the build system.
By default, dozens of modules are enabled, such as GDScript (which, yes,
is not part of the base engine), GridMap support, a regular expressions
module, and others. As many new modules as desired can be
created and combined. The SCons build system will take care of it
transparently.
可以做什么?
尽管我们建议使用脚本编写游戏的大部分代码(因为这能够节省大量的时间), 但使用 C++ 进行开发也是完全可行的. 在以下情况下, 添加C ++模块可能会很有用:
将外部库绑定到 Godot(例如 PhysX、FMOD 等)。
优化游戏的核心部分。
为引擎和/或编辑器添加新功能。
将现有的游戏移植到 Godot。
使用 C++ 编写整个新游戏,因为你离不开 C++。
备注
虽然可以使用模块来实现自定义游戏逻辑,但通常更推荐使用 GDExtension,因为它无需在每次代码修改后重新编译引擎。
C++ 模块主要在 GDExtension 无法满足需求、且需要更深层次引擎集成时使用。
创建新模块
创建模块之前,请先下载 Godot 源代码并编译。
To create a new module, the first step is creating a directory inside
modules/. If you want to maintain the module separately, you can checkout
a different VCS into modules and use it.
示例模块的名字就叫加法器“summator”(godot/modules/summator)。我们在里面创建一个简单的加法器类:
#pragma once
#include "core/object/ref_counted.h"
class Summator : public RefCounted {
GDCLASS(Summator, RefCounted);
int count;
protected:
static void _bind_methods();
public:
void add(int p_value);
void reset();
int get_total() const;
Summator();
};
然后是 cpp 文件。
#include "summator.h"
void Summator::add(int p_value) {
count += p_value;
}
void Summator::reset() {
count = 0;
}
int Summator::get_total() const {
return count;
}
void Summator::_bind_methods() {
ClassDB::bind_method(D_METHOD("add", "value"), &Summator::add);
ClassDB::bind_method(D_METHOD("reset"), &Summator::reset);
ClassDB::bind_method(D_METHOD("get_total"), &Summator::get_total);
}
Summator::Summator() {
count = 0;
}
然后需要注册这个新类,因此需要再创建两个文件:
register_types.h
register_types.cpp
重要
这些文件必须在模块的顶层文件夹中(SCsub 和 config.py 文件旁边),这样才能正确注册模块。
这些文件应包含以下内容:
#include "modules/register_module_types.h"
void initialize_summator_module(ModuleInitializationLevel p_level);
void uninitialize_summator_module(ModuleInitializationLevel p_level);
/* yes, the word in the middle must be the same as the module folder name */
#include "register_types.h"
#include "core/object/class_db.h"
#include "summator.h"
void initialize_summator_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
ClassDB::register_class<Summator>();
}
void uninitialize_summator_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
// Nothing to do here in this example.
}
接下来,我们需要创建一个 SCsub 文件,让构建系统编译此模块:
# SCsub
Import('env')
env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build
如果有多个源文件,你还可以将每个文件都分别添加到 Python 字符串列表中:
src_list = ["summator.cpp", "other.cpp", "etc.cpp"]
env.add_source_files(env.modules_sources, src_list)
这样就可以借助 Python 的强大功能通过循环和逻辑语句来构造文件列表。例子可以参考 Godot 自带的模块。
要添加供编译器查看的包含目录,可以将其追加到环境的路径中:
env.Append(CPPPATH=["mylib/include"]) # this is a relative path
env.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path
如果你想在构建模块时添加自定义编译器标志,你需要先克隆 env ,这样它就不会把这些标志添加到整个Godot构建中(这可能导致错误)。例子 SCsub 带有自定义标志:
Import('env')
module_env = env.Clone()
module_env.add_source_files(env.modules_sources, "*.cpp")
# Append CCFLAGS flags for both C and C++ code.
module_env.Append(CCFLAGS=['-O2'])
# If you need to, you can:
# - Append CFLAGS for C code only.
# - Append CXXFLAGS for C++ code only.
最后是模块的配置文件,这是一个 Python 脚本,必须命名为 config.py:
# config.py
def can_build(env, platform):
return True
def configure(env):
pass
询问模块是否可以针对特定平台进行构建(在这种情况下, True 表示它将针对每个平台进行构建).
就是这样. 希望它不太复杂! 你的模块应如下所示:
godot/modules/summator/config.py
godot/modules/summator/summator.h
godot/modules/summator/summator.cpp
godot/modules/summator/register_types.h
godot/modules/summator/register_types.cpp
godot/modules/summator/SCsub
然后, 你可以压缩它并与其他所有人分享该模块. 当针对每个平台进行构建时(上一节中的说明), 你的模块将包括在内.
使用模块
现在, 你可以通过任何脚本使用新创建的模块:
var s = Summator.new()
s.add(10)
s.add(20)
s.add(30)
print(s.get_total())
s.reset()
输出将是 60 .
参见
前面的Summator例子对于小型的自定义模块来说是很好的, 但是如果你想使用一个更大的外部库呢?请参考 绑定到外部库, 了解有关绑定外部库的细节.
警告
如果要从正在运行的项目(而不仅仅是从编辑器)访问模块,则还必须重新编译计划使用的每个导出模板,然后在每个导出预设中指定自定义模板的路径。否则,由于未在导出模板中编译模块,因此在运行项目时会出现错误。更多信息见编译页面。
从外部编译模块
编译一个模块需要将模块的源代码直接移到引擎的 modules/ 目录下. 虽然这是最直接的编译模块的方法, 但是有几个原因说明为什么这样做不实用:
每次编译引擎时, 不管有没有模块, 都必须手动复制模块源码, 或者在编译过程中使用类似
module_summator_enabled=no的编译选项, 采取额外的步骤手动禁用模块. 创建符号链接也是一种解决方案, 但你可能还需要克服操作系统的限制, 比如如果通过脚本来做, 需要符号链接的权限.根据你是否需要使用引擎的源代码, 直接添加到
modules/的模块文件会改变工作树, 以至于使用VCS(比如git)被证明是很麻烦的, 因为你需要通过过滤变化来确保只提交与引擎相关的代码.
所以, 如果你觉得需要自定义模块的独立结构, 把 "summator" 模块移到引擎的父目录下:
mkdir ../modules
mv modules/summator ../modules
通过提供 custom_modules 构建选项来编译我们的引擎, 该选项接受一个以逗号分隔的包含自定义C++模块的目录路径列表, 类似于下面:
scons custom_modules=../modules
构建系统将检测到 ./modules 目录下的所有模块并进行相应的编译, 包括 "summator" 模块.
警告
传递给 custom_modules 的任何路径都将在内部转换为绝对路径, 以区分自定义模块和内置模块, 这意味着像生成模块文档这样的事情可能会依赖于你机器上的特定路径结构.
自定义模块类型初始化
模块可以在运行时与引擎内置的其他类进行交互,甚至可以影响核心类型的初始化。目前我们使用的是 register_summator_types 将模块类引入引擎。
引擎初始化的粗略顺序可以总结为以下类型注册方法列表:
preregister_module_types();
preregister_server_types();
register_core_singletons();
register_server_types();
register_scene_types();
EditorNode::register_editor_types();
register_platform_apis();
register_module_types();
initialize_physics();
initialize_navigation_server();
register_server_singletons();
register_driver_types();
ScriptServer::init_languages();
我们的 Summator 类是在 register_module_types() 调用期间初始化的。假设我们需要满足一些常见的模块运行时依赖(例如单例),或者需要让我们在引擎自身分配之前覆盖现有的引擎方法回调。在这种情况下,我们希望确保我们的模块类在所有其他内置类型之前注册。
这里我们可以定义一个可选的 preregister_summator_types() 方法,它将在引擎设置的 preregister_module_types() 阶段中先于其他内容调用。
我们现在需要将这个方法添加到 register_types 头文件和源文件中:
#define MODULE_SUMMATOR_HAS_PREREGISTER
void preregister_summator_types();
void register_summator_types();
void unregister_summator_types();
备注
与其他注册方法不同,我们必须显式定义 MODULE_SUMMATOR_HAS_PREREGISTER,让构建系统知道在编译时要包含哪些相关方法调用。模块名称也必须转换为大写。
#include "register_types.h"
#include "core/object/class_db.h"
#include "summator.h"
void preregister_summator_types() {
// Called before any other core types are registered.
// Nothing to do here in this example.
}
void register_summator_types() {
ClassDB::register_class<Summator>();
}
void unregister_summator_types() {
// Nothing to do here in this example.
}
编写自定义文档
Writing documentation may seem like a boring task, but it is highly recommended to document your newly created module to make it easier for users to benefit from it. Not to mention that the code you've written one year ago may become indistinguishable from the code that was written by someone else, so be kind to your future self!
为了设置模块的自定义文档, 有几个步骤:
在模块的根目录中创建一个新目录. 目录名称可以是任何名称, 但是在本节中, 我们将使用
doc_classes名称.现在, 我们需要编辑
config.py, 添加以下片段:def get_doc_path(): return "doc_classes" def get_doc_classes(): return [ "Summator", ]
构建系统使用 get_doc_path() 函数来确定文档的位置. 在这种情况下, 它们将位于 modules/summator/doc_classes 目录下. 如果你不定义这个, 你的模块的文档路径将退回到主 doc/classes 目录.
get_doc_classes() 方法对于构建系统知道哪些注册的类属于该模块是必要的. 你需要在这里列出你所有的类. 你没有列出的类最终将出现主 doc/classes 目录中.
小技巧
You can use Git to check if you have missed some of your classes by checking the
untracked files with git status. For example:
git status
Example output:
Untracked files:
(use "git add <file>..." to include in what will be committed)
doc/classes/MyClass2D.xml
doc/classes/MyClass4D.xml
doc/classes/MyClass5D.xml
doc/classes/MyClass6D.xml
...
现在我们可以生成文档:
我们可以通过运行Godot的doctool, 即 godot -doctool <path> , 它将以XML格式转储引擎API引用到给定的 <path> .
在我们的例子中, 将把它指向克隆的版本库的根目录. 你可以把它指向另一个文件夹, 然后复制需要的文件.
运行命令:
bin/<godot_binary> --doctool .
现在, 如果进入 godot/modules/summator/doc_classes 文件夹, 会看到它包含一个在 get_doc_classes 函数中引用的 Summator.xml 文件, 或者其他类.
Edit the file(s) following the class reference primer and recompile the engine.
一旦编译过程完成, 这些文档将可以在引擎的内置文档系统中访问.
为了保持文档的更新, 你所要做的就是简单地修改其中一个XML文件, 然后从现在开始重新编译引擎.
如果你改变了模块的API, 可以重新提取文档, 它们会包含你之前添加的东西. 当然如果你把它指向 godot 文件夹, 请确保不会因为在新的文档上提取旧引擎构建的旧文档而损失工作.
请注意, 如果你对提供的 <path> 没有写访问权限, 可能会遇到类似下面的错误:
ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
At: editor/doc/doc_data.cpp:956
编写自定义单元测试
It's possible to write self-contained unit tests as part of a C++ module. If you are not familiar with the unit testing process in Godot yet, please refer to 单元测试.
The procedure is the following:
在你的模组的根目录下创建一个命名为
tests/的新目录:
cd modules/summator
mkdir tests
cd tests
Create a new test suite:
test_summator.h. The header must be prefixed withtest_so that the build system can collect it and include it as part of thetests/test_main.cppwhere the tests are run.编写一些测试样例。以下是一个例子:
#pragma once
#include "tests/test_macros.h"
#include "modules/summator/summator.h"
namespace TestSummator {
TEST_CASE("[Modules][Summator] Adding numbers") {
Ref<Summator> s = memnew(Summator);
CHECK(s->get_total() == 0);
s->add(10);
CHECK(s->get_total() == 10);
s->add(20);
CHECK(s->get_total() == 30);
s->add(30);
CHECK(s->get_total() == 60);
s->reset();
CHECK(s->get_total() == 0);
}
} // namespace TestSummator
用
scons tests=yes选项编译引擎,然后用以下命令运行测试:
./bin/<godot_binary> --test --source-file="*test_summator*" --success
You should see the passing assertions now.
添加自定义编辑器图标
与如何在模块中编写独立的文档类似, 你也可以为类创建自己的自定义图标, 以使其出现在编辑器中.
有关创建要集成到引擎中的编辑器图标的实际过程, 首先请参考 编辑器图标.
创建图标后, 请执行以下步骤:
在名为
icons的模块的根目录中创建一个新目录. 这是引擎查找模块的编辑器图标的默认路径.将新创建的
svg图标(已优化或未优化)移动到该文件夹中.重新编译引擎并运行编辑器. 现在, 相应的图标将出现在编辑器的界面中合适的位置中.
如果你想将图标存储在模块内的其他位置, 请将以下代码段添加到 config.py 以覆盖默认路径:
def get_icons_path(): return "path/to/icons"
总结
记得:
对于继承使用
GDCLASS宏, 因此Godot可以封装它。Use
_bind_methodsto bind your functions to scripting, and to allow them to work as callbacks for signals.Avoid multiple inheritance for classes exposed to Godot, as
GDCLASSdoesn't support this. You can still use multiple inheritance in your own classes as long as they're not exposed to Godot's scripting API.
但这还不是全部, 取决于你做什么, 你会得到一些(希望是积极的)惊喜.