この記事は、私が Godot エンジンを使ってきた経験の中で、「うまくいったこと」と「うまくいかなかったこと」について書いたものです。
私は約 1 年間、Godot エンジンを使用してきました。最初は GDScript でスクリプトを書いていましたが、最適化の要件から C++ に切り替えました。Godot と C++ を組み合わせて使うのは、間違いなく挑戦的な体験でした。
Godot で主に使われているスクリプト言語は GDScript と C# であり、C++ は主に GDExtensions 用、つまりプロジェクトとは別にコードを書き、それを共有ライブラリとしてコンパイルし、実行時に Godot が読み込む仕組みのために使われるものだと理解しています。
1. GDExtensions のドキュメント不足
まず最初に、GDExtensions にはまだ十分なドキュメントがありません。そのため、何かがうまく動かないときには、手探りでさまざまな方法を試し、「これで動いてくれ」と祈るような気持ちで作業することが多々ありました。
GDExtensions のセットアップ方法については詳しいドキュメントがありますが、それ以降の実践的な部分については情報がかなり少ないです。
2. 非常に長いコンパイル時間
次に、GDExtensions のコンパイル時間が非常に長いという点です。
- 初回コンパイルでは、すべてをビルドするのに 10〜20 分 かかります。
- これは自分の拡張コードだけでなく、Godot エンジン本体のファイルもすべてコンパイルされるためです。
- その後、変更を加えて再コンパイルする場合は、変更されたファイルのみが再ビルドされるため 1〜2 分程度 で済みます。
初回ほどではないにせよ、決して速いとは言えません。また、毎回まったく同じコマンドと条件でコンパイルする必要があります。コンパイルコマンドに少しでも違いがあると、Godot はすべてを最初から再コンパイルする必要があると判断し、再び 20 分かかってしまいます。
3. GDExtensions でのノードの扱い方
GDExtensions では、プロジェクト内のノードを扱う方法が 2 通り あります。
方法 1: 既存ノードを取得する
1 つ目は、プロジェクト内で物理的に作成されたノードを get_node() を使って取得する方法です。
また、ヘッダーファイルで以下のように宣言した場合:
Button *button;
この宣言はノードそのものではなく、Godot のシーンツリー内に存在するノードへのポインタである点に注意が必要です。
方法 2: 実行時にノードを生成する
2 つ目の方法は、memnew を使って 実行時に新しいノードを生成する方法です。
この方法の欠点は以下の通りです。
- エディタでプロジェクトを開いたときにそのノードが見えない
- 修正したい場合は コード上でしか変更できない
それでも最終的には、私は 2 つ目の方法をおすすめします。
私のプロジェクトは重く複雑で、原因不明の理由で シーンツリー内のノードが壊れてしまう ケースに何度も遭遇しました。その結果、エディタ上で シーンを最初から作り直さなければならない こともありました。
4. C++ とメモリ管理の難しさ
4 つ目の重要な話題は、C++ 自体とメモリ管理についてです。私の場合、メモリ関連の問題が最も厄介でした。
Godot では、シーンを切り替えると前のシーンは自動的に削除され、メモリから解放されます。しかし、実際に完全に削除されるまでには少し時間がかかり、シーンの大部分が消えた後でも、いくつかの要素が数秒間残り続けることがあります。
node->queue_free(); を使えば早めに解放することもできますが、そうすると今度は レースコンディション の問題が発生します。ノードを早く解放しすぎると、Godot がクラッシュしてしまうのです。
何を、いつ解放すべきなのかを理解するまでには、かなり時間がかかりました。
5. ready() と動的ロードの問題
レースコンディションの話で言うと、私の経験では Godot は ready() 内でノードを読み込むことをあまり好まない 場合があります。
これは、
- ノードが早く解放されすぎている
- まだ完全にロードされていない
といった問題が関係しているのかもしれません。
GDExtensions では、動的ロードが最も安定して動作すると感じました。本来しばらく表示されるべきでないノードを ready() 内で 非表示の状態で事前ロードすると、Godot がクラッシュすることがありました。
そのため、必要になる直前にノードをロードするように変更しました。
6. GDB を使ったデバッグ
GDExtensions のデバッグにおいて非常に役立ったのが **GDB(GNU デバッガ)**です。
一度、Godot がクラッシュする原因を理解するのに 2 日間 費やしたことがありました。Godot のログからは「いくつかのノードが早く解放されている」ことは分かりましたが、
- どのノードが
- いつ
問題を起こしているのかまでは分かりませんでした。
デバッグ手順
- コンパイル時に以下を追加してビルドします。
debug_symbols=yes
- Godot プロジェクトのあるフォルダでターミナルを開き、GDB を起動します。
- GDB モード内で
runコマンドを実行すると、Godot プロジェクトが起動します。 - クラッシュが発生した後、GDB モードで以下を実行します。
bt full
これにより、何がどこで問題を起こしたのかを詳細に確認できます。
この方法を使って、すでに解放された後にも呼び出されていた 「暴走したタイマー」 がクラッシュの原因であることを突き止めることができました。このデバッグ方法は、その後も何度も私を助けてくれました。
おわりに
以上が、Godot の GDExtensions と C++ を使う中で、私が特に苦労した主なポイントです。今後、続きを書くかもしれません。