C++中的自定义模块

模块

Godot允许以模块化的方式扩展引擎。可以创建新模块,然后启用/禁用。这允许在每个级别添加新的引擎功能,而无需修改核心,核心可以拆分以在不同模块中使用和重用。

模块位于 modules/ 生成系统的子目录。默认情况下,存在许多不同的模块,例如gdscript(是的,它不是基本引擎的一部分)、mono运行时、正则表达式模块和其他模块。可以根据需要创建和组合尽可能多的新模块,而scons构建系统将透明地处理这些模块。

为何?

虽然建议大多数游戏都是用脚本编写的(因为它是一个巨大的时间节省器),所以完全可以使用C++来代替。在下面的场景中添加C++模块是有用的:

  • 将外部库绑定到godot(如physx、fmod等)。

  • 优化游戏的关键部分。

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

  • 移植现有游戏。

  • 在C++中编写一个全新的游戏,因为没有C++你就无法生存。

创建新模块

在创建模块之前,一定要下载godot的源代码并设法编译它。文档中有相关的教程。

要创建新模块,第一步是在 modules/ . 如果要单独维护模块,可以将不同的VCS签入模块并使用它。

示例模块将被称为“求和器”,并放置在godot源树中。 (C:\godot 指Godot来源所在地):

C:\godot> cd modules
C:\godot\modules> mkdir summator
C:\godot\modules> cd summator
C:\godot\modules\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

内容如下:

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

这允许使用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 使用自定义标志:

# 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

然后您可以压缩它并与其他人共享模块。为每个平台构建时(前面章节中的说明),将包括您的模块。

使用模块

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

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

输出将是 60 .

参见

前面的求和器示例非常适合于小的自定义模块,但是如果您想使用更大的外部库呢?请参阅 绑定到外部库 有关绑定到外部库的详细信息。

完善开发建设体系

到目前为止,我们定义了一个干净而简单的scsub,它允许我们添加新模块的源代码作为godot二进制文件的一部分。

当我们想要构建一个游戏的发布版本时,这种静态方法是很好的,因为我们希望所有模块都在一个二进制文件中。

然而,权衡的是,每一次改变都意味着对游戏进行全面的重新编译。即使scons能够只检测和重新编译已更改的文件,查找此类文件并最终链接最终的二进制文件也是一个漫长而昂贵的部分。

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

# SCsub

Import('env')

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

# First, create a custom env for the shared library.
module_env = env.Clone()
module_env.Append(CCFLAGS=['-fPIC'])  # Needed to compile shared library
# We don't want godot's dependencies to be injected into our shared library.
module_env['LIBS'] = []

# Now define the shared library. Note that by default it would be built
# into 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 env it has our shared lirary as a new dependency.
# To do so, SCons wants the name of the lib with it custom suffixes
# (e.g. ".x11.tools.64") but without the final ".so".
# We pass this along with the directory of our library to the main env.
shared_lib_shim = shared_lib[0].name.rsplit('.', 1)[0]
env.Append(LIBS=[shared_lib_shim])
env.Append(LIBPATH=['#bin'])

一旦编译完成,我们应该 bin 目录包含 godot* 二进制和我们的 libsummator*.so . 但是给定的.so不在标准目录中(如 /usr/lib ,我们必须在运行时使用 LD_LIBRARY_PATH 环境变量:

user@host:~/godot$ export LD_LIBRARY_PATH=`pwd`/bin/
user@host:~/godot$ ./bin/godot*

note :注意你必须 export 环境变量,否则您将无法在编辑器中播放项目。

除此之外,还可以选择将模块编译为共享库(用于开发)还是作为godot二进制文件(用于发布)的一部分。为此,我们可以使用 ARGUMENT 命令:

# 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 .

最后,您甚至可以通过在scons命令中将共享模块显式指定为目标来进一步加速构建:

user@host:~/godot$ scons summator_shared=yes platform=x11 bin/libsummator.x11.tools.64.so

编写自定义文档

编写文档可能看起来很无聊,但强烈建议您记录新创建的模块,以便用户更容易从中受益。更别提你一年前写的代码可能会和别人写的代码不一样,所以对你未来的自己要友善!

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

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

  2. 将以下代码段附加到 config.py

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

这个 get_doc_classes() 方法对于构建系统来说是必要的,因为模块可能包含多个类,所以必须合并模块的哪些文档类。更换 ClassName 使用要为其编写文档的类的名称。如果您需要多个类的文档,也可以附加这些文档。

这个 get_doc_path() 方法被构建系统用于确定文档的位置。在我们的情况下,它们将位于 doc_classes 目录。

  1. 运行命令:

    godot --doctool <path>
    

这将把引擎API引用转储到给定的 <path> XML格式。请注意,您需要配置 PATH 找到Godot的可执行文件,并确保您具有写访问权限。否则,您可能会遇到类似以下错误:

ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
   At: editor/doc/doc_data.cpp:956
  1. 从中获取生成的文档文件 godot/doc/classes/ClassName.xml

  2. 将此文件复制到 doc_classes ,或者编辑它,然后编译引擎。

生成系统将从 doc_classes 目录并将它们与基类型合并。编译过程完成后,文档将可以在引擎的内置文档系统中访问。

为了使文档保持最新,您只需修改 ClassName.xml 文件并从现在开始重新编译引擎。

总结

记住:

  • 使用 GDCLASS 用于继承的宏,因此Godot可以包装它

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

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

  • 如果你继承自 结点 (或任何派生节点类型,如sprite),您的新类将出现在编辑器中的“添加节点”对话框的继承树中。

  • 如果你继承自 资源 ,它将出现在资源列表中,并且在保存/加载时可以序列化所有公开的属性。

  • 通过同样的逻辑,您可以扩展编辑器和引擎的几乎任何区域。