C++ 自分用新手筆記4 : Godot CMake C++ 綁定 GDExtension 專案設置
引言
Godot Engine 4 能夠使用C++來編寫與自訂Node的行為的方式名為GDExtension,對於喜歡C++這門語言,但又不想from scratch重寫一個遊戲引擎的獨立開發者來說可以說是一大福音,畢竟能使用C++作為Scripting語言的遊戲引擎的確不多,要不然就是能使用C++作為Scripting語言但Users base卻非常小,能找到的技術支援也不多,要不然就是要用Unreal Engine,但我相信使用過Unreal的人都能了解,其臃腫的程度對一個獨立開發者而言絕對是一個困擾。
而Godot Engine作為一個近期發展快速、且社群龐大的新晉引擎,GDExtension絕對是個十分有吸引力的方案,但其設置的麻煩度也無奈的讓Godot Engine的入門者有點卻步,因此想寫一篇文章分享在GDExtension設置上的研究心得。
工具
本文會用到以下的工具,記得把它們都安裝好再往下看。
1. C++編譯器,如g++、appleclang、MSVC都可以。
2. CMake,版本3.24版本以上
3. Godot Engine,版本4.3
GDExtension如何運作 ?
GDExtension簡單而言就是由Godot Engine讀取使用者編寫的動態庫 (即 .dll / .so / .dylib 檔),並把動態庫中自訂的Node implement到Godot Engine中,其行為步驟如下 :
1. 開啟Godot Engine,並開啟任一專案。
2. Godot Engine每一次開啟會都檢查專案中是否存在res://bin的folder
3. 存在的話,會在bin資料夾裡搜尋以「.gdextension」為結尾的文字檔案
4. 在*.gdextension檔案會列出需要被連結的動態庫檔案路徑(以 Godot 中的 res://path/to/file 呈現)
5. 在*.gdextension檔案還會列出動態庫Entry Point的Function名字
6. 讀取完*.gdextension檔案便會以檔案中提供的Entry Point進入動態庫中
7. 在動態庫中,Godot Engine會被要求登記動態庫裡的自訂類別
8. 回到Editor,在Godot Engine中按下Add Node便會出現剛才動態庫中的自訂Node
雖然在Github和官方Documents裡看到的GDExtension設置過程看起來十分繁瑣,但從上文提到的過程可以看到其實設置起來一點也不難理解,簡化而言就是,先在Godot專案裡開一個bin資料夾,放入*.gdextension檔案,再放入動態庫檔案,完成。
動態庫編譯1
GDExtension設置起來並不複雜,只有三樣東西需要使用者製作,.dll、.gdextension、bin資料夾。但問題是其動態庫如何製作出來呢?
這部分其實也不難,如果對CMake動態庫編譯完全不了解的話,可以先看看我之前的另一篇文章( https://oxoio.blogspot.com/2024/08/c-3-cmake-ii.html )。當然如果你不想用CMake,而是用Godot 官方文件中也有使用的Scons來進行編譯也不會有什麼大問題,但畢竟我比較喜歡CMake,因此接下來出現的示範也會使用CMake來編譯。
首先,在考慮動態庫的事情之前,我們要先取得一個我們編寫動態庫時會用到的Library,不然我們手上什麼都沒有的話,可是什麼都寫不出來的。而這個Library是Godot CPP Binding的靜態庫,如果對靜態庫完全不了解的話可以看一下剛才提到的那篇文章( https://oxoio.blogspot.com/2024/08/c-3-cmake-ii.html )。
這個Library會有Godot Engine裡的各種Node的duplication以及各種會用到的數據類型,例如 Vector2等等。
編譯Godot-CPP靜態庫的過程十分簡單,畢竟大多數需要的文件Godot官方已經準備好了。
我們只需要 :
1. 從https://github.com/godotengine/godot-cpp下載這個庫的代碼 或者用 git clone來下載
2. 用cmd.exe或terminal進入下載下來的代碼的根目錄
3. 用mkdir build創建一個build資料夾
4. 用cd build進入資料來
5. 用cmake或scons來編釋靜態庫,如果使用的是cmake的話,
請用cmake -DCMAKE_BUILD_TYPE=Debug .. --fresh && cmake --build . --config Debug
以及cmake -DCMAKE_BUILD_TYPE=Release .. --fresh && cmake --build . --config Release
因為Debug和Release版都會用到,所以分別兩個被本都要build一次。
(對CMake不太了解的可以看這篇文章( https://oxoio.blogspot.com/2024/07/c-2-cmake-i.html )。 )。
完成編譯後,在godot-cpp/bin/Debug以及godot-cpp/bin/Debug會找到相應版本的靜態庫檔案,
如果是MacOs使用AppleClang來編譯的話應該會叫libgodot-cpp.windows.release.64.dylib,在Linux上使用g++編釋的話則會叫libgodot-cpp.windows.release.64.so,而果你在Windows上使用MSVC來編譯的話則會叫godot-cpp.windows.release.64.lib。
** 新手HINT :Github上有大量的第三方函式庫可以使用,一般Repo的管理者都會在Release裡提供已經編譯館好的檔案,
但個人建議使用任何第三方函式庫都最好自己編譯源代碼,因為使用不同的編譯器,編譯出來的成果未必能夠通用,
像我前陣子在玩SDL2庫時,官方Release下載下來的是GNU編譯的格式,而我在測試時用的是MSVC,因此,在編譯的時候便會比較麻煩,
例如要在CMake中加入改Preffix和Suffix之類的語句。
動態庫編譯2
在完成godot-cpp的編譯後,是時候回來處理我們的動態庫了。
關於動態庫,godot-cpp下載下來的檔案裡有一個godot-cpp/test的資料夾,那是我們最終會使用到的動態庫的代碼範例。無論是Cmake和Scons的範例都有。
我們來研究一下這個範例,便能大概了解如何寫一個GDExtension了。
首先是CMakeLists.txt的部分
在17行至27行有一系列的變數設置,我先不吐嘈為什麼上四行set是小寫,下六行set是大寫,
這一些變數分別是指定最終Build出來的成品所在的路徑至root/bin/build_type。
有趣的是,我在MacOs上用AppleClang編譯時,動態庫會被編譯至CMAKE_LIBRARY_OUTPUT_DIRECTORY的路徑,但如果我在Windows上用MSVC或g++編譯的話,會成品會出現在CMAKE_RUNTIME_OUTPUT_DIRECTORY上。
在92行至100行有兩個指令,分別是add_library()以及target_include_directories()。
於93行的add_library()是用來命令CMake把${SORUCES}和 ${HEADERS}組合成最終的動態庫檔案。
而95行的target_include_directories()是用來提示CMake到三個不同的路徑搜尋godot-cpp靜態庫會需要的頭文件。
而GODOT_GDEXTENSION_DIR是 godot-cpp/gdextension
而CPP_BINDINGS_PATH也就是我們從Github下載下來的godot-cpp根目錄。
回到godot-cpp的根目錄看,使用到的分別是上圖的這三個資料夾。
最後124行至137行是連結我們在上一部分剛編譯出來的靜態庫(aka .a / .lib 檔)。
而這個靜態庫會在godot-cpp/bin搜尋。
看完CMakeLists.txt的部分,是時候創建一個比較乾淨的專案資料夾,讓我們能集中精神在動態庫的編寫上。
1. 首先,我們開一個獨立的新資料夾,暫時把它命名為gdextension,
2. 再來把godot-cpp的gen和include以及gdextension複製到新資料夾gdextension中
3. 開一個新的子資料夾,並命名為lib,並在把godot-cpp/bin/Debug和godot-cpp/bin/Release的靜態庫檔案複製到gdextension/lib中。
4. 開一個新的子資料夾,並命名為src,以便我們放置我們未來寫的代碼。
5. 把godot-cpp/test中的CMakeLists.txt複製到gdextension的根目錄裡。
6. 最後分別把剛才看到4行和5行的GODOT_GDEXTENSION_DIR和MAKE_RUNTIME_OUTPUT_DIRECTORY
修改成 ./gdextension以及 ./
7. 完成 ! 可以開始寫我們的GDExtension了 !
編寫GDExtension
GDExtension需要一個用來供Godot Engine進入至動態庫的Entry Point,而這個Entry Point的文件名在官方的範例中叫做register_types.cpp,但這名字並不是必須的,你想改成任何文件名都可以,而範例中也有把register_types分成頭文件以及代碼文件,但因為register_types裡的Function不會被其他文件調用,所以就算把頭文件以及代碼文件結合回去一個register_types.cpp也是可以的。
如果不太能理解它的邏輯,其實可以簡單的把register_types.cpp當成一般C++程式中的main.cpp。
接下來,在register_types.cpp加入一個動態庫的Entry Point(可以理解成一般C++程式中的main Function)。這個Entry Point的函式名字隨你便,在範例中被叫做example_library_inti()。在這裡我們也把它命名為example_library_inti()。
這個Entry Point需要三個參數,分別是 : ( <Type> para_name )
1. GDExtensionInterfaceGetProcAddress p_get_proc_address
2. GDExtensionClassLibraryPtr p_library
3. GDExtensionInitialization *r_initialization
再者,因為是動態庫對外的介面( Interface ),因此,要把這個函式包括在 extern "C"{}之中,外加GDE_EXPORT的標識。最後在函式要把回傳一個GDExtensionBool給Godot Engine (類似於main function 的return 0),而這個GDExtensionBool是由一個GDExtensionBinding::InitObject產生的,因此,我們也還要再實例化一個InitObject。
完成後會如下圖 :
在這個Entry Point函式裡,我們會做三件事情,
1. 回傳一個用來在引擎登記我們自訂類別的函式,在範例裡叫做initialize_example_module() (名字任意)
2. 回傳一個用來在引擎關閉我們自訂類別的函式,在範例裡叫做uninitialize_example_module() (名字任意)
3. 最後還要設定初始化等級,先不要管這是什麼,我們先設為MODULE_INITIALIZATION_LEVEL_SCENE
最後成品會如下圖 :
到這裡,一個完整的Entry Point便完成了。接下來便是編寫一個GDExtension的自訂類別。
創建一個自訂類別也不會太難,簡明扼要的說就是定義一個Class,並繼承一個Godot的類別。最後再於register_type.cpp中登記該Class,便可以進行編譯了。
example.h
exmple.cpp
這部分與一般C++程式碼差異不會太大,便不過多描述了。比較要留意的是 :
1. Print Function在UtilityFunction類別中。
2. 在頭文件中_process與_ready需要加上override的後綴詞。
3. 每一個GDExtension的類別都需要一個名為_bind_method的函式。
4. 在頭文件中,類別的定義都需要在最上方加上GDCLASS(),左方是新類別,右方是被繼承的類別。
2. 在Godot Project中創建一個bin資料夾
3. 在bin資料夾中,新增一個*.gdextension( * 代表名字隨意取 ) ,
內容要告訴Godot Engine你dll檔案的Entry Function,以及 最低版本,還有dll檔的路徑。
最後放入dll檔。
4. 給Godot Engine Debug Build 一下,Godot Engine便會Refresh。
5. 在創造新Node的頁面就可以找到你寫的GDExtension。
https://github.com/Oxoi5583/godot-cxx-ez-setup