※ 注意:本稿は下記の日本語訳です。
はじめに
GDExtension の C++ バインディングは、C GDExtension API を基盤として構築されており、C++ を使用して Godot のノードやその他の組み込みクラスを「拡張」するためのより洗練された方法を提供します。この新しいシステムにより、静的にリンクされた C++ モジュールと遜色ないレベルまで Godot を拡張することが可能です。
GitHub の godot-cpp リポジトリにある test フォルダにはサンプルが収録されていますので、ダウンロードしてお試しください。
プロジェクトの設定
必要な前提条件がいくつかあります:
- Godot 4 実行可能ファイル
- C++コンパイラ
- ビルドツール(SCons)
- godot-cpp リポジトリ
ビルドツールは Godot をソースコードからコンパイルする際にも必要となるものと同じです。「IDE の設定」と「コンパイル」についても必要に応じて参照してください。
godot-cpp リポジトリは GitHub からダウンロードするか、Git を使用してクローンすることもできます。リポジトリには Godot のバージョンごとに異なるブランチが存在することにご注意ください。 GDExtension は Godot の旧バージョンでは動作しません (Godot 4.x 以降のみ対応) 。同様に、古いバージョンの godot-cpp を新しい Godot で使用することもできません。 適切なブランチをダウンロードするようにしてください。
警告
GDExtension の長期的な目標として、旧 Godot バージョンを対象とする GDExtension が、それ以降のマイナーバージョンでも動作するようにすることを目指しています。 ただし、逆方向の互換性はありません。 例えば、Godot 4.1 を対象とした GDExtension は Godot 4.2 でも問題なく動作しますが、Godot 4.2 向けの GDExtension は Godot 4.1 では動作しません。
しかしながら、GDExtension は現在も実験段階であるため、重大なバグ修正や重要な機能追加のために、やむを得ず互換性を損なう場合があります。 実際、 Godot 4.0 向けに作成された GDExtension は Godot 4.1 と互換性がありません (Godot 4.1 対応の GDExtension への更新が必要になります)。
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
あるいは、プロジェクト フォルダーにクローンすることもできます。
mkdir gdextension_cpp_example
cd gdextension_cpp_example
git clone -b 4.x https://github.com/godotengine/godot-cpp
概要で指定したリンクからサンプルを複製した場合、サブモジュールは自動的に初期化されません。次のコマンドを実行する必要があります。
cd gdextension_cpp_example
git submodule update --init
これにより、プロジェクト フォルダー内のリポジトリが初期化されます。
C++バインディングのビルド
前章で前提条件として挙げられているものが用意できたら、C++ バインディングを作成しましょう。
リポジトリには、現在の Godot リリースにおけるメタデータのコピーが含まれています。 もし新しいバージョンの Godot 用にこれらのバインディングをビルドする必要がある場合は、Godot 実行可能ファイルを使ってメタデータを生成する必要があります。
godot --dump-extension-api
結果として得られる extension_api.json ファイルは、実行可能ファイルのディレクトリに作成されます。それをプロジェクト フォルダーにコピーし、custom_api_file=<
PATH_TO_FILE>
を以下の scons コマンドに追加します。
バインディングを生成してコンパイルするには、次のコマンドを使用します (<
platform>
は、OS に応じて windows、linux、または macos に置き換えます)。
ビルド プロセスは、並列ビルドに使用する CPU スレッドの数を自動的に検出します。使用する CPU スレッドの数を指定するには、SCons コマンド ラインの末尾に -jN を追加します。ここで、N は使用する CPU スレッドの数です。
cd godot-cpp
scons platform=<platform> custom_api_file=<PATH_TO_FILE>
cd ..
この手順にはしばらく時間がかかります。完了後、プロジェクト内の godot-cpp/bin/ に、コンパイルされた静的ライブラリが格納されているはずです。
シンプルなプラグインの作成
次に、実際のプラグインを作成します。まず、空のGodotプロジェクトを作成し、そこにいくつかのファイルを配置します。
Godot を開いて新しいプロジェクトを作成します。この例では、GDExtension のフォルダー構造内の demo というフォルダーに配置します。
デモプロジェクトでは、"Main" というノードを含むシーンを作成し、main.tscn として保存します。それについては後で説明します。
最上位の GDExtension モジュール フォルダーに戻り、ソース ファイルを配置する src というサブフォルダーも作成します。
これで、GDExtension モジュールに demo、godot-cpp、および src ディレクトリが作成されます。
フォルダー構造は次のようになります。
gdextension_cpp_example/
|
+--demo/ # game example/demo to test the extension
|
+--godot-cpp/ # C++ bindings
|
+--src/
src フォルダーで、作成する GDExtension ノードのヘッダー ファイルの作成を開始します。名前は gdexample.h にします。
#ifndef GDEXAMPLE_H
#define GDEXAMPLE_H
#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;
};
}
#endif
上記には注目すべき点がいくつかあります。 Sprite2D クラスへのバインディングを含む sprite2d.hpp をインクルードします。 このクラスをモジュールで拡張します。
GDExtension のすべてがこの名前空間内で定義されているため、名前空間 godot を使用します。
次に、コンテナ クラスを介して Sprite2D から継承するクラス定義があります。 この副作用については後で説明します。 GDCLASS マクロは、内部でいくつかの設定を行います。
その後、time_passed という単一のメンバー変数を宣言します。
次のブロックではメソッドを定義しています。コンストラクタとデストラクタが定義されていますが、他にも一部の人には馴染みのある関数が 2 つと、新しいメソッドが 1 つあります。
1 つ目は _bind_methods です。これは、どのメソッドを呼び出せるか、どのプロパティを公開するかを調べるために Godot が呼び出す静的関数です。2 つ目は _process 関数です。これは、GDScript で慣れ親しんでいる _process 関数とまったく同じように動作します。
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++ ファイルがもう 1 つあります。このファイルを register_types.cpp と名付けます。GDExtension プラグインには、上で実装した GDExample のように、それぞれ独自のヘッダーとソース ファイルを持つ複数のクラスを含めることができます。ここで必要なのは、GDExtension プラグイン内のすべてのクラスについて Godot に通知する小さなコードです。
#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();
}
}
Initialize_example_module 関数と uninitialize_example_module 関数は、それぞれ Godot がプラグインをロードするときとアンロードするときに呼び出されます。ここで行っているのは、バインディング モジュール内の関数を解析して初期化することだけですが、必要に応じてさらに設定する必要があるかもしれません。ライブラリ内の各クラスに対して GDREGISTER_RUNTIME_CLASS マクロを呼び出します。これにより、GDScript のデフォルトと同様に、ゲーム内でのみ実行されるようになります。
重要な関数は、example_library_init という 3 番目の関数です。まず、バインディング ライブラリ内の関数を呼び出して、初期化オブジェクトを作成します。このオブジェクトは、GDExtension の初期化関数と終了関数を登録します。さらに、初期化のレベル (コア、サーバー、シーン、エディター、レベル) を設定します。
最後に、register_types.cpp のヘッダー ファイル (register_types.h) が必要です。
#ifndef GDEXAMPLE_REGISTER_TYPES_H
#define GDEXAMPLE_REGISTER_TYPES_H
#include <godot_cpp/core/class_db.hpp>
using namespace godot;
void initialize_example_module(ModuleInitializationLevel p_level);
void uninitialize_example_module(ModuleInitializationLevel p_level);
#endif // GDEXAMPLE_REGISTER_TYPES_H
プラグインのコンパイル
プロジェクトをコンパイルするには、godot-cpp 内のファイルを参照する SConstruct ファイルを使用して、SCons がプロジェクトをコンパイルする方法を定義する必要があります。ゼロから作成することはこのチュートリアルの範囲外ですが、用意した SConstruct ファイルを使用することはできます。以降のチュートリアルでは、これらのビルド ファイルの使用方法について、よりカスタマイズ可能で詳細な例を説明します。
SConstruct ファイルをダウンロードしたら、それを godot-cpp、src、demo と一緒に GDExtension フォルダー構造に配置し、次を実行します。
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
GDExtensionモジュールの使用
Godot に戻る前に、demo/bin/ にもう 1 つファイルを作成する必要があります。
このファイルは、各プラットフォームにロードする必要がある動的ライブラリとモジュールのエントリ関数を 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": ""
}
このファイルには、モジュールのエントリ機能を制御する設定セクションが含まれています。また、互換性のある最小の Godot バージョンを compatability_minimum で設定する必要があります。これにより、古いバージョンの Godot が拡張機能をロードしようとしなくなります。reloadable フラグにより、エディターを再起動せずに、再コンパイルするたびにエディターによって拡張機能が自動的に再ロードされます。これは、拡張機能をデバッグ モード (デフォルト) でコンパイルした場合にのみ機能します。
ライブラリ セクションは重要な部分です。これは、サポートされている各プラットフォームのプロジェクトのファイルシステム内の動的ライブラリの場所を Godot に伝えます。また、プロジェクトをエクスポートするときにそのファイルだけがエクスポートされるため、データ パックにはターゲット プラットフォームと互換性のないライブラリが含まれません。
最後に、依存関係セクションでは、含める必要のある追加の動的ライブラリに名前を付けることができます。これは、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 ノードをシーンに追加します。
Godot ロゴをテクスチャとしてこのノードに割り当て、centered プロパティを無効にします。
プロジェクトを実行する準備ができました:
プロパティの追加
GDScript では、export キーワードを使用してスクリプトにプロパティを追加できます。GDExtension では、プロパティを getter および setter 関数で登録するか、オブジェクトの _get_property_list、_get、および _set メソッドを直接実装する必要があります (ただし、これはこのチュートリアルの範囲をはるかに超えます)。
波形の振幅を制御できるプロパティを追加してみましょう。
gdexample.h ファイルでは、メンバー変数と getter および setter 関数を追加する必要があります。
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 アイコンが大きな図形に沿って移動することがわかります。
アニメーションの速度についても同じことを行い、セッター関数とゲッター関数を使用します。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 という別のプロパティが表示されます。この値を変更すると、アニメーションが速くなったり遅くなったりします。さらに、値の範囲を示す range プロパティを追加しました。最初の 2 つの引数は最小値と最大値で、3 番目はステップ サイズです。
シグナル
最後になりましたが、シグナルは GDExtension でも完全に機能します。拡張機能を別のオブジェクトから発行されたシグナルに反応させるには、そのオブジェクトで connect を呼び出す必要があります。揺れる Godot アイコンの良い例が思い浮かばないので、もっと完全な例を紹介する必要があります。
必要な構文は次のとおりです。
some_other_node->connect("the_signal", Callable(this, "my_method"));
他のノードからのシグナル the_signal をメソッド my_method に接続するには、シグナルの名前と Callable を connect メソッドに提供する必要があります。Callable は、メソッドを呼び出すことができるオブジェクトに関する情報を保持します。この場合、現在のオブジェクト インスタンス this をオブジェクトのメソッド my_method に関連付けます。次に、connect メソッドは this を the_signal のオブザーバーに追加します。the_signal が発行されるたびに、Godot はどのオブジェクトのどのメソッドを呼び出す必要があるかを認識します。
my_method を呼び出すことができるのは、_bind_methods メソッドで以前に登録した場合のみであることに注意してください。そうでない場合、Godot は my_method の存在を認識しません。
Callable について詳しくは、クラス リファレンスの Callable を参照してください。
オブジェクトから信号を送信する方が一般的です。揺れる Godot アイコンでは、動作を示すためにちょっとおかしなことをします。1 秒が経過するたびに信号を送信し、新しい位置を渡します。
gdexample.h ヘッダー ファイルで、新しいメンバー time_emit を定義する必要があります。
...
double time_passed;
double time_emit;
double amplitude;
...
gdexample.cpp の変更は今回はもう少し複雑です。まず、_init メソッドまたはコンストラクターで time_emit = 0.0; を設定する必要があります。ですが、他の2つは、必要な変更を1つずつ見ていきます。
_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 引数を使用した単一の呼び出しになります。MethodInfo の最初のパラメータはシグナルの名前になり、残りのパラメータは、メソッドの各パラメータの要点を記述する PropertyInfo 型になります。PropertyInfo パラメータは、パラメータのデータ型で定義され、次にパラメータがデフォルトで持つ名前で定義されます。
そこで、ここでは、信号に「position_changed」という名前を付ける MethodInfo を持つ信号を追加します。PropertyInfo パラメータは、2 つの重要な引数を記述します。1 つは Object 型、もう 1 つは Vector2 型で、それぞれ「node」と「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;
}
}
1 秒が経過したら、シグナルを発信してカウンターをリセットします。パラメータ値を emitting_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 のノードを制御する本格的なスクリプトを作成できます。