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需要三個參數,分別是 : ( 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(),左方是新類別,右方是被繼承的類別。


最後在register_type.cpp的初始化函式中登記上這個新類別便可以進行編譯了。

register_type.cpp


至於編譯的部分一樣就是在build資料夾簡單的用cmake .. –fresh && cmake –build .便完成編譯了。

GDExtension使用


1. 創建一個新的Godot Project。

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。

結語

GDExtension的使用大概如上,但其實還有一大堆奇奇怪怪的東西,例如_binding_method等等,但暫時就先不談太多枝尾末節的事。

另外,如果覺得設置起來太麻煩的話,我寫了一個CMakeList放在Github上,按一個Batch檔案就設置好了,再把bin資料夾複製到Godot Project便可。但主要支援的是Windows。
https://github.com/Oxoi5583/godot-cxx-ez-setup

最後,提一提GDExtension對MacOS的支援比較差,Hot Reload經常失效,最好每一次編譯都重開一下Godot Engine。

Published by