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.
Checking the stable version of the documentation...
以 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 類別:
#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 檔。
#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
重要
這些檔案必須放在模組的最上層資料夾(與 SCsub 和 config.py 同一層),模組才能正確註冊。
這些檔案應包含以下內容:
#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 */
#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 檔案,讓建置系統可以編譯這個模組:
# 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 範例:
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 腳本:
# 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/ 資料夾下。雖然這是最簡單直觀的做法,不過有幾種情境可能不太適合:
每次想在有或沒有模組的情況下編譯引擎,都必須手動複製模組原始碼,或是額外用
module_summator_enabled=no這類建置選項來手動關閉模組。你也可以用符號連結,但若用腳本自動化,還得處理作業系統權限(如需要符號連結權限)等限制。如果你有維護引擎原始碼,將模組檔案直接放在
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 的標頭和原始檔:
#define MODULE_SUMMATOR_HAS_PREREGISTER
void preregister_summator_types();
void register_summator_types();
void unregister_summator_types();
備註
與其他註冊方法不同,這裡要明確定義 MODULE_SUMMATOR_HAS_PREREGISTER,讓建置系統知道在編譯時要包含對應方法呼叫。模組名稱必須轉為全大寫。
#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.
}
撰寫自訂說明文件
撰寫說明文件或許乏味,但強烈建議為新模組寫文件,這樣使用者才容易受惠。況且,一年前你寫過的程式碼,未來看起來可能和別人寫的沒兩樣,所以請善待未來的自己!
設定模組自訂說明文件需經過以下步驟:
在模組根目錄下新增一個資料夾,名稱可自訂,本教學以
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 檢查是否遺漏了某些類別,方法是用 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
...
現在可以產生說明文件:
只要執行 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 的單元測試流程,請參考 單元測試。
步驟如下:
在模組根目錄新增一個
tests/資料夾:
cd modules/summator
mkdir tests
cd tests
建立新的測試套件:
test_summator.h。標頭檔需以test_為開頭,讓建置系統能自動收集並包含於tests/test_main.cpp執行測試。寫幾個測試案例。例如:
#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
以
scons tests=yes編譯引擎,然後用以下指令執行測試:
./bin/<godot_binary> --test --source-file="*test_summator*" --success
你現在應該會看到所有測試通過。
新增自訂編輯器圖示
就像你可以在模組中撰寫獨立說明文件一樣,也能為類別製作自訂圖示,讓它們在編輯器裡顯示。
實際建立並整合編輯器圖示,請先參考 編輯器圖示。
建立好圖示後,請依下列步驟進行:
在模組根目錄建立一個叫
icons的資料夾。這是引擎尋找模組編輯器圖示的預設路徑。將你新做的
svg圖示(無論有無最佳化)都放進這個資料夾。重新編譯引擎並開啟編輯器。你的圖示會在適當位置顯示於編輯器介面。
如果你想把圖示存放在模組其他位置,請在 config.py 加入下列程式碼,覆寫預設路徑:
def get_icons_path(): return "path/to/icons"
總結
記得:
繼承時使用
GDCLASS巨集,這樣 Godot 才能正確包裝該類別。使用
_bind_methods將函式繫結到腳本,並讓它們支援訊號回呼。避免將多重繼承的類別暴露給 Godot,因為
GDCLASS不支援多重繼承。只要類別未暴露給 Godot 腳本 API,仍可自行多重繼承。
不過要注意,根據用途不同,還會遇到許多(希望是正面的)驚喜。