入門

工作流程總覽

作為 GDExtension,godot-cpp 的使用會比 GDScriptC# 複雜。若你決定使用它,你的工作流程大致如下:

  • 建立新的 godot-cpp 專案(可使用 範本,或依下文從零開始)。

  • 在本機使用你 偏好的 IDE 進行開發。

  • 使用最早支援的 Godot 版本建置並測試你的程式碼。

  • 為你要支援的所有平台建立建置(例如使用 GitHub Actions)。

  • 可選:發佈到 Godot 資產庫

範例專案

第一次使用 godot-cpp 時,建議先依本指南了解 godot-cpp 涉及的技術。完成後,你可以改用 godot-cpp 範本,它涵蓋更多功能,例如 GitHub 動作工作流程與實用的 SConstruct 樣板程式碼。不過該範本本身沒有提供詳細說明,因此我們建議先讀完本指南。

設定專案

你需要先準備以下幾項:

  • Godot 4 可執行檔。

  • C++ 編譯器。

  • SCons 作為建置工具。

  • 一份 godot-cpp 儲存庫 的複本。

另請參閱 設定 IDE編譯,因為這些建構工具與從原始碼編譯 Godot 使用的工具相同。

你可以從 GitHub 下載 godot-cpp 倉庫,或用 Git 指令自動抓取。請注意,這個倉庫針對不同的 Godot 版本有不同分支。GDExtension 僅支援 Godot 4 及以上版本,且不同版本之間不相容,所以請務必下載正確的分支。

備註

要使用 GDExtension,你必須使用與目標 Godot 版本相符的 godot-cpp 分支。例如,如果你的目標是 Godot 4.1,請使用 4.1 分支。本教學以 4.x 為例,實際操作時請依你的目標版本替換分支名稱。

master 分支是開發中的分支,會定期與 Godot 的 master 分支同步更新。

警告

以較早的 Godot 版本為目標所建置的 GDExtension 通常可在後續的小版本中運作,反之則不行。例如,目標為 Godot 4.2 的 GDExtension 在 Godot 4.3 應可正常運作,但目標為 Godot 4.3 的 GDExtension 無法在 Godot 4.2 運作。

此規則有一個例外: 以 Godot 4.0 為目標的擴充在 Godot 4.1 及後續版本中將 無法 運作 (請見 將 GDExtension 更新到 4.1)。

如果你有用 Git 進行專案版本管理,建議將它加為 Git 子模組:

mkdir gdextension_cpp_example
cd gdextension_cpp_example
git init
git submodule add -b 4.x https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init

你也可以直接將它 clone 到專案目錄下:

mkdir gdextension_cpp_example
cd gdextension_cpp_example
git clone -b 4.x https://github.com/godotengine/godot-cpp

備註

如果你選擇直接下載或 clone 倉庫到資料夾,請務必保持資料夾結構與本教學一致,因為後續範例程式碼都假設你的專案有這樣的結構。

如果你是從前面介紹的連結 clone 範例,子模組預設並不會自動初始化,你需要手動執行以下指令:

cd gdextension_cpp_example
git submodule update --init

這會在你的專案資料夾中初始化所需的子模組。

建構 C++ 綁定

先決條件都備齊後,接下來開始建構 C++ 綁定。

倉庫裡包含了目前 Godot 版本的中繼資料副本,但如果你要針對較新版本建構綁定,請用 Godot 執行檔產生:

godot --dump-extension-api

產生的 extension_api.json 會出現在執行檔所在資料夾。請將它複製到專案資料夾,並在下方 scons 指令中加上 custom_api_file=<檔案路徑>

要產生並編譯綁定,請使用下列指令(依你的作業系統將 <platform> 替換為 windowslinuxmacos):

建構過程會自動偵測可用的 CPU 執行緒數量以進行平行建構。若要自訂執行緒數,可在 SCons 指令結尾加上 -jN,其中 N 是你想使用的執行緒數。

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

這個步驟會花一點時間。完成後,你會在 godot-cpp/bin/ 看到可供專案編譯連結的靜態函式庫。

備註

在 Windows 或 Linux 下可能要在指令後加上 bits=64

建立簡單外掛

現在來實作一個真正的外掛。我們將先建立一個空的 Godot 專案,並在裡面放入一些檔案。

開啟 Godot,建立一個新專案。這裡我們會將專案放在 GDExtension 資料夾結構下的 demo 資料夾中。

在 demo 專案裡,建立一個名為「Main」的節點並儲存為 main.tscn 場景。我們稍後會再回來用到它。

回到 GDExtension 模組的根目錄,再建立一個名為 src 的子資料夾,這之後會用來放我們的原始碼檔案。

你的 GDExtension 模組目錄下,現在應該有 demogodot-cppsrc 三個資料夾。

目前的資料夾結構應該長這樣:

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

src 資料夾下,先建立我們要用來擴充的 GDExtension 節點的標頭檔,我們把它命名為 gdexample.h

gdextension_cpp_example/src/gdexample.h
#pragma once

#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;
};

} // namespace godot

上面程式碼有幾點要注意。我們引入了 sprite2d.hpp,這個檔案包含對 Sprite2D 類的綁定。我們會以這個類別做為擴充的基礎。

我們使用 godot 命名空間,因為 GDExtension 所有內容都定義在此命名空間中。

接下來是類別定義,這裡我們透過容器類別繼承自 Sprite2D。GDCLASS 巨集會幫我們處理一些必要的內部註冊。

接著宣告了一個名為 time_passed 的成員變數。

在接下來的方法區塊中,除了建構子和解構子,還有兩個你可能很熟悉的方法,另外還有一個新方法。

第一個是 _bind_methods,Godot 會呼叫這個靜態方法來註冊可以被呼叫的方法和屬性。第二個是 _process,它的功能就跟你在 GDScript 使用的 _process 一樣。

接下來,實作這些方法並建立 gdexample.cpp 檔案:

gdextension_cpp_example/src/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++ 檔案,命名為 register_types.cpp。一個 GDExtension 外掛可以有多個類別,每個類別有自己的標頭和原始檔,像前面那樣實作 GDExample。現在我們需要一小段程式碼告訴 Godot 外掛裡有哪些類別。

gdextension_cpp_example/src/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;
    }

    GDREGISTER_RUNTIME_CLASS(GDExample);
}

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();
}
}

Godot 載入外掛時會呼叫 initialize_example_module,解除安裝時則呼叫 uninitialize_example_module。這邊我們只做初始化綁定模組,依需求你也可以設定其他內容。對於每個類別都要呼叫 GDREGISTER_RUNTIME_CLASS 巨集,代表它們只會在遊戲執行時註冊,這與 GDScript 預設行為相同。

第三個函式 example_library_init 很重要。我們會先呼叫綁定庫的初始化函式來建立初始化物件,這個物件會註冊 GDExtension 的初始化與終結函式,也能設定初始化階段(如 core、servers、scene、editor、level)。

最後,還需要為 register_types.cpp 建立標頭檔 register_types.h

gdextension_cpp_example/src/register_types.h
#pragma once

#include <godot_cpp/core/class_db.hpp>

using namespace godot;

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

編譯外掛

要編譯這個專案,我們需要以 SConstruct 檔案定義 SCons 應如何編譯,並引用 godot-cpp 中的那一份。從零撰寫不在本教學範圍內,不過你可以 下載我們準備好的 SConstruct 。在後續教學中,我們會介紹更可自訂、也更詳細的建置檔使用方式。

備註

這個 SConstruct 檔案是針對最新的 godot-cpp master 分支撰寫的,如要與舊版搭配使用,可能需做些微調整或參考 Godot 4.x 文件的 SConstruct 範例。

下載好 SConstruct 檔案後,請將它放在 GDExtension 目錄下,跟 godot-cppsrcdemo 目錄同一層,然後執行:

scons platform=<platform>

此時你應該可以在 demo/bin/<platform> 下找到編譯出來的模組。

若要為 iOS 編譯,需將模組包裝成靜態 .xcframework,可以用以下指令完成:

# compile simulator and device modules
scons arch=universal ios_simulator=yes platform=ios target=<target>
scons arch=arm64 ios_simulator=no platform=ios target=<target>

# assemble xcframeworks
xcodebuild -create-xcframework -library demo/bin/libgdexample.ios.<target>.a -library demo/bin/libgdexample.ios.<target>.simulator.a -output demo/bin/libgdexample.ios.<target>.xcframework
xcodebuild -create-xcframework -library godot-cpp/bin/libgodot-cpp.ios.<target>.arm64.a -library godot-cpp/bin/libgodot-cpp.ios.<target>.universal.simulator.a  -output demo/bin/libgodot-cpp.ios.<target>.xcframework

備註

此範例同時將 godot-cpp 和 gdexample 函式庫都編譯為 debug 版本。若要釋出版或最佳化,請將指令加上 target=template_release 參數。

使用 GDExtension 模組

回到 Godot 之前,還需在 demo/bin/ 新增一個檔案。

這個檔案讓 Godot 知道要針對各平台載入哪些動態函式庫,以及模組的進入點。名稱為 gdexample.gdextension

[configuration]

entry_symbol = "example_library_init"
compatibility_minimum = "4.1"
reloadable = true

[libraries]

macos.debug = "res://bin/libgdexample.macos.template_debug.framework"
macos.release = "res://bin/libgdexample.macos.template_release.framework"
ios.debug = "res://bin/libgdexample.ios.template_debug.xcframework"
ios.release = "res://bin/libgdexample.ios.template_release.xcframework"
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"

[dependencies]
ios.debug = {
    "res://bin/libgodot-cpp.ios.template_debug.xcframework": ""
}
ios.release = {
    "res://bin/libgodot-cpp.ios.template_release.xcframework": ""
}

此檔案包含 configuration 區段,用於控制模組的進入點函式。你也應以 compatibility_minimum 設定最低相容的 Godot 版本,避免舊版 Godot 嘗試載入你的擴充。reloadable 旗標可讓你在每次重新編譯後,由編輯器自動重新載入擴充,而不必重新啟動編輯器。這僅在以除錯模式(預設)編譯擴充時有效。

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

現在可以回到 Godot。請打開一開始建立的主場景,並新增一個 GDExample 節點到場景中:

../../../_images/gdextension_cpp_nodes.webp

我們將 Godot 標誌指定為這個節點的紋理,並將 centered 屬性關閉:

../../../_images/gdextension_cpp_sprite.webp

現在一切就緒,可以執行專案了:

新增屬性

GDScript 可以用 export 關鍵字讓腳本有可編輯屬性。在 GDExtension 則需用 getter 和 setter 方法註冊屬性,或直接實作物件的 _get_property_list_get_set 方法(但這屬進階內容,不在本教學範圍)。

現在我們來新增一個屬性,讓我們可以調整波動的振幅。

gdexample.h 檔案中,新增一個成員變數及 getter/setter 方法:

...
private:
    double time_passed;
    double amplitude;

public:
    void set_amplitude(const double p_amplitude);
    double get_amplitude() const;
...

gdexample.cpp 中也需做一些變動,以下只列出有異動的方法,未提及的行請勿移除:

void GDExample::_bind_methods() {
    ClassDB::bind_method(D_METHOD("get_amplitude"), &GDExample::get_amplitude);
    ClassDB::bind_method(D_METHOD("set_amplitude", "p_amplitude"), &GDExample::set_amplitude);

    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "amplitude"), "set_amplitude", "get_amplitude");
}

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

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

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

    set_position(new_position);
}

void GDExample::set_amplitude(const double p_amplitude) {
    amplitude = p_amplitude;
}

double GDExample::get_amplitude() const {
    return amplitude;
}

將這些變動編譯完成後,就會看到介面多了一個屬性。你可以隨時調整它,執行專案時 Godot 圖示就會隨之變動軌跡幅度。

我們再來對動畫速度做同樣的處理,一樣用 getter 與 setter。在 gdexample.h 標頭檔只需再加幾行:

...
    double amplitude;
    double speed;
...
    void _process(double delta) override;
    void set_speed(const double p_speed);
    double get_speed() const;
...

gdexample.cpp 檔案還要再做幾個小改動,以下只列出異動的部份,其餘請保留原本內容:

void GDExample::_bind_methods() {
    ...
    ClassDB::bind_method(D_METHOD("get_speed"), &GDExample::get_speed);
    ClassDB::bind_method(D_METHOD("set_speed", "p_speed"), &GDExample::set_speed);

    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,20,0.01"), "set_speed", "get_speed");
}

GDExample::GDExample() {
    time_passed = 0.0;
    amplitude = 10.0;
    speed = 1.0;
}

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

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

    set_position(new_position);
}

...

void GDExample::set_speed(const double p_speed) {
    speed = p_speed;
}

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

這樣編譯後你會看到介面多一個 speed 屬性。調整它就能改變動畫快慢。此外,我們還加上了屬性範圍,前兩個參數為最小與最大值,第三個為步進值。

備註

這裡我們只用到 property 方法的 hint_range 屬性。事實上還有更多選項可以自訂屬性在 Godot 裡的顯示與設定方式。

訊號

最後,GDExtension 也完整支援訊號。如果想讓你的擴充對其他物件發出的訊號有反應,須先對該物件呼叫 connect。我們的 Godot 圖示搖擺例子不太適合這種情境,如果要示範需要更完整的範例。

語法如下:

some_other_node->connect("the_signal", Callable(this, "my_method"));

要將某個節點的 the_signal 訊號連結到我們的 my_method 方法,需在 connect 方法中指定訊號名稱與一個 CallableCallable 持有待呼叫方法的物件資訊。本例中,它把目前物件實例 thismy_method 方法綁定,之後 connect 會將這個 Callable 加入 the_signal 的觀察者。每當 the_signal 發射時,Godot 就知道要呼叫對應物件的方法。

請注意,只有在你事先於 _bind_methods 註冊過 my_method,Godot 才會知道這個方法的存在,才能將訊號連結過來。

如需更多 Callable 相關資訊,請參考類別文件:Callable

讓你的物件發出訊號也是很常見的需求。這裡我們用搖擺的 Godot 圖示作為例子,每過一秒就發出一個訊號並傳送新的座標。

gdexample.h 標頭檔加上新的成員變數 time_emit

...
    double time_passed;
    double time_emit;
    double amplitude;
...

這次 gdexample.cpp 的變更較多。首先,要在 _init 方法或建構子裡設定 time_emit = 0.0 。剩下兩項修改我們分開說明。

_bind_methods 方法中要註冊我們的訊號,範例如下:

void GDExample::_bind_methods() {
    ...
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed", PROPERTY_HINT_RANGE, "0,20,0.01"), "set_speed", "get_speed");

    ADD_SIGNAL(MethodInfo("position_changed", PropertyInfo(Variant::OBJECT, "node"), PropertyInfo(Variant::VECTOR2, "new_pos")));
}

ADD_SIGNAL 巨集的參數是 MethodInfo。第一個參數為訊號名稱,後續每個參數都是 PropertyInfo,描述訊號會帶入的資料型別與名稱。

這裡我們新增一個名為 "position_changed" 的訊號,有兩個參數:一個是 Object 型別叫做 "node",另一個是 Vector2 型別叫做 "new_pos"。

接下來修改 _process 方法:

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

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

    set_position(new_position);

    time_emit += delta;
    if (time_emit > 1.0) {
        emit_signal("position_changed", this, new_position);

        time_emit = 0.0;
    }
}

每經過一秒,就發送訊號並重設計時器。我們可以直接把參數丟給 emit_signal

編譯好 GDExtension 後,回到 Godot 選取精靈節點,在 節點 面板就會看到剛剛新增的訊號,可以按 連接 或直接點兩下連結。主節點加上一個腳本後,訊號可以這樣處理:

extends Node

func _on_Sprite2D_position_changed(node, new_pos):
    print("The position of " + node.get_class() + " is now " + str(new_pos))

每過一秒,我們就會將座標輸出到主控台。

下一步

希望以上範例能讓你掌握基礎。你可以在此基礎上以 C++ 撰寫完整的腳本來控制 Godot 的節點!

與其以以上的範例設定為基礎,我們建議你現在改為複製 godot-cpp 範本,並以它作為專案的起點。該範本涵蓋更多功能,例如 GitHub 建置動作與額外實用的 SConstruct 樣板。