Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

以 C++ 自訂模組

模組

Godot 支援以模組化方式擴充引擎。你可以建立新模組,並啟用或停用它們。這讓你能在不修改核心的情況下,於各層級加入新功能,並可分拆成不同模組重複利用。

模組位於建置系統的 modules/ 子目錄。預設已啟用數十個模組,例如 GDScript(沒錯,它不是基礎引擎的一部分)、GridMap 支援、正規表示式模組等。你可以依需求建立並組合任意數量的新模組。SCons 建置系統會透明地處理這一切。

用途說明?

雖然大多數遊戲建議主要以腳本撰寫(如此可大幅節省開發時間),但你完全可以選擇 C++。下列情境適合以 C++ 製作模組:

  • 將外部函式庫(如 PhysX、FMOD 等)繫結到 Godot。

  • 最佳化遊戲中關鍵部分。

  • 為引擎或編輯器新增功能。

  • 將現有遊戲移植到 Godot。

  • 如果你就是非 C++ 不可,也可以用 C++ 製作整個新遊戲。

備註

雖然可以用模組來撰寫自訂遊戲邏輯,但通常 GDExtension 更適合,因為每次修改程式碼後不需要重新編譯引擎。

當 GDExtension 不足以滿足需求、需要更深層的引擎整合時,才主要需要使用 C++ 模組。

建立新模組

在建立模組前,請先 下載 Godot 原始碼並完成編譯

建立新模組的第一步是在 modules/ 目錄下新增一個資料夾。若想獨立維護模組,也可以在 modules 目錄內簽出(checkout)不同的版本控制系統(VCS)來使用。

假設範例模組名為「summator」(godot/modules/summator),我們會在其中建立一個 summator 類別:

godot/modules/summator/summator.h
#pragma once

#include "core/object/ref_counted.h"

class Summator : public RefCounted {
    GDCLASS(Summator, RefCounted);

    int count;

protected:
    static void _bind_methods();

public:
    void add(int p_value);
    void reset();
    int get_total() const;

    Summator();
};

接著是 cpp 檔。

godot/modules/summator/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

重要

這些檔案必須放在模組的最上層資料夾(與 SCsubconfig.py 同一層),模組才能正確註冊。

這些檔案應包含以下內容:

godot/modules/summator/register_types.h
#include "modules/register_module_types.h"

void initialize_summator_module(ModuleInitializationLevel p_level);
void uninitialize_summator_module(ModuleInitializationLevel p_level);
/* yes, the word in the middle must be the same as the module folder name */
godot/modules/summator/register_types.cpp
#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

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

void uninitialize_summator_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
   // Nothing to do here in this example.
}

接下來,需要建立 SCsub 檔案,讓建置系統可以編譯這個模組:

godot/modules/summator/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 內建模組的寫法作為範例。

若要新增編譯器搜尋的 include 目錄,可將其附加至建置環境的路徑中:

env.Append(CPPPATH=["mylib/include"]) # this is a relative path
env.Append(CPPPATH=["#myotherlib/include"]) # this is an 'absolute' path

若要為模組新增自訂編譯器旗標,須先複製 env,避免影響整個 Godot 編譯(否則可能導致錯誤)。以下為帶有自訂旗標的 SCsub 範例:

godot/modules/summator/SCsub
Import('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 的 Python 腳本:

godot/modules/summator/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

也參考

前面的 Summator 範例適合用來製作小型自訂模組。若要整合大型外部函式庫,請參考 繫結外部函式庫 取得詳細繫結教學。

警告

如果你的模組要讓執行中的專案存取(不只是編輯器),那你還需要重新編譯所有要用到的匯出範本,並在每個匯出預設設定中指定自訂範本路徑。否則執行專案時會因為模組未被編譯進匯出範本而發生錯誤。詳情請參考 編譯 頁面。

外部編譯模組

編譯模組通常是將原始碼直接放到引擎的 modules/ 資料夾下。雖然這是最簡單直觀的做法,不過有幾種情境可能不太適合:

  1. 每次想在有或沒有模組的情況下編譯引擎,都必須手動複製模組原始碼,或是額外用 module_summator_enabled=no 這類建置選項來手動關閉模組。你也可以用符號連結,但若用腳本自動化,還得處理作業系統權限(如需要符號連結權限)等限制。

  2. 如果你有維護引擎原始碼,將模組檔案直接放在 modules/ 會讓工作目錄變雜,使用版控(如 git)時,還得過濾只提交引擎相關變更,會變得麻煩。

如果你想讓自訂模組維持獨立結構,可以把「summator」模組移到引擎的上層目錄:

mkdir ../modules
mv modules/summator ../modules

然後以 custom_modules 建置選項編譯引擎,該選項可用逗號分隔路徑,指定多個自訂 C++ 模組目錄,指令如下:

scons custom_modules=../modules

建置系統會自動偵測 ../modules 目錄下所有模組並編譯,包括我們的 summator 模組。

警告

所有傳給 custom_modules 的路徑都會在內部轉為絕對路徑,以區分自訂與內建模組。這也表示,例如產生模組說明文件時,可能會仰賴你電腦上的特定路徑結構。

自訂模組類型初始化流程

模組可以在執行時與其他內建引擎類別互動,甚至影響核心型別的初始化方式。到目前為止,我們都是透過 register_summator_types 來讓模組類別在引擎中可用。

引擎設定的執行順序大致如下,包含這些型別註冊方法:

preregister_module_types();
preregister_server_types();
register_core_singletons();
register_server_types();
register_scene_types();
EditorNode::register_editor_types();
register_platform_apis();
register_module_types();
initialize_physics();
initialize_navigation_server();
register_server_singletons();
register_driver_types();
ScriptServer::init_languages();

我們的 Summator 類別會在 register_module_types() 被呼叫時初始化。假如我們需要滿足一些常見的模組執行時相依(例如單例),或希望可在引擎指派前覆寫既有的引擎方法回呼,那麼就得確保模組類別會在其他內建型別*之前*註冊。

這時你可以選擇定義 preregister_summator_types() 方法,這個方法會在 preregister_module_types() 引擎初始化階段最早被呼叫。

現在要把這個方法加到 register_types 的標頭和原始檔:

godot/modules/summator/register_types.h
#define MODULE_SUMMATOR_HAS_PREREGISTER
void preregister_summator_types();

void register_summator_types();
void unregister_summator_types();

備註

與其他註冊方法不同,這裡要明確定義 MODULE_SUMMATOR_HAS_PREREGISTER,讓建置系統知道在編譯時要包含對應方法呼叫。模組名稱必須轉為全大寫。

godot/modules/summator/register_types.cpp
#include "register_types.h"

#include "core/object/class_db.h"
#include "summator.h"

void preregister_summator_types() {
    // Called before any other core types are registered.
    // Nothing to do here in this example.
}

void register_summator_types() {
    ClassDB::register_class<Summator>();
}

void unregister_summator_types() {
   // Nothing to do here in this example.
}

撰寫自訂說明文件

撰寫說明文件或許乏味,但強烈建議為新模組寫文件,這樣使用者才容易受惠。況且,一年前你寫過的程式碼,未來看起來可能和別人寫的沒兩樣,所以請善待未來的自己!

設定模組自訂說明文件需經過以下步驟:

  1. 在模組根目錄下新增一個資料夾,名稱可自訂,本教學以 doc_classes 為例。

  2. 接下來編輯 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 檢查是否遺漏了某些類別,方法是用 git status 檢視未追蹤檔案。例如:

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,例如 godot --doctool <路徑>,即可將引擎 API 參考文件以 XML 格式匯出到指定路徑。

本例會輸出到 clone 下來的儲存庫根目錄。你也可以輸出到其他資料夾,再將需要的檔案複製過去。

執行指令:

bin/<godot_binary> --doctool .

現在打開 godot/modules/summator/doc_classes 資料夾,會看到裡面有 Summator.xml 或你在 get_doc_classes 內列出的類別 XML 檔。

Edit the file(s) following the class reference primer and recompile the engine.

編譯完成後,你就能在引擎內建說明文件系統中存取這些文件。

之後要更新說明文件,只需修改 XML 檔並重新編譯引擎即可。

如果你有更動模組 API,也可以重新產生文件,原本加入的內容還會保留。但若你將文件存到 Godot 資料夾時,請避免用舊引擎覆蓋新文件,造成內容遺失。

注意:如果你對指定的 <路徑> 沒有寫入權限,可能會遇到類似下列的錯誤:

ERROR: Can't write doc file: docs/doc/classes/@GDScript.xml
   At: editor/doc/doc_data.cpp:956

撰寫自訂單元測試

你可以把獨立的單元測試寫成 C++ 模組的一部分。如果你還不熟 Godot 的單元測試流程,請參考 單元測試

步驟如下:

  1. 在模組根目錄新增一個 tests/ 資料夾:

cd modules/summator
mkdir tests
cd tests
  1. 建立新的測試套件:test_summator.h。標頭檔需以 test_ 為開頭,讓建置系統能自動收集並包含於 tests/test_main.cpp 執行測試。

  2. 寫幾個測試案例。例如:

godot/modules/summator/tests/test_summator.h
#pragma once

#include "tests/test_macros.h"

#include "modules/summator/summator.h"

namespace TestSummator {

TEST_CASE("[Modules][Summator] Adding numbers") {
    Ref<Summator> s = memnew(Summator);
    CHECK(s->get_total() == 0);

    s->add(10);
    CHECK(s->get_total() == 10);

    s->add(20);
    CHECK(s->get_total() == 30);

    s->add(30);
    CHECK(s->get_total() == 60);

    s->reset();
    CHECK(s->get_total() == 0);
}

} // namespace TestSummator
  1. scons tests=yes 編譯引擎,然後用以下指令執行測試:

./bin/<godot_binary> --test --source-file="*test_summator*" --success

你現在應該會看到所有測試通過。

新增自訂編輯器圖示

就像你可以在模組中撰寫獨立說明文件一樣,也能為類別製作自訂圖示,讓它們在編輯器裡顯示。

實際建立並整合編輯器圖示,請先參考 編輯器圖示

建立好圖示後,請依下列步驟進行:

  1. 在模組根目錄建立一個叫 icons 的資料夾。這是引擎尋找模組編輯器圖示的預設路徑。

  2. 將你新做的 svg 圖示(無論有無最佳化)都放進這個資料夾。

  3. 重新編譯引擎並開啟編輯器。你的圖示會在適當位置顯示於編輯器介面。

如果你想把圖示存放在模組其他位置,請在 config.py 加入下列程式碼,覆寫預設路徑:

def get_icons_path():
    return "path/to/icons"

總結

記得:

  • 繼承時使用 GDCLASS 巨集,這樣 Godot 才能正確包裝該類別。

  • 使用 _bind_methods 將函式繫結到腳本,並讓它們支援訊號回呼。

  • 避免將多重繼承的類別暴露給 Godot,因為 GDCLASS 不支援多重繼承。只要類別未暴露給 Godot 腳本 API,仍可自行多重繼承。

不過要注意,根據用途不同,還會遇到許多(希望是正面的)驚喜。

  • 如果你繼承自 Node (或如 Sprite2D 等其他節點類型),新類別會出現在編輯器的「新增節點」對話框的繼承樹上。

  • 如果你繼承自 Resource,該類別會出現在資源列表,且所有公開屬性都能在儲存/讀取時自動序列化。

  • 用同樣方式,你還能擴充編輯器或是引擎幾乎任何部分。