GDExtension的C++示例

biiigwang / 2024-04-20 / 原文

GDExtension的C++示例

本文按照官方文档,进行c++的GDExtension​插件开发,主要进行文档进行复刻,同时对文档中未涉及步骤进行补充

什么是GDExtension

除了GDScript​和C#​这两种脚本语言外,Godot​引擎可以执行其他编程语言编写的代码。目前有两种方式实现:C++模块与GDExtension

简单的对这两种方式进行对比:

支持的语言 是否需要和引擎一起编译
C++模块 C++
GDExtension C++,Go,D,Haxe,Rust​Swift

GDExtension官方明确维护C++版本,同时目前没有别的语言的维护计划,上表所列出的其他语言均由社区维护,用于生产前请进行充分调研

由上表可以知道,GDExtension​由于无需和引擎一起编译,使用更灵活,并且支持更多语言。

使用GDExtension的依赖

  • Godot4可执行程序
  • c++编译器
  • SCons 作为构建工具
  • godot-cpp 仓库的副本

安装godot-cpp

推荐将godot-cpp​作为项目的git子模块

# 创建项目目录
mkdir gdextension_cpp_example

# 进入目录
cd gdextension_cpp_example
# 创建demo目录用于存放godot项目相关文件
mkdir demo
# 创建src目录用于存放GDExtension的c++代码
mkdir src

# 初始化git版本管理
git init
# 将godot-cpp作为git子模块加入项目,并切换至4.2分支(对应你的godot版本)
git submodule add -b 4.2 https://github.com/godotengine/godot-cpp
# 更新godot-cpp子模块
cd godot-cpp
git submodule update --init

项目结构

本次项目结构如下所示

gdextension_cpp_example/
|
+--demo/                  # game example/demo to test the extension
|
+--godot-cpp/             # C++ bindings
|
+--src/                   # source code of the extension we are building

SCons构建系统

使用Anaconda​+Python虚拟环境​+pip​来搭建SCons​构建系统环境

安装Anaconda

该步骤可参考官方文档

Conda安装Python虚拟环境

All SCons software (SCons itself, tests, supporting utilities) will be written to work with Python version 3.6+.

根据官网手册介绍,Scons的python依赖版本为3.6+​,这里我搭建一个3.11​的环境(版本介绍页面,python每个版本支持5年,3.11支持到2027-10)

# 查看当前环境(可选)
conda env list

# 查看当前环境的包(可选)
conda list
pip list

# 检查conda更新(可选)
conda update conda

# conda清理(可选)
conda clean -p      //删除没有用的包(推荐)

# 创建虚拟环境
conda create -n your_env_name python=3.11

# 激活虚拟环境
conda activate your_env_name

安装SCons

由于conda中SCons​最新版为V3.12​无法进行godot构建,要求V4.0+​,因此需要使用pip方式安装

pip install scons

# 安装后使用查看版本
scons --version

构建C++绑定

项目中的godot-cpp/gdextension/extension_api.json

仓库中该文件已经存在,当你的版本与仓库配置文件中描述的版本不同时,你需要自己手动生成你使用版本对应的extension_api.json

extension_api.json​解读

查看版本

查看header​字段的版本与使用的版本是否相同,例子中我使用的godot版本为V4.2.2​,与配置文件对应

	"header": {
		"version_major": 4,
		"version_minor": 2,
		"version_patch": 2,
		"version_status": "stable",
		"version_build": "official",
		"version_full_name": "Godot Engine v4.2.2.stable.official"
	},

image

生成当前版本的extension_api.json

当你的版本与仓库中的extension_api.json​版本不匹配时,你可以手动生成当前版本的配置文件,详细信息查看官方手册,方法如下:

# 进入godot安装目录
cd godot_bin_path

# 生成extension_api.json
godot --dump-extension-api

# 执行结束会在可执行程序godot目录下生成配置文件godot_bin_path/extension_api.json

# 拷贝到项目目录中,放在哪里自己决定,后续需要根据该文件配置scons
cp extension_api.json path_to_your_project/

构建绑定

若要生成和编译绑定,请使用以下命令(替换 <platform>​ 为 windows​ 或 linuxmacos​ 取决于您的操作系统):

cd path_to_your_project/godot-cpp
scons platform=<platform> custom_api_file=<PATH_TO_FILE>
cd ..

这一步将需要一段时间. 完成后, 你应该有一个静态库, 可以编译到你的项目中, 存储在 godot-cpp / bin / ​中.

你可能需要在 Windows 或 Linux 的命令行中添加 bits=64​。

实现一个简单的插件

  • 打开Godot​在demo​中新建一个项目
  • 在我们的演示项目中, 我们将创建一个包含名为 "Main" 的节点的场景, 我们将其保存为 main.tscn​ . 我们稍后再回过头来看看.

该例中创建一个名为GDExample​的插件,该插件继承自Sprite2D

创建插件头文件

首先新建插件的头文件gdexample.h

#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H

#include <godot_cpp/classes/sprite2d.hpp>

namespace godot {

class GDExample : public Sprite2D {
	GDCLASS(GDExample, Sprite2D)

private:
	double time_passed;

protected:
	static void _bind_methods();

public:
	GDExample();
	~GDExample();

	void _process(double delta) override;
};

}

#endif

我们解读上述的代码

  • 命名空间godot​,因为GDExtension中所有内容都在这个命名空间内
  • 继承Sprite2D​包含sprite2d.hpp
  • GDCLASS​用于设置一些例行执行的操作
  • 我们定义了一个time_passed​成员,用于保存运行时间
  • 静态方法_bind_methods​,Godot 将调用它来找出可以调用哪些方法以及它公开了哪些属性。
  • _process​方法,与GDScript​中的_process​工作方式相同,每帧会进行调用

创建源文件

新建gdexample.cpp​,上述插件方法的具体实现在此处进行实现

#include "gdexample.h"
#include <godot_cpp/core/class_db.hpp>

using namespace godot;

void GDExample::_bind_methods() {
}

GDExample::GDExample() {
	// Initialize any variables here.
	time_passed = 0.0;
}

GDExample::~GDExample() {
	// Add your cleanup here.
}

void GDExample::_process(double delta) {
	time_passed += delta;

	Vector2 new_position = Vector2(10.0 + (10.0 * sin(time_passed * 2.0)), 10.0 + (10.0 * cos(time_passed * 1.5)));

	set_position(new_position);
}
  • _process​中我们根据当前时间,使用正弦与余弦函数计算出精灵的新位置

除了以上插件类的c++代码,我们的GDExtension​插件可以包含多个插件类,每个类都有自己的头文件与源文件,就像GDExample​一样。

在插件类编码完毕后,我们还需要一个register_types.cpp​文件,该文件负责告诉Godot​我们插件中的所有类。

编写注册函数

创建register_types.cpp

#include "register_types.h"

#include "gdexample.h"

#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>

using namespace godot;

void initialize_example_module(ModuleInitializationLevel p_level) {
	if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
		return;
	}

	ClassDB::register_class<GDExample>();
    // ClassDB::register_class<YourOtherClass>();
}

void uninitialize_example_module(ModuleInitializationLevel p_level) {
	if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
		return;
	}
}

extern "C" {
// Initialization.
GDExtensionBool GDE_EXPORT example_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
	godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);

	init_obj.register_initializer(initialize_example_module);
	init_obj.register_terminator(uninitialize_example_module);
	init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);

	return init_obj.init();
}
}

解读上述代码:

  • initialize_example_module​、uninitialize_example_module​:分别在Godot​进行加载​或卸载​我们的GDExtension​时执行。

    目前在这里所做的只是解析绑定模块中的函数以初始化它们,但您可能需要根据需要,设置更多内容。我们为库中的每个类调用该函数register_class​ 。

  • 最重要的是第三个函数example_library_init​,该函数可以认为是插件的入口,后续会在配置文件(xxx.gdextension​)中标注出来。

    这个函数进行了如下工作:

    • 注册加载​时需要执行的操作initialize_example_module

    • 注册卸载​时需要执行的操作uninitialize_example_module

    • 设置初始化级别(level of initialization)​涉及(core(核心), servers(服务器), scene(场景), editor(编辑器), level(级别))

      注:官方文档中对此没有更多说明,本文作者猜测涉及到插件在何时初始化的时机设置

创建对应的头文件register_types.h

#ifndef GDEXAMPLE_REGISTER_TYPES_H
#define GDEXAMPLE_REGISTER_TYPES_H

#include <godot_cpp/core/class_db.hpp>

using namespace godot;

void initialize_example_module(ModuleInitializationLevel p_level);
void uninitialize_example_module(ModuleInitializationLevel p_level);

#endif // GDEXAMPLE_REGISTER_TYPES_H

编译插件

编译插件需要手工编写SConstruct​文件,这并不容易,官方提供一个根据本例硬编码的文件,如下所示:

#!/usr/bin/env python
import os
import sys

env = SConscript("godot-cpp/SConstruct")

# For reference:
# - CCFLAGS are compilation flags shared between C and C++
# - CFLAGS are for C-specific compilation flags
# - CXXFLAGS are for C++-specific compilation flags
# - CPPFLAGS are for pre-processor flags
# - CPPDEFINES are for pre-processor defines
# - LINKFLAGS are for linking flags

# tweak this if you want to use different folders, or more folders, to store your source code in.
env.Append(CPPPATH=["src/"])
sources = Glob("src/*.cpp")

if env["platform"] == "macos":
    library = env.SharedLibrary(
        "demo/bin/libgdexample.{}.{}.framework/libgdexample.{}.{}".format(
            env["platform"], env["target"], env["platform"], env["target"]
        ),
        source=sources,
    )
else:
    library = env.SharedLibrary(
        "demo/bin/libgdexample{}{}".format(env["suffix"], env["SHLIBSUFFIX"]),
        source=sources,
    )

Default(library)

将改文件放入项目根目录,即gdextension_cpp_example/​,与godot-cpp​,src​,demo​同级,然后开始执行编译操作,执行如下命令

scons platform=<platform>

你现在应该能够在demo/bin/<platform>​ 中找到该模块.

在这里,我们将 godot-cpp 和我们的 gdexample 库编译为调试版本。对于优化的构建,应使用target=template_release​开关进行编译。

使用GDExtension模块

终于,我们可以使用自己编译的插件了,再回到Godot​之前,我们需要在demo/bin/​中创建一个文件

这个文件让Godot​知道,在不同平台应该从哪里拿到不同的动态库文件,同时声明模块的入口函数,函数定义在编写注册函数中。

[configuration]

entry_symbol = "example_library_init"
compatibility_minimum = "4.2"

[libraries]

macos.debug = "res://bin/libgdexample.macos.template_debug.framework"
macos.release = "res://bin/libgdexample.macos.template_release.framework"
windows.debug.x86_32 = "res://bin/libgdexample.windows.template_debug.x86_32.dll"
windows.release.x86_32 = "res://bin/libgdexample.windows.template_release.x86_32.dll"
windows.debug.x86_64 = "res://bin/libgdexample.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://bin/libgdexample.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "res://bin/libgdexample.linux.template_debug.x86_64.so"
linux.release.x86_64 = "res://bin/libgdexample.linux.template_release.x86_64.so"
linux.debug.arm64 = "res://bin/libgdexample.linux.template_debug.arm64.so"
linux.release.arm64 = "res://bin/libgdexample.linux.template_release.arm64.so"
linux.debug.rv64 = "res://bin/libgdexample.linux.template_debug.rv64.so"
linux.release.rv64 = "res://bin/libgdexample.linux.template_release.rv64.so"
android.debug.x86_64 = "res://bin/libgdexample.android.template_debug.x86_64.so"
android.release.x86_64 = "res://bin/libgdexample.android.template_release.x86_64.so"
android.debug.arm64 = "res://bin/libgdexample.android.template_debug.arm64.so"
android.release.arm64 = "res://bin/libgdexample.android.template_release.arm64.so"

这个文件中我们需要关注如下信息

  • configuration​中定义了兼容的Godot​最低版本,填写该字段防止旧版本Godot​加载你的插件
  • libraries​ 部分是重要的部分:它告诉 Godot 每个受支持平台的动态库在项目文件系统中的位置。这也将导致在导出项目时仅导出该文件,这意味着数据包将不包含与目标平台不兼容的库。
  • 最后,该dependencies​ 部分允许您声明还应包含的其他动态库。当您的GDExtension插件实现其他人的库并要求您为项目提供第三方动态库时,这一点很重要。

现在我们的项目结构看起来应该像这样:

gdextension_cpp_example/
|
+--demo/                  # game example/demo to test the extension
|   |
|   +--main.tscn
|   |
|   +--bin/
|       |
|       +--gdexample.gdextension
|
+--godot-cpp/             # C++ bindings
|
+--src/                   # source code of the extension we are building
|   |
|   +--register_types.cpp
|   +--register_types.h
|   +--gdexample.cpp
|   +--gdexample.h

添加我们的插件

现在在节点中就可以找到我们的插件GDExample

image

我们加入该节点,并进行如下设置:对Sprite2d​添加Texture​,同时取消勾选Offset/Centered

image

此时我们可以运行我们的项目:

gdextension_run

自定义编辑器图标

默认情况下, Godot 使用 GDExtension 节点的场景停靠栏中的节点图标.可以通过 gdextension ​文件添加自定义图标。节点的图标是通过引用其名称和 SVG 文件的资源路径来设置的。

例如

[icons]

GDExample = "res://icons/gd_example.svg"

参考资料

[官方资料]GDExtension C++ 示例