自定义C++模块

模块(Modules)

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), the Mono runtime, 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等)。
  • 优化游戏的核心部分。
  • 为引擎和/或编辑器添加新功能。
  • 移植现有的游戏项目。
  • 使用 C++ 编写整个新游戏,因为您离不开 C++。

创新新模块

在创建模块之前,请确保下载Godot的源代码并进行编译。文档中有相关教程。

要创建一个新模块,首先我们要在 modules/ 文件夹下创建一个新目录。如果要单独维护模块,则可以在版本控制系统(VCS)检出到模块中并使用它。

示例模块将被称为 求和器(summator),并放置在Godot源代码树的内部(C:\godot 指的是Godot源代码所在的位置):

C:\godot> cd modules
C:\godot\modules> mkdir summator
C:\godot\modules> cd summator
C:\godot\modules\summator>

在内部,我们将创建一个简单的 summator 类:

/* summator.h */

#ifndef SUMMATOR_H
#define SUMMATOR_H

#include "core/reference.h"

class Summator : public Reference {
    GDCLASS(Summator, Reference);

    int count;

protected:
    static void _bind_methods();

public:
    void add(int p_value);
    void reset();
    int get_total() const;

    Summator();
};

#endif // SUMMATOR_H

然后是cpp文件。

/* 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

重要

These files must be in the top-level folder of your module (next to your SCsub and config.py files) for the module to be registered properly.

These files should contain the following:

/* register_types.h */

void register_summator_types();
void unregister_summator_types();
/* yes, the word in the middle must be the same as the module folder name */
/* register_types.cpp */

#include "register_types.h"

#include "core/class_db.h"
#include "summator.h"

void register_summator_types() {
    ClassDB::register_class<Summator>();
}

void unregister_summator_types() {
   // 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)

This allows for powerful possibilities using Python to construct the file list using loops and logic statements. Look at some modules that ship with Godot by default for examples.

要添加供编译器查看的包含目录,可以将其追加到环境的路径中:

env.Append(CPPPATH=["mylib/include"]) # this is a relative path
env.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path

如果要在构建模块时添加自定义编译器标志,则需要首先克隆 env,这样它就不会将这些标志添加到整个Godot构建中(这可能会导致错误)。带有自定义标志的示例 SCsub

# SCsub

Import('env')

module_env = env.Clone()
module_env.add_source_files(env.modules_sources, "*.cpp")
module_env.Append(CCFLAGS=['-O2']) # Flags for C and C++ code
module_env.Append(CXXFLAGS=['-std=c++11']) # Flags 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

然后,您可以压缩它并与其他所有人分享该模块。当针对每个平台进行构建时(上一节中的说明),您的模块将包括在内。

注解

There is a parameter limit of 5 in C++ modules for things such as subclasses. This can be raised to 13 by including the header file core/method_bind_ext.gen.inc.

使用模块

现在,您可以通过任何脚本使用新创建的模块:

var s = Summator.new()
s.add(10)
s.add(20)
s.add(30)
print(s.get_total())
s.reset()

The output will be 60.

参见

The previous Summator example is great for small, custom modules, but what if you want to use a larger, external library? Refer to 绑定到外部库 for details about binding to external libraries.

警告

If your module is meant to be accessed from the running project (not just from the editor), you must also recompile every export template you plan to use, then specify the path to the custom template in each export preset. Otherwise, you'll get errors when running the project as the module isn't compiled in the export template. See the Compiling pages for more information.

从外部编译模块

Compiling a module involves moving the module's sources directly under the engine's modules/ directory. While this is the most straightforward way to compile a module, there are a couple of reasons as to why this might not be a practical thing to do:

  1. Having to manually copy modules sources every time you want to compile the engine with or without the module, or taking additional steps needed to manually disable a module during compilation with a build option similar to module_summator_enabled=no. Creating symbolic links may also be a solution, but you may additionally need to overcome OS restrictions like needing the symbolic link privilege if doing this via script.
  2. Depending on whether you have to work with the engine's source code, the module files added directly to modules/ changes the working tree to the point where using a VCS (like git) proves to be cumbersome as you need to make sure that only the engine-related code is committed by filtering changes.

So if you feel like the independent structure of custom modules is needed, lets take our "summator" module and move it to the engine's parent directory:

mkdir ../modules
mv modules/summator ../modules

Compile the engine with our module by providing custom_modules build option which accepts a comma-separated list of directory paths containing custom C++ modules, similar to the following:

scons custom_modules=../modules

The build system shall detect all modules under the ../modules directory and compile them accordingly, including our "summator" module.

警告

Any path passed to custom_modules will be converted to an absolute path internally as a way to distinguish between custom and built-in modules. It means that things like generating module documentation may rely on a specific path structure on your machine.

改善开发的构建系统

到目前为止,我们定义了一个干净简单的SCsub,它允许我们将新模块的源文件添加为Godot二进制文件的一部分。

那么当我们要构建游戏的发行版,并希望将所有模块都放在一个二进制文件中时,这种静态方法就很好。

However, the trade-off is every single change means a full recompilation of the game. Even if SCons is able to detect and recompile only the file that have changed, finding such files and eventually linking the final binary is a long and costly part.

避免这种成本的解决方案是将我们自己的模块构建为共享库,该库在启动游戏二进制文件时将动态加载。

# SCsub

Import('env')

sources = [
    "register_types.cpp",
    "summator.cpp"
]

# First, create a custom env for the shared library.
module_env = env.Clone()

# Position-independent code is required for a shared library.
module_env.Append(CCFLAGS=['-fPIC'])

# Don't inject Godot's dependencies into our shared library.
module_env['LIBS'] = []

# Define the shared library. By default, it would be built in the module's
# folder, however it's better to output it into `bin` next to the
# Godot binary.
shared_lib = module_env.SharedLibrary(target='#bin/summator', source=sources)

# Finally, notify the main build environment it now has our shared library
# as a new dependency.

# LIBPATH and LIBS need to be set on the real "env" (not the clone)
# to link the specified libraries to the Godot executable.

env.Append(LIBPATH=['#bin'])

# SCons wants the name of the library with it custom suffixes
# (e.g. ".x11.tools.64") but without the final ".so".
shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
env.Append(LIBS=[shared_lib_shim])

Once compiled, we should end up with a bin directory containing both the godot* binary and our libsummator*.so. However given the .so is not in a standard directory (like /usr/lib), we have to help our binary find it during runtime with the LD_LIBRARY_PATH environment variable:

export LD_LIBRARY_PATH="$PWD/bin/"
./bin/godot*

注解

You have to export the environment variable otherwise you won't be able to play your project from within the editor.

最重要的是,能够选择将我们的模块编译为共享库(用于开发)还是作为Godot二进制文件的一部分(用于发行版)将是一件很不错的事情。为此,我们可以使用 ARGUMENT 命令定义要传递给SCons的自定义标志:

# SCsub

Import('env')

sources = [
    "register_types.cpp",
    "summator.cpp"
]

module_env = env.Clone()
module_env.Append(CCFLAGS=['-O2'])
module_env.Append(CXXFLAGS=['-std=c++11'])

if ARGUMENTS.get('summator_shared', 'no') == 'yes':
    # Shared lib compilation
    module_env.Append(CCFLAGS=['-fPIC'])
    module_env['LIBS'] = []
    shared_lib = module_env.SharedLibrary(target='#bin/summator', source=sources)
    shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
    env.Append(LIBS=[shared_lib_shim])
    env.Append(LIBPATH=['#bin'])
else:
    # Static compilation
    module_env.add_source_files(env.modules_sources, sources)

现在默认情况下,scons 命令会将我们的模块构建为Godot二进制文件的一部分,并在传递 summator_shared=yes 时构建为共享库。

Finally, you can even speed up the build further by explicitly specifying your shared module as target in the SCons command:

scons summator_shared=yes platform=x11 bin/libsummator.x11.tools.64.so

编写自定义文档

编写文档看起来可能是一项无聊的任务,但仍然强烈建议您为新创建的模块编写文档,以便使用这个模块的其他人从中受益。更不用说,您一年后可能与无法区分它与其他人写的代码,所以对未来的您自己好一点吧!

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

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

  2. Now, we need to edit config.py, add the following snippet:

    def get_doc_path():
        return "doc_classes"
    
    def get_doc_classes():
        return [
            "Summator",
        ]
    

The get_doc_path() function is used by the build system to determine the location of the docs. In this case, they will be located in the modules/summator/doc_classes directory. If you don't define this, the doc path for your module will fall back to the main doc/classes directory.

The get_doc_classes() method is necessary for the build system to know which registered classes belong to the module. You need to list all of your classes here. The classes that you don't list will end up in the main doc/classes directory.

小技巧

You can use Git to check if you have missed some of your classes by checking the untracked files with git status. For example:

user@host:~/godot$ 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
    ...
  1. Now we can generate the documentation:

We can do this via running Godot's doctool i.e. godot --doctool <path>, which will dump the engine API reference to the given <path> in XML format.

In our case we'll point it to the root of the cloned repository. You can point it to an another folder, and just copy over the files that you need.

运行命令:

user@host:~/godot/bin$ ./bin/<godot_binary> --doctool .

Now if you go to the godot/modules/summator/doc_classes folder, you will see that it contains a Summator.xml file, or any other classes, that you referenced in your get_doc_classes function.

Edit the file(s) following 在类参考手册中贡献 and recompile the engine.

Once the compilation process is finished, the docs will become accessible within the engine's built-in documentation system.

In order to keep documentation up-to-date, all you'll have to do is simply modify one of the XML files and recompile the engine from now on.

If you change your module's API, you can also re-extract the docs, they will contain the things that you previously added. Of course if you point it to your godot folder, make sure you don't lose work by extracting older docs from an older engine build on top of the newer ones.

Note that if you don't have write access rights to your supplied <path>, you might encounter an error similar to the following:

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

添加自定义编辑器图标

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

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

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

  1. 在名为 icons 的模块的根目录中创建一个新目录。这是引擎查找模块的编辑器图标的默认路径。
  2. 将新创建的 svg 图标(已优化或未优化)移动到该文件夹中。
  3. 重新编译引擎并运行编辑器。现在,相应的图标将出现在编辑器的界面中合适的位置中。

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

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

总结

记得:

  • 对于继承使用 GDCLASS 宏,因此Godot可以封装它
  • 使用 _bind_methods 将您的函数绑定到脚本,并允许它们充当信号的回调。

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

  • 如果从 Node (或任何派生的节点类型,例如Sprite)继承,则新类将显示在编辑器的“添加节点”对话框的继承树中。
  • 如果您从 Resource 继承,则它将出现在资源列表中,并且所有暴露的属性在保存/加载时都可以序列化。
  • 通过同样的逻辑,您可以扩展编辑器,以及引擎中几乎所有领域。