自定义 C++ 模块
模块
Godot 允许通过模块化的方式对引擎进行扩展. 你可以创建新的模块, 然后启用/禁用它. 这允许在每个级别添加新的引擎功能, 而无需修改内核, 可以将其拆分以供在不同模块中使用和重用.
modules/ 子目录下. 默认情况下, 有几十个模块被启用, 如GDScript(是的, 它不是基础引擎的一部分), Mono运行时, 正则表达式模块和其他. 可以根据需要创建和组合尽可能多的新模块.SCons构建系统将清晰地处理它.
可以做什么?
尽管我们建议使用脚本编写游戏的大部分代码(因为这能够节省大量的时间), 但使用 C++ 进行开发也是完全可行的. 在以下情况下, 添加C ++模块可能会很有用:
- 将外部库绑定到Godot(例如PhysX, FMOD等).
- 优化游戏的核心部分.
- 为引擎和/或编辑器添加新功能.
- 移植现有的游戏项目.
- 使用 C++ 编写整个新游戏, 因为你离不开 C++.
创建新模块
下载 Godot 源代码并编译。modules/文件夹下创建一个新目录. 如果要单独维护模块, 则可以在版本控制系统(VCS)检出到模块中并使用它.godot/modules/summator)。我们在里面创建一个简单的加法器类:
然后是cpp文件./* 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
然后, 需要以某种方式注册新类, 因此需要再创建两个文件:/* 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.hregister_types.cpp
SCsub和config.py文件旁边), 这样模块才能被正确注册. 这些文件应包含以下内容:/* 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文件, 以便构建系统编译此模块:
使用多个源文件, 你还可以将每个文件分别添加到Python字符串列表中:# SCsubImport('env')env.add_source_files(env.modules_sources, "*.cpp") # Add all cpp files to the build
使用循环和逻辑语句, 这使得使用Python来构建文件列表有了强大的可行性. 请看Godot默认的一些模块的例子. 要添加供编译器查看的包含目录, 可以将其追加到环境的路径中:src_list = ["summator.cpp", "other.cpp", "etc.cpp"]env.add_source_files(env.modules_sources, src_list)
env.Append(CPPPATH=["mylib/include"]) # this is a relative pathenv.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path
env,这样它就不会把这些标志添加到整个Godot构建中(这可能导致错误)。例子SCsub带有自定义标志:# SCsubImport('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.
config.py:# config.pydef can_build(env, platform): return Truedef configure(env): pass
True表示它将针对每个平台进行构建). 就是这样. 希望它不太复杂! 你的模块应如下所示:
然后, 你可以压缩它并与其他所有人分享该模块. 当针对每个平台进行构建时(上一节中的说明), 你的模块将包括在内. 注解godot/modules/summator/config.pygodot/modules/summator/summator.hgodot/modules/summator/summator.cppgodot/modules/summator/register_types.hgodot/modules/summator/register_types.cppgodot/modules/summator/SCsub
core/method_bind_ext.gen.inc, 可以将其增加到13个.使用模块
现在, 您可以通过任何脚本使用新创建的模块: GDScriptvar s = Summator.new()s.add(10)s.add(20)s.add(30)print(s.get_total())s.reset()
60. 参见 绑定到外部库, 了解有关绑定外部库的细节. 警告 编译页面。从外部编译模块
modules/目录下. 虽然这是最直接的编译模块的方法, 但是有几个原因说明为什么这样做不实用:
module_summator_enabled=no的编译选项, 采取额外的步骤手动禁用模块. 创建符号链接也是一种解决方案, 但你可能还需要克服操作系统的限制, 比如如果通过脚本来做, 需要符号链接的权限.modules/的模块文件会改变工作树, 以至于使用VCS(比如git)被证明是很麻烦的, 因为你需要通过过滤变化来确保只提交与引擎相关的代码. 所以, 如果你觉得需要自定义模块的独立结构, 把 “summator” 模块移到引擎的父目录下:mkdir ../modulesmv modules/summator ../modules
custom_modules构建选项来编译我们的引擎, 该选项接受一个以逗号分隔的包含自定义C++模块的目录路径列表, 类似于下面:scons custom_modules=../modules
./modules目录下的所有模块并进行相应的编译, 包括 “summator” 模块. 警告custom_modules的任何路径都将在内部转换为绝对路径, 以区分自定义模块和内置模块, 这意味着像生成模块文档这样的事情可能会依赖于你机器上的特定路径结构. 参见 构建系统介绍 - 自定义模块构建选项.改善开发的构建系统
到目前为止, 我们定义了一个干净简单的SCsub, 它允许我们将新模块的源文件添加为Godot二进制文件的一部分. 那么当我们要构建游戏的发行版, 并希望将所有模块都放在一个二进制文件中时, 这种静态方法就很好. 然而, 代价是每一个变化都意味着游戏的全面重新编译. 即使SCons能够检测并重新编译已经改变的文件, 找到这些文件并最终链接到最终的二进制文件是一个漫长而消耗的部分. 避免这种成本的解决方案是将我们自己的模块构建为共享库, 该库在启动游戏二进制文件时将动态加载.# SCsubImport('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])
bin目录, 其中包含godot*可执行文件和libsummator*.so. 然而, 鉴于.so不在一个标准的目录中(如/usr/lib), 我们必须在运行时用LD_LIBRARY_PATH环境变量帮助可执行文件找到它:
注解export LD_LIBRARY_PATH="$PWD/bin/"./bin/godot*
export环境变量, 否则你将无法从编辑器中播放你的项目. 最重要的是, 能够选择将我们的模块编译为共享库(用于开发)还是作为Godot二进制文件的一部分(用于发行版)将是一件很不错的事情. 为此, 我们可以使用 ARGUMENT 命令定义要传递给SCons的自定义标志:# SCsubImport('env')sources = [ "register_types.cpp", "summator.cpp"]module_env = env.Clone()module_env.Append(CCFLAGS=['-O2'])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命令中明确指定你的共享模块为目标来进一步加快构建速度:scons summator_shared=yes platform=x11 bin/libsummator.x11.tools.64.so
编写自定义文档
编写文档看起来可能是一项无聊的任务, 但仍然强烈建议你为新创建的模块编写文档, 以便使用这个模块的其他人从中受益. 更不用说, 你一年后可能与无法区分它与其他人写的代码, 所以对未来的你自己好一点吧! 为了设置模块的自定义文档, 有几个步骤: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 目录中.
小技巧
git status 检查未跟踪的文件。例如:
user@host:~/godot$ 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 <path> , 它将以XML格式转储引擎API引用到给定的 <path> .
在我们的例子中, 将把它指向克隆的版本库的根目录. 你可以把它指向另一个文件夹, 然后复制需要的文件.
运行命令:
user@host:~/godot/bin$ ./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添加自定义编辑器图标
与如何在模块中编写独立的文档类似, 您也可以为类创建自己的自定义图标, 以使其出现在编辑器中. 编辑器图标. 创建图标后, 请执行以下步骤:
icons的模块的根目录中创建一个新目录. 这是引擎查找模块的编辑器图标的默认路径.svg图标(已优化或未优化)移动到该文件夹中.- 重新编译引擎并运行编辑器. 现在, 相应的图标将出现在编辑器的界面中合适的位置中.
config.py以覆盖默认路径:
def get_icons_path(): return "path/to/icons"总结
记得:
GDCLASS宏, 因此Godot可以封装它_bind_methods将你的函数绑定到脚本, 并允许它们充当信号的回调. 但这还不是全部, 取决于您做什么, 您会得到一些(希望是积极的)惊喜.- Node (或任何派生的节点类型, 例如Sprite)继承, 则新类将显示在编辑器的 “添加节点” 对话框的继承树中.
- Resource 继承, 则它将出现在资源列表中, 并且所有暴露的属性在保存/加载时都可以序列化.
- 通过同样的逻辑, 你可以扩展编辑器, 以及引擎中几乎所有领域.
