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.

自定义 C++ 模块

模块

Godot 允许通过模块化的方式对引擎进行扩展. 你可以创建新的模块, 然后启用/禁用它. 这允许在每个级别添加新的引擎功能, 而无需修改内核, 可以将其拆分以供在不同模块中使用和重用.

模块都位于构建系统的 modules/ 子目录中。默认情况下,引擎已经启用了数十个模块,比如 GDScript(没错,它其实并不属于基础引擎的一部分)、GridMap(网格地图)支持、正则表达式模块等等。你可以根据需要创建并组合任意数量的新模块,而 SCons 构建系统会自动帮你处理好这一切。

可以做什么?

尽管我们建议使用脚本编写游戏的大部分代码(因为这能够节省大量的时间), 但使用 C++ 进行开发也是完全可行的. 在以下情况下, 添加C ++模块可能会很有用:

  • 将外部库绑定到 Godot(例如 PhysX、FMOD 等)。

  • 优化游戏的核心部分。

  • 为引擎和/或编辑器添加新功能。

  • 将现有的游戏移植到 Godot。

  • 使用 C++ 编写整个新游戏,因为你离不开 C++。

备注

虽然可以使用模块来实现自定义游戏逻辑,但通常更推荐使用 GDExtension,因为它无需在每次代码修改后重新编译引擎。

C++ 模块主要在 GDExtension 无法满足需求、且需要更深层次引擎集成时使用。

创建新模块

创建模块之前,请先下载 Godot 源代码并编译

要创建一个新模块,第一步是在 modules/ 目录里新建一个文件夹。如果你想对这个模块进行独立维护,也可以直接在 modules 目录下使用另一个版本控制系统(VCS)来管理它。

示例模块的名字就叫加法器“summator”(godot/modules/summator)。我们在里面创建一个简单的加法器类:

godot/modules/summator/summator.h
#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 文件。

godot/modules/summator/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

重要

这些文件必须在模块的顶层文件夹中(SCsubconfig.py 文件旁边),这样才能正确注册模块。

这些文件应包含以下内容:

godot/modules/summator/register_types.h
#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 */
godot/modules/summator/register_types.cpp
#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 文件,让构建系统编译此模块:

godot/modules/summator/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 带有自定义标志:

godot/modules/summator/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

godot/modules/summator/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/ 目录下. 虽然这是最直接的编译模块的方法, 但是有几个原因说明为什么这样做不实用:

  1. 每次编译引擎时, 不管有没有模块, 都必须手动复制模块源码, 或者在编译过程中使用类似 module_summator_enabled=no 的编译选项, 采取额外的步骤手动禁用模块. 创建符号链接也是一种解决方案, 但你可能还需要克服操作系统的限制, 比如如果通过脚本来做, 需要符号链接的权限.

  2. 根据你是否需要使用引擎的源代码, 直接添加到 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 头文件和源文件中:

godot/modules/summator/register_types.h
#define MODULE_SUMMATOR_HAS_PREREGISTER
void preregister_summator_types();

void register_summator_types();
void unregister_summator_types();

备注

与其他注册方法不同,我们必须显式定义 MODULE_SUMMATOR_HAS_PREREGISTER,让构建系统知道在编译时要包含哪些相关方法调用。模块名称也必须转换为大写。

godot/modules/summator/register_types.cpp
#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.
}

编写自定义文档

写文档看起来可能像是一件枯燥乏味的工作,但我们强烈建议你为新建的模块编写文档,这样其他用户才能更轻松地上手和使用。更别提,哪怕是你一年前亲手写的代码,到时候可能也看起来跟别人写的没什么两样(完全看不懂了)——所以,请善待未来的自己吧!

为了设置模块的自定义文档, 有几个步骤:

  1. 在模块的根目录中创建一个新目录. 目录名称可以是任何名称, 但是在本节中, 我们将使用 doc_classes 名称.

  2. 现在, 我们需要编辑 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 目录中.

小技巧

你可以通过 git status 命令查看未追踪(untracked)的文件,来检查自己是不是漏掉了某些类(文件)。例如:

git status

输出示例:

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
    ...
  1. 现在我们可以生成文档:

我们可以通过运行Godot的doctool, 即 godot -doctool <path> , 它将以XML格式转储引擎API引用到给定的 <path> .

在我们的例子中, 将把它指向克隆的版本库的根目录. 你可以把它指向另一个文件夹, 然后复制需要的文件.

运行命令:

bin/<godot_binary> --doctool .

现在, 如果进入 godot/modules/summator/doc_classes 文件夹, 会看到它包含一个在 get_doc_classes 函数中引用的 Summator.xml 文件, 或者其他类.

请按照 类参考入门指南 的说明编辑相关文件,然后重新编译引擎。

一旦编译过程完成, 这些文档将可以在引擎的内置文档系统中访问.

为了保持文档的更新, 你所要做的就是简单地修改其中一个XML文件, 然后从现在开始重新编译引擎.

如果你改变了模块的API, 可以重新提取文档, 它们会包含你之前添加的东西. 当然如果你把它指向 godot 文件夹, 请确保不会因为在新的文档上提取旧引擎构建的旧文档而损失工作.

请注意, 如果你对提供的 <path> 没有写访问权限, 可能会遇到类似下面的错误:

ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
   At: editor/doc/doc_data.cpp:956

编写自定义单元测试

可以在 C++ 模块中编写自包含的单元测试。如果你还不熟悉 Godot 中的单元测试流程,请先参考 单元测试

具体步骤如下:

  1. 在你的模组的根目录下创建一个命名为 tests/ 的新目录:

cd modules/summator
mkdir tests
cd tests
  1. 新建一个测试套件: test_summator.h 。该头文件的命名必须带有 test_ 前缀,这样构建系统才能收集到它,并将其作为 tests/test_main.cpp 的一部分包含进来,从而运行这些测试。

  2. 编写一些测试样例。以下是一个例子:

godot/modules/summator/tests/test_summator.h
#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
  1. scons tests=yes 选项编译引擎,然后用以下命令运行测试:

./bin/<godot_binary> --test --source-file="*test_summator*" --success

你现在应该能看到通过的断言了。

添加自定义编辑器图标

与如何在模块中编写独立的文档类似, 你也可以为类创建自己的自定义图标, 以使其出现在编辑器中.

有关创建要集成到引擎中的编辑器图标的实际过程, 首先请参考 编辑器图标.

创建图标后, 请执行以下步骤:

  1. 在名为 icons 的模块的根目录中创建一个新目录. 这是引擎查找模块的编辑器图标的默认路径.

  2. 将新创建的 svg 图标(已优化或未优化)移动到该文件夹中.

  3. 重新编译引擎并运行编辑器. 现在, 相应的图标将出现在编辑器的界面中合适的位置中.

如果你想将图标存储在模块内的其他位置, 请将以下代码段添加到 config.py 以覆盖默认路径:

def get_icons_path():
    return "path/to/icons"

总结

记得:

  • 对于继承使用 GDCLASS 宏, 因此Godot可以封装它。

  • 使用 _bind_methods 将你的函数绑定到脚本系统,并允许它们作为信号的回调函数来工作。

  • 避免对暴露给 Godot 的类使用多重继承,因为 GDCLASS 宏并不支持这种写法。不过,只要是你自己的、不需要暴露给 Godot 脚本 API 的类,依然可以随意使用多重继承。

但这还不是全部, 取决于你做什么, 你会得到一些(希望是积极的)惊喜.

  • 如果你继承自 Node (或者任何派生出的节点类型,比如 Sprite2D),你的新类就会出现在编辑器的‘添加节点’对话框的继承树中。

  • 如果你从 Resource 继承, 则它将出现在资源列表中, 并且所有暴露的属性在保存/加载时都可以序列化.

  • 通过同样的逻辑, 你可以扩展编辑器, 以及引擎中几乎所有领域.