强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Godot 4 GDScript 教程 / 22 - GDExtension(C++ 扩展)

22 - GDExtension(C++ 扩展)

GDExtension 是 Godot 4 中用于编写原生 C/C++ 扩展的系统,替代了 Godot 3 的 GDNative。它允许你将性能关键代码用 C++ 实现,同时保持与 GDScript 的无缝交互。


GDExtension 概述

特性GDNative (Godot 3)GDExtension (Godot 4)
绑定库godot-cpp (旧版)godot-cpp (新版)
加载方式.gdnlib + .gdns.gdextension
API 兼容性需要重新编译更好的 ABI 稳定性
性能良好更好(减少包装层开销)
调试困难支持原生调试器

💡 GDExtension 的最大优势:无需编译 Godot 引擎源码即可扩展引擎功能,且扩展可以在不同 Godot 版本间二进制兼容(同主版本内)。


开发环境搭建

前置条件

# 安装必要工具
# Ubuntu/Debian
sudo apt install build-essential scons pkg-config

# macOS
brew install scons

# Windows
# 安装 Visual Studio(带 C++ 桌面开发工作负载)
# 安装 Python 和 SCons: pip install scons

克隆 godot-cpp

# 在你的扩展项目目录中
git clone https://github.com/godotengine/godot-cpp.git
cd godot-cpp
git checkout godot-4.2-stable  # 选择匹配的 Godot 版本
git submodule update --init --recursive

SCons 构建系统

项目目录结构

my_extension/
├── godot-cpp/              # godot-cpp 子模块
├── src/
│   ├── register_types.cpp  # 入口文件
│   ├── register_types.h
│   ├── example_node.h
│   └── example_node.cpp
├── SConstruct              # 主构建脚本
├── .gdextension            # 扩展描述文件
└── demo/                   # 测试项目
    ├── project.godot
    └── bin/

SConstruct 构建脚本

#!/usr/bin/env python
import os

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

# 添加源文件
env.Append(CPPPATH=["src/"])
sources = Glob("src/*.cpp")

# 构建共享库
library = env.SharedLibrary(
    "demo/bin/libgdexample{}{}".format(env["suffix"], env["SHLIBSUFFIX"]),
    source=sources,
)

Default(library)

注册类与方法

头文件

// src/example_node.h
#ifndef EXAMPLE_NODE_H
#define EXAMPLE_NODE_H

#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/core/class_db.hpp>

using namespace godot;

class ExampleNode : public Node3D {
    GDCLASS(ExampleNode, Node3D)

private:
    double speed;
    String player_name;
    int health;

protected:
    static void _bind_methods();

public:
    ExampleNode();
    ~ExampleNode();

    virtual void _ready() override;
    virtual void _process(double delta) override;

    // 属性 getter/setter
    void set_speed(double p_speed);
    double get_speed() const;

    void set_player_name(const String &p_name);
    String get_player_name() const;

    void set_health(int p_health);
    int get_health() const;

    // 自定义方法
    void take_damage(int amount);
    bool is_alive() const;
};

#endif

实现文件

// src/example_node.cpp
#include "example_node.h"
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/classes/engine.hpp>

using namespace godot;

ExampleNode::ExampleNode() : speed(5.0), player_name("Player"), health(100) {
}

ExampleNode::~ExampleNode() {
}

void ExampleNode::_bind_methods() {
    // 注册属性(自动生成 getter/setter)
    ClassDB::bind_method(D_METHOD("get_speed"), &ExampleNode::get_speed);
    ClassDB::bind_method(D_METHOD("set_speed", "speed"), &ExampleNode::set_speed);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed"), "set_speed", "get_speed");

    ClassDB::bind_method(D_METHOD("get_player_name"), &ExampleNode::get_player_name);
    ClassDB::bind_method(D_METHOD("set_player_name", "name"), &ExampleNode::set_player_name);
    ADD_PROPERTY(PropertyInfo(Variant::STRING, "player_name"), "set_player_name", "get_player_name");

    ClassDB::bind_method(D_METHOD("get_health"), &ExampleNode::get_health);
    ClassDB::bind_method(D_METHOD("set_health", "health"), &ExampleNode::set_health);
    ADD_PROPERTY(
        PropertyInfo(Variant::INT, "health", PROPERTY_HINT_RANGE, "0,100,1"),
        "set_health", "get_health"
    );

    // 注册方法
    ClassDB::bind_method(D_METHOD("take_damage", "amount"), &ExampleNode::take_damage);
    ClassDB::bind_method(D_METHOD("is_alive"), &ExampleNode::is_alive);

    // 注册信号
    ADD_SIGNAL(MethodInfo("health_changed",
        PropertyInfo(Variant::INT, "new_health")));
    ADD_SIGNAL(MethodInfo("player_died"));

    // 注册枚举(示例)
    BIND_ENUM_CONSTANT(STATE_IDLE);
    BIND_ENUM_CONSTANT(STATE_WALKING);
    BIND_ENUM_CONSTANT(STATE_RUNNING);

    // 注册常量
    BIND_CONSTANT(MAX_HEALTH);
}

void ExampleNode::_ready() {
    // 跳过编辑器中的执行
    if (Engine::get_singleton()->is_editor_hint()) {
        return;
    }
    UtilityFunctions::print("ExampleNode ready: ", player_name);
}

void ExampleNode::_process(double delta) {
    if (Engine::get_singleton()->is_editor_hint()) {
        return;
    }
    // 处理逻辑
    Vector3 pos = get_position();
    pos.x += speed * (float)delta;
    set_position(pos);
}

// 属性实现
void ExampleNode::set_speed(double p_speed) {
    speed = p_speed;
}

double ExampleNode::get_speed() const {
    return speed;
}

void ExampleNode::set_player_name(const String &p_name) {
    player_name = p_name;
}

String ExampleNode::get_player_name() const {
    return player_name;
}

void ExampleNode::set_health(int p_health) {
    health = CLAMP(p_health, 0, 100);
    emit_signal("health_changed", health);
    if (health <= 0) {
        emit_signal("player_died");
    }
}

int ExampleNode::get_health() const {
    return health;
}

void ExampleNode::take_damage(int amount) {
    set_health(health - amount);
}

bool ExampleNode::is_alive() const {
    return health > 0;
}

入口注册文件

// src/register_types.h
#ifndef REGISTER_TYPES_H
#define REGISTER_TYPES_H

#include <godot_cpp/core/class_db.hpp>

using namespace godot;

void initialize_example(ModuleInitializationLevel p_level);
void uninitialize_example(ModuleInitializationLevel p_level);

#endif
// src/register_types.cpp
#include "register_types.h"
#include "example_node.h"

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

using namespace godot;

void initialize_example(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
    ClassDB::register_class<ExampleNode>();
}

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

extern "C" {
GDExtensionBool GDE_EXPORT example_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);
    init_obj.register_terminator(uninitialize_example);
    init_obj.set_minimum_library_initialization_level(
        MODULE_INITIALIZATION_LEVEL_SCENE
    );

    return init_obj.init();
}
}

.gdextension 文件

; bin/my_extension.gdextension
[configuration]
entry_symbol = "example_init"
compatibility_minimum = "4.2"

[libraries]
macos.debug = "res://bin/libgdexample.macos.debug.framework"
macos.release = "res://bin/libgdexample.macos.release.framework"
windows.debug.x86_64 = "res://bin/libgdexample.windows.debug.x86_64.dll"
windows.release.x86_64 = "res://bin/libgdexample.windows.release.x86_64.dll"
linux.debug.x86_64 = "res://bin/libgdexample.linux.debug.x86_64.so"
linux.release.x86_64 = "res://bin/libgdexample.linux.release.x86_64.so"

⚠️ 注意compatibility_minimum 指定支持的最低 Godot 版本,确保与目标版本匹配。


从 GDScript 调用 C++

# demo.gd
extends Node

@onready var example = $ExampleNode  # ExampleNode 类型

func _ready():
    # 读写属性
    example.speed = 10.0
    example.player_name = "Hero"
    print("速度: ", example.speed)

    # 调用方法
    example.take_damage(25)
    print("生命值: ", example.health)
    print("存活: ", example.is_alive())

    # 连接信号
    example.health_changed.connect(_on_health_changed)
    example.player_died.connect(_on_player_died)

func _on_health_changed(new_health: int):
    print("生命值变化: ", new_health)

func _on_player_died():
    print("玩家死亡!")

从 C++ 调用 GDScript

// 在 C++ 中调用 GDScript 定义的方法
void ExampleNode::call_gdscript_method() {
    // 调用节点上的 GDScript 方法
    Variant result = call("gdscript_function", 42, "hello");

    // 调用子节点的方法
    Node *child = get_node<Node>("ChildNode");
    if (child) {
        child->call("custom_method");
    }

    // 设置 GDScript 变量
    set("custom_property", 100);
}

💡 使用 call() 调用 GDScript 方法时,如果方法不存在会报错。建议先用 has_method() 检查。


性能对比

以下是一些常见操作的性能对比(C++ vs GDScript):

操作GDScriptC++ GDExtension提升倍数
简单数学运算 (100万次)120ms2ms~60x
字符串处理 (10万次)85ms5ms~17x
数组遍历 (100万元素)200ms8ms~25x
粒子系统 (1万粒子/帧)16ms0.5ms~32x
寻路算法 (A* 1000x1000)3500ms45ms~78x

⚠️ 注意:不要过度优化。只有当 GDScript 确实成为瓶颈时才使用 C++ 扩展。维护 C++ 代码的成本更高。


构建与调试

# 构建 debug 版本
scons target=template_debug

# 构建 release 版本
scons target=template_release

# 清理构建
scons --clean

# 指定平台
scons target=template_debug platform=linux
scons target=template_debug platform=windows
scons target=template_debug platform=macos

调试技巧

// 使用 Godot 的打印函数
#include <godot_cpp/variant/utility_functions.hpp>

UtilityFunctions::print("调试信息: ", some_variable);
UtilityFunctions::push_warning("警告信息");
UtilityFunctions::push_error("错误信息");

// 使用标准 C++ 调试器 (GDB/LLDB)
// 在 VS Code 中配置 launch.json 即可断点调试 C++ 扩展

游戏开发场景

实际案例:高性能加密库

// src/crypto_helper.h
#ifndef CRYPTO_HELPER_H
#define CRYPTO_HELPER_H

#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/core/class_db.hpp>

using namespace godot;

class CryptoHelper : public RefCounted {
    GDCLASS(CryptoHelper, RefCounted)

protected:
    static void _bind_methods();

public:
    // AES-256 加密
    PackedByteArray encrypt_aes256(
        const PackedByteArray &data,
        const PackedByteArray &key
    );

    // AES-256 解密
    PackedByteArray decrypt_aes256(
        const PackedByteArray &data,
        const PackedByteArray &key
    );

    // SHA-256 哈希
    String hash_sha256(const String &input);

    // 生成安全随机字节
    PackedByteArray generate_random_bytes(int length);
};

#endif
# 使用示例
var crypto = CryptoHelper.new()
var key = crypto.generate_random_bytes(32)  # 256 位密钥

var original = "存档数据".to_utf8_buffer()
var encrypted = crypto.encrypt_aes256(original, key)
var decrypted = crypto.decrypt_aes256(encrypted, key)
print(decrypted.get_string_from_utf8())  # "存档数据"

💡 扩展阅读