見出し画像

Godot Engineでつまづいたところ集 1

Godotエンジン使ってます。ノウハウは複数の非公開記事にメモ代わりに書き溜めているところです。それらはある程度Godotに慣れて来て、考えも固まってきたら公開しようかな、と思ってます。
でもこの記事、『つまづいたところ集』くらいは、不完全な状態でも公開していいかな、と思ったので公開する次第です。随時追記していきます。

公開: 2023/04/03
最終更新: 2023/09/23

以下、特に脈絡なく、「失敗や気付き」を書き連ねます。


設定画面の見間違い
エディター設定とプロジェクト設定は、設定画面がぱっと見似ている。探している項目が見当たらない時は、間違えて別の画面を開いている。
あるある。


count回ループするfor文
for文でcount回ループするときは、for i in range(0, count)。
for i in [0, count]は間違い。この場合、inの右の部分は0とcountの2要素からなる配列になってしまう。2回しかループしない。


配列の配列
Arrayは中身を型指定できるが、型のネストはできない。例えばArray[Array[MyClass]]はNG。
MyClassの2次元配列を型安全に使いたければ、X×YサイズのArray[MyClass]で代替するしかなさそう。


enumはクラスに属す
enumは他クラスで定義していても、ちゃんと使用可能。例えば、MyClass. MyEnum. ENUM_ITEM_0 のように書く。
ちなみに、未保存でも構文解析して評価してくれるVSのインテリセンスと違って、my_class.gd側をちゃんとファイル保存していないと、「そんなenumは無い」とエディタに怒られる。これはenumに限らず、クラスや関数の定義でも同様。


コードエディタの矩形選択
GDScriptのエディタは、普通のテキストエディタのように、マウスによる矩形選択はできない。その代わり、Shift + Ctrl + カーソルキーで、矩形選択できる。ただし、操作は慣れるまで少し難しい。


「参照の検索」は無い
GDScriptのエディタは、VSのような「参照の検索」機能が無い。なので文字列による検索を頼ることになる。コードもそれを意識した書き方をしたほうがよさそう。
ちょっと冗長に書いたほうがいい。Cellクラスのメンバ変数_cell_index、これはC#なら_indexに直したくなる。だが、"index"というワードはいろんなクラスや関数で使うだろう。なので、_cell_indexのほうが、検索性が上がる。
(関係無いけど、indexはidxと略して書いたほうがGodot風である)


_readyと_init
_readyはノードがシーンツリーに追加されたときに呼ばれる関数。UnityのStart関数のようなものであって、コンストラクタ的なものではない。Objectのnew時に呼ばれるのは_init。こっちがいわば、コンストラクタ。

慣れるまでは何度か間違える。非ノードクラスに_readyを定義してしまっても何もワーニング等出ないので、誤りに気付かないことも。

_init: コンストラクタ
_ready: ノードの初回更新時に呼ばれる
_process: ノードの更新
_update: そんな関数は無い
_proc: こんな関数も無い


あえてオーバーライド前の関数を呼ぶ
親クラスの同関数をオーバーライド先のクラスから呼ぶには、キーワード"super"を使って"super.my_func()"と書く。
ちなみにGodot3では、ドットだけを使って".my_func()"のように呼ぶ書式だったが、Godot4でsuperに変わった。わかりやすくなった。


整数の割り算はワーニング対象
整数同士の割り算をしようとすると、「余りは捨てられるけどいいの?」とワーニングが出る。奇妙に感じるメッセージだが、これはPythonユーザーを意識したワーニング。Pythonは整数同士の割り算で自動的にfloatになる仕様なので。
プロジェクト設定から、このワーニングをオフにできる。もしくは、floatにキャストしてから割り算をして、intにキャストしなおすことでも回避できる。


enumでループ
C#のようにenumでforeachしたい場合、for mode in Enemy.Mode:とやると、名前(String)でループしてしまう。for mode in Enemy.Mode.values():とすれば、enumの値(int)でループされる。
ちなみに、どうやらenumの値は暗黙にintのようで、とくにキャストとかせず、my_array[mode]などのように、配列の添え字に使用可能。


Blenderのバージョンは古いとNG?
blendファイルのインポートでERR_FILE_CANT_OPENエラーが出て上手くいかないときは、Godotが参照しているBlenderのexeのバージョンが、ファイルよりも古い。
さらにどうやら、ファイルに対してBlenderのバージョンが「新しくても」エラーが出る(のかもしれない)。Blender3.2で保存したファイルを3.4のexe設定で開こうとしてもエラーが出た。


テスト実行とシーンエディタ
エディタ上のゲーム実行(UEの用語でPIE = Play In Editor)の話。
Godotはエディタ上で実行中に、ライブなシーンの状態をビジュアル的に見ることができない。UnityやUEのエディタは、PIE中にワールドの状態をエディタ上で可視化する。それがない。

確かに、ツリーのヒエラルキーや各ノードのパラメータは、シーンタブの「リモート」を選択することで見ることができる。しかし一方で、シーンエディタに映っている映像は、PIEの状況を視覚的に表したものではない。そもそも、この画面はシーン(tscnファイル)を編集するためのもので、デバッグする用途ではないのだ。

なので、PIE中に編集画面上で何も動いていないのは、正常である。これは多分Unityとかやってた人は、最初引っかかると思う。

結局、ゲーム画面で確認するしかない。一応救済機能がある。エディタには、「プロジェクトカメラのオーバーライド」というボタンが備わっている。カメラのアイコンだ。それをオンにすると、PIEのゲーム画面が強制的に、シーンエディタ上の視点で描画されるようになる。PIE中にエディタの視点を動かすと、ゲーム画面の視点にリアルタイムに反映される。これがデバッグ機能として想定されているようだ。


コード生成メッシュのワイヤーフレームを見る方法
通常、メッシュはシーンエディタ上でワイヤーフレーム表示することができる。しかし、ランタイムで動的に作成したArrayMeshは、通常はワイヤーフレームで見る手段が無い。なぜなら前述の通り、実行中の状態はエディタで見ることができないから。
どうしてもエディタ上で形状を確認したければ、@toolを先頭に付けたデバッグ専用のビューワーシーンを作成するアイデアがある。ビューワーシーンの_ready内でメッシュを作成すればよい。


デフォルトの背景色の落とし穴
StandardMaterial3Dを作成して、デフォルト値のまま使用すると、ノーマルの無いメッシュなどは、カメラの背景色のデフォルト値(暗い灰色)とまったく同じRGB値で描かれる。結果的に何も映ってないように見える。これは、知ってないと「なぜオブジェクトが見えない?」となる。映ってないのではなく、背景と同化しているだけだ。

カメラの背景色のデフォルト値(暗い灰色)は、「アンビエント以外でライティングされていない白い物体の色」なのだ。そういう仕様にした経緯はなんとなく解るけど、慣れるまではデフォルト値は変えてしまったほうがいい。
プロジェクト設定の、「レンダリング>環境>デフォルトのクリアー色」から変更可能。


エディタ実行コードとstatic関数
@toolを先頭に付けたデバッグ用のシーンでは、class_name経由で他の自作クラスのstatic関数をいきなり呼び出すことができない。"Invalid call, Nonexistent function" のエラーになる。スクリプトをloadすればいけそう。


play関数の引数
AnimationPlayerノードのplay関数の引数となるのは、"ライブラリ名/アニメーションキー名"。例えば、"MyAnimLib/Evade"。"Evade"だけではだめ。
AnimationLibraryのget_animation_list()で返ってくるのは、アニメーションキー名だけなので、これにライブラリ名とスラッシュを付ける必要がある。


マテリアルのパラメータ、個別変更
Unityでは、特定のオブジェクトだけマテリアルのパラメータを変えたい場合、明示的にマテリアルのコピーを作るか、MeshRenderer.materialの副作用を使って一時的なマテリアルを暗黙に作成していた。マテリアルの複製が必要なのは、Godotでも同様である。しかし、Godotでは、マテリアルはメッシュに紐づいている。ということは、たとえマテリアルをコピーしても、メッシュに直接セットしては、同じメッシュを使っているインスタンスノードはすべて見た目が変わってしまう。

メッシュに紐づいているマテリアルは、いわばメッシュの「デフォルトマテリアル」なのだ。だから動的にマテリアルを変更するには、これを変えるのではない。デフォルトを隠ぺいして上書きする手段が別途用意されている。それが、GeometryInstance3D. set_surface_override_material()だ。

これでノードにマテリアルをセットすることで、描画に使われるマテリアルが変更される。もちろん、マテリアルのコピーは自分で事前にやっておく必要がある。コピーしたマテリアルは自分のコードで保持しておいてもいいが、get_surface_override_material()でセット済みのものを再取得することもできる。(get_surface_override_material()は、ソースコード見る限り、簡単なエラーチェックの後にメンバ変数をreturnしているだけなので、パフォーマンスも問題無さそう。)


ちなみに、GeometryInstance3D. material_overlayというプロパティもある。名前が似ていてこちらにもマテリアルをセットできるので、つい混同してしまう。が、まったく別物。こちらは上塗り表現用のもの。バリアやサビ、濡れの表現などに使えるだろう。


クラス内クラスはRefCounted
Inner Class、つまりクラス内クラスは、外のクラスが何であれ、RefCounted派生クラスである。なので、free()不要。むしろfree()するとエラーになる。


InputEventでトリガーは取れない
InputEventシステムはキーやボタンが「今押された」かどうか(トリガー)を通知できない。そのような機能がそもそも無い。
確かに、InputEventKeyにはechoというプロパティがある。これはキーがリピートして押されているかどうかを示すbool値だが、押された後も数フレームはfalseを返す。(OSが検知するキーの長押しを取得しているのではないか?)なので、echo==falseでトリガーを検知することはできない。InputEventKeyのボタン版、InputEvenButtonに至っては、echoに相当するものも無い。
ある程度の規模のゲームでInputEventを入力の機構として直接使うのは現実的ではないだろう。結局なんらかのラッピングは必須だ。


InputMapのデフォルト設定は盛沢山
InputMapには、プロジェクトのデフォルト値が存在する。プロジェクト設定のInputMap画面で、「組み込みアクションを表示」をオンにすると、これらが表示される。"ui_何々"という名前のアクションがたくさん登録されている。このデフォルトアクションは、アクションに紐づいているイベント(操作)は削除できるが、アクション自体は削除できない。ちなみに、イベントを削除すると、削除した分だけ、プロジェクトファイル(project.godot)に、消した旨の記載がむしろ増えていく。
使うつもりのない設定がプロジェクトに含まれていると、つい消したくなってしまうが、消さずにそのままでいいだろう。有っても邪魔にはならないし、消してもプロジェクトがそれほどクリーンになるわけではないので。


Blenderの「無効」と「隠す」
blendファイルをインポートする際に、Blende上で「ビューポートで無効」(モニタアイコン)になっているオブジェクトは、ミラーモディファイアが適用されない。
「ビューポートで隠す」(目アイコン)になっているオブジェクトなら、モディファイアが期待通り適用される。

Blender3.0以降、特に意識せず「無効」ばかり使ってきたが、Blenderのコンセプトに従って、「無効」と「隠す」を意識して使い分けたほうがよさそう。カメラやライトのような、普段いじらないものは「無効」、編集のために一時的に見えなくしておきたいものは「隠す」を使う。こんな運用がいいと思う。


スクリプトのアタッチ
Object.set_scriptによるスクリプトのアタッチは、次の通り動作する。ドキュメントにも詳細が書かれてある。

  • 「自作クラスのオブジェクト」に対するアタッチは、機能がまったく上書きされる。新しいスクリプトに存在しない変数や関数は消える。

  • 自作クラスのサブクラスをアタッチする場合は、オーバーライドされる。
    というよりも、そもそもサブクラスがアタッチしなおされる。なので、親クラスから継承したメンバ変数は初期化される。

  • 組み込みクラスのオブジェクトに対するアタッチは、機能が共存する形になる。名前が衝突する関数はオーバーライドされる。

  • set_scriptを2回呼び出すと、新しいほうで上書きされる。2つ以上アタッチすることはできないようだ。

「自作クラスのオブジェクト」に対するアタッチは、あまり意味というか使いどころがない。オブジェクトを作り直すのと変わらないのだ。ポリモーフィズム的な使い方はあるかもしれないけど。あとは、パフォーマンスを出すための「オブジェクトプール」的な使い方とか・・・。
やはりset_scriptの使用は、組み込みクラスに自作クラスの機能を足す用途が基本になるだろう。


custom_blend
AnimationPlayer.playの引数custom_blendは、なぜかドキュメントに説明がない。これは期待通り、今再生中のアニメーションと、新しく再生するアニメーションをクロスフェードするためのパラメータだ。単位は秒だ。

ちなみに、AnimationPlayer.set_blend_timeは、play関数でcunstom_blendを指定しなかったときの、デフォルトのcunstom_blend値である。


playの多重呼び出し
AnimationPlayer.playに再生中のアニメーションを指定すると、始めから改めて再生とはならず、再生が継続される。この仕様自体は問題無いのだが、バグで毎フレーム呼んでいたとしても、そのバグに気付きにくい。

ちなみに、毎フレーム呼ぶと、custom_blendは意図しない動作をする。おそらく、playを呼んだ数だけ多重フェードする。音楽や映像でいうところの「トラック」が追加されるイメージだ。例えばwalk再生中にattackを4回playすると、walk1attack4の割合くらいでattackにクロスフェードする動きになっているように見える。
「なんかやたらフェードが速いな?」と思ったら、これを疑っていい。


Godotにとっての「前」とは
Godotではカメラもライトも-Zが前方である。Vector3.FRONTは(0, 0, -1)で、Vector3.BACKは(0, 0, 1)だ。(ちなみにドキュメントによると-Zが北らしい。)だからGodotは、-Z Frontなのだ。
しかし一方で、Blenderで慣習通りに-Y向きに作ったキャラクター(Blenderのおサルさんスザンヌなど)は、+Z向きでインポートされる。Godot公式のサンプルプロジェクト(3d/IKと3d/platformer)もまた、キャラクターの向きは+Zになっている。さらに、StandardMaterial3DのBillboard機能は、メッシュの+Z側をカメラを向かせるものだ。
どうやらGodotにおいては、モデルは後方(観測者=あなた)を向いている、という認識のようだ。これはもう、そういうものとして割り切るしかない。異文化においては物の見え方も常識も変わる。キャラクターや物体に、彼らにとって主体的な方向を向かせたければ、180度回転を加えて振り返らせる。違いを意識して取り扱うことが必要。
(以下、追記)
Godot 4.1では、Model Frontという概念が追加された。モデルは+Zが前だ。逆を言うと通常のFrontは-Zだ。向きに関して、Godotが明確に方針を示したことになる。もう迷わない。


set_surface_override_materialのバグ
MeshInstance3D.set_surface_override_materialでオーバライドマテリアルをセットすると、Object.free時にエラーメッセージが表示されることがある。

Condition "!material" is true. Returning: true

free前にnullを同関数でセットしなおすことで回避できる。
海外のフォーラムの様子を見る限り、これはGodot 4.0のバグらしい。
追記 2023/06/17:
https://github.com/godotengine/godot/pull/77326
よくわからないけど、もうすぐ解決するっぽい?
追記2023/09/14:
Godot 4.1で解決済み。バグだった模様。直ってよかった。


pause使用の注意点
AnimationPlayer.is_playing()がfalseのとき、アニメーションが最後まで再生された結果falseなのか、pause()をかけた結果falseなのか、判断する手段がおそらく無い。見当たらない。

再生位置から推測することはできない。なぜなら、pause中も、通常の再生終了後も、「再生中」ではないので、AnimationPlayer. current_animation_positionはエラーとなり取得できない。
なので、animation_finishedシグナルを受け取れるようにしておいて、事前判定するしかない。それかそもそも、pause()を使わないことだ。

こういった事情を踏まえて、筆者はAnimationPlayerをplayback_process_mode = MANUALで運用することにした。外部からクロックを与えて駆動させるモードだ。このほうが確実。


initはアタッチでも呼ばれる
set_script()によるスクリプトのアタッチでも、_init()は通常通り呼ばれる。set_script()したタイミングで呼ばれる。


継承クラス経由で親のenum呼ばない方がいい
gdスクリプトをフォルダ移動すると、"Class ○○ hides a global script class" というエラーが出るようになった。海外のフォーラムによると4.0系のバグらしい。プロジェクトをリロードすると直るとの報告もあるが、自分の場合はそれだけでは駄目だった。

下記コードで、Flameが名前衝突する旨がエラーとして出ていたのだが、water_glare.gdのFlame. MatShareMode.~ をMiniature. MatShareMode.~に修正したら、エラーが出なくなった。修正前は別に文法的に間違っちゃいないのだが、意図としておかしな記述ではあったので、なんとなく気になっていた。「もしや」と思って修正したら直った。
親クラス定義のenumを子クラス名経由で記述することが想定外なのだろう。

# miniature.gd
class_name Miniature
enum MatShareMode {
 INSTANCE_MAT,
 SHARE_MAT,
}

# flame.gd
class_name Flame
extends Miniature

# water_glare.gd
class_name WaterGlare
extends Miniature
const _MAT_SHARE_MODE: Flame.MatShareMode = Flame.MatShareMode.INSTANCE_MAT
↓ 修正後
const _MAT_SHARE_MODE: Miniature.MatShareMode = Miniature.MatShareMode.INSTANCE_MAT

BlenderとGodotのボーンの座標系の違い
BoneAttachment3DによるボーンへのNode3Dのアタッチメントは、Blender上で上向きに生やしたひねりゼロのボーンに対してアタッチしたときに、方向が(直観的な期待通りに)維持される。Godotはボーンの先端方向を「上」と捉えている。

これはBlender上で親をボーンに設定(ペアレントタイプがボーン)したオブジェクトの向きとは一致しない。
おそらく、Blenderではボーンは+Y方向(奥)に延びるのを基準としており、そのときの上は+Zだからだ。Blenderにとってはボーンの側面が上だ。

Godot: ボーンの生えてる先端方向が上
Blender: ボーンの側面が上

BoneAttachment3Dによるアタッチと向きが一致するようにしたければ、Blender上ではボーン上のオブジェクトをローカルでX=-90回転させればよい。逆に、Godot側をBlender上の向きと合わせたければ、BoneAttachment3Dに載せるNode3Dを正か負に90度回転させてやればいい。


current_animation_position取得できる条件
AnimationPlayerについて。

current_animation_positionの取得は、「is_playing()がfalseの時にエラーとなり失敗する」という認識だったのだが、違うようだ。「current_animationが""のときにエラーとなり失敗する」が有力なように思う。例えば、pause()した状態で、advance()等すると、is_playing()がfalseなのに、current_animationが取れる時がある。このときはcurrent_animation_positionも取れる。

一方で、current_animation_lengthの取得は、エラーメッセージこそ"has no current animation"なのだが、実際は「assigned_animationが""のときにエラーとなり失敗する」だと思う。current animationが""のときでも取得できることがあるのだ。

current_animation_positionは current_animation != ""なら取得OK
current_animation_lengthは assigned_animation != ""なら取得OK
こんな違いがある。


でも、ソースコード見ると、エラー出す条件同じっぽいんだよなあ。なんでなんだろうなあ・・・。

// AnimationPlayer::get_current_animation_position
ERR_FAIL_COND_V_MSG(!playback.current.from, 0, "AnimationPlayer has no current animation");

// AnimationPlayer::get_current_animation_length
ERR_FAIL_COND_V_MSG(!playback.current.from, 0, "AnimationPlayer has no current animation");

アニメーション進行を自コードで制御
AnimationPlayerについて。アニメーションの更新をGodotのクロックによる自動更新ではなく、自分で制御したい場合。playback_process_modeに ANIMATION_PROCESS_MANUALをセットすればいい。advanceを呼べば、アニメーションが進行する。

別の方法として、assigned_animationに直接アニメーション名をセットして、play()をせず、seek()で時間を進めるという手段もある。ただしseekによる更新では、シグナルやコールバックの類は発生せず、アニメーションのクロスフェードも(やり方があるかもしれないが)できない。時間管理が手の内にある安心感はあるが、マニアックなやり方。例えばプレイヤーが時間を巻き戻すゲームとかならいいかも。


アニメーションのadvanceとspeed
AnimationPlayerのadvane()は、speed_scaleとcustom_speedの影響を受ける。どちらかがゼロだと当然進まない。


RefCountedには_processも_readyも無い!
当たり前だが、RefCounted派生の自作クラスは、Node派生ではないので、_process()関数は呼ばれない。わかってても、やらかす。GDScriptはoverride宣言が不要なゆえにミスに気づかない。
同様に、_init()の間違いで定義しちゃった_ready()も呼ばれない。


BiDiとは
LabelノードのプロパティにBiDiという項目がある。
BiDi = Bi-Direction Text、バイダイ。基本的に右横書きだが、アラビア数字などは左書きで書く、双方向の文字体系のこと。アラビア文字などはこの文化。


デフォルトフォント
デフォルト(この場合、ユーザーが何も一切設定していない状態の意味)のフォントはソースコード中の<thirdparty / fonts>フォルダ内の各woff2ファイルが組み込まれて用いられているようだ。woff2はWeb用のフォントファイル形式。ソースコードを読む限り、ゲーム本体(Export Template)に組み込まれるのは、OpenSans_SemiBold.woff2のみで、46KB。たぶん英字数字と記号だけだろう。他の30個程度のwoff2ファイル、合計2.5MB程度はエディタに組み込まれるようだ。

Windows10と11では実行時の絵文字の見え方が明らかに違うので、OpenSans_SemiBoldに無い文字はOSのシステムフォントから取得しているっぽい。エディタもビルド成果物もサイズが小さいのに、仮名や漢字が問題無く表示されるのは、こういうことだろう。

ちなみに、プロジェクト設定にはCustom Fontという項目があり、これがLabelSettings.font未設定時のデフォルトのフォントになる。上記OpenSansは、いわばデフォルトのデフォルトである。
LabelSettings.font -> Custom Font -> OpenSans
このような優先順で適用される。


Labelノードの「バグらせ」方
Labelノード。
・new()後にサイズを設定していない初期状態
・autowrap_modeをARBITRARYに設定
・textをセットする
この条件をすべて満たすと、その後サイズを設定しても、sizeプロパティの動作がおかしくなる。セットしたサイズよりも大きな値が返る。
サイズセット→ update_minimum_size() → 再度サイズセット、という手順を踏むと正常に戻るが、そもそもこんな状態になること自体避けたほうがいい。

そもそも、幅がゼロの状態なのに「このテキストを適切に折り返せ」という無茶振りをするのが想定外なのだろう。Control系のノードにおいては、まずLayoutを設定してから、LabelやTextureRectの表示内容をセットするという運用にしたい。


コールバック関数の引数の数
signalとconnectとbindの関係。
コールバック関数の引数 = signalの引数 + bind引数

connect時にコールバック関数をbindすると、引数の後ろ側を呼ばれる側が決めることになる。決してデフォルト値とかではないので、呼ぶ側が上書きすることはできない。不足があっても、余剰があってもエラーとなる。型の不一致も当然エラーになる。

何個の引数をbindしたとか、コールバック関数の引数が何個あるとか、呼ぶ側は知らなくていい。自分のsignalの引数をemit_signal時に過不足無く指定するだけである。

逆に、呼ばれる側は、コールバック関数の引数の前方部分を、signalの引数と一致させなければならない。そして、自分のための付加情報を引数の後方に足す。それをbind時に指定する。

# 呼ばれる側 ----------------

func _on_sub_finished(
  arg0: int, arg1: String, arg2: Vector2,
  arg3: float, arg4: bool, arg5: Vector3
  ) -> void:

# arg3, 4, 5をbindする
signal_emit_node.finished.connect(
  _on_sub_finished.bind(123.0, false, Vector3(4.0, 5.0, 6.0))
  )

# 呼ぶ側 ----------------

signal finished(arg0: int, arg1: String, arg2: Vector3)

# arg0, 1, 2を指定してemitする
emit_signal("finished", 12, "34", Vector2(5.0, 6.0))

.godotフォルダは消していい
GDScriptエディタのオートコンプリートというか、サジェストの機能が壊れた。一度実行したり、プロジェクトを開きなおしたりすると動くようになることもあるが、それでも直らない事態に陥った。"godot autocomplete not work"あたりでググっても、「型を指定しろ」的な初歩的な問答のログしか得られない。
プロジェクトの.godotフォルダを一度消したら直った。

原因がなんにせよ、対処法はわかった。.godotフォルダは気軽に消していい。


循環依存はオートコンプリートを邪魔する?
GDScriptエディタのオートコンプリートやUnsafe Lineの機能の動作が不安定なのは、クラスの循環依存・循環定義が原因な気がする。クラスAがクラスBを使っているのに、Bが間接的にでもAを使ってるような定義だ。newしていなくても、クラス名が登場するだけで依存性が発生する。
確証はない。しかし、循環依存を無くしてからは、メソッドの候補は全部出るし、Ctrl+クリックで定義に飛べるし、書いたばかりの行のUnsafe判定は数秒待つと解除されるようになった。

Godotは3.xから4.xになるにあたって、クラスのCyclic Dependencyを許すようになったはずである。実際、ゲーム実行中の動作は問題無い。だが、エディタによるコード解析の面ではまだ発展途上なのではないか?

ストレスなくコードを書きたければ、依存性を解いていくしかない。そのためにはクラスを、抽象度や応用度、分野で分類して、関係を一方通行にするしかない。双方向の依存が要る場合は、適切にインターフェースクラスを挟んでいくことになる。


ワーニング設定の厳格化
プロジェクト設定において、デフォルトでIgnoreになっている4つの警告について、Warnに設定して使い勝手を見てみた。必要なワーニングはできるだけ出してほしいので。

  • Unsafe Property Access
    $を使って、シーン中のノードのプロパティに直接アクセスしてると、このワーニングが出る。一度キャストして変数で受けてやれば出なくなる。回避可能である。
    var shaft_y: Node3D = $ShaftY as Node3D
    shaft_y.rotation = Vector3()

  • Unsafe Method Access
    Autoloadクラス内で、自分のクラス名(Worldなど)ではなく、自分のノード名(g_Worldなど)経由でstatic関数を呼ぶと、このワーニングが出る。すなおにクラス名経由で関数を呼べばいい。回避可能。
    そもそも、存在しない関数を呼ぶと出るワーニングなのだが、それは適切にキャストしていないってことなので、本来出てほしいワーニング。
    でも、キャストが必要ということはだいたい、サブクラスへのダウンキャストか、DictionaryのValueのキャストなので、今度はUnsafe Castのワーニングが出ることになる。

  • Unsafe Cast
    ダウンキャストをする限り必ず出る、ダウンキャストは組み込みクラスの関数や(Node. dupulicateなど)、Dictionaryを使う限り、必ず実施する。なので回避不可能なワーニングである。Dictionaryを型安全に記述するすべが無い。多分無いので、どうしようもない。
    これをWarn設定にしていると、何もできない。Ignore一択である。

  • Unsafe Call Argument
    引数が型安全でない時にワーニングが出るらしいが、自分のコードでは該当箇所が無かった。これは常にONでいいと思う。回避可能。

結論:Unsafe Cast以外はお行儀よく書けば出ないワーニング。

Unsafe CastだけはIgnoreにして、残り3つ、Unsafe Method Access / Unsafe Property Access / Unsafe Call ArgumentはWarnにするのが、いい妥協点ではないだろうか。

Unsafe MethodとUnsafe Propertyに付いては、適切にキャストすることでUnsafe Castに「押し付ける」ことができるが、Unsafe CastはDictionaryで必ず出るのでどうしようもない。

ではこの運用で、安全でないキャスト(Unsafe Cast)を見逃してしまうだろうか? それは大丈夫。
GDScripのキャストはasを明示的に記述しない限り発生しない。人間が意識的にas書くんだ。Unsafe Castのワーニングが無くても十分清潔さはコントロール可能である。


Godot製ゲームのライセンス表記
(間違ってるかもなので注意。)

Godotで作ったゲームには、エンジン本体とそれが使ってるライブラリ3つのライセンス表記が必ず必要。

【Godot本体】
必要な表記は、Engine.get_license_text()で全文Stringで取得可能。

【FreeType】
Godot公式ドキュメントによれば、URLを含む、"Portions of this software ~"で始まる短い一文だけで良さそうだ。条文を関数から取得可能なのだが、勝手な解釈はしないほうがよさそう。一文だけ。
表記する年数を、Godotが用いているFreeTypeのバージョンに合わせる必要があり、それはGodotエディタのヘルプ>サードパーティライセンスを参照せよ、とのこと。

【ENet】【mbed TLS】
ライセンス条文はEngine.get_license_info()で取得できるが、その頭に付ける「Copyright 誰々」みたいなのは、手書きで用意する必要がある。公式ドキュメントを参考にしよう。条文はget_license_info()が返すDictionaryから、それぞれ、"Expat"と"Apache-2.0"をキーに取得可能。


2D on 3Dレンダリングのセットアップ
3D空間上の2D画面(オフスクリーンレンダリングした2D画面)を、スクリプトで動的にセットアップする方法。
吹き出しとか、ソウルライクの雑魚敵の体力バーとか、頭上のバフアイコンとか、ゲーセンの筐体に映る2Dゲームとか、宇宙船の操縦室の空間に浮かぶコンソール画面とか。

  1. SubViewportを作成する。

  2. SubViewportを適当なノード下に配置する。

  3. SubViewportの解像度を設定する。

  4. SubViewport下に画像や文字などの2Dノードを配置する。

  5. SubViewportから、ViewportTextureを取得する。

  6. マテリアルのカラーに、取得したViewportTextureに設定する。

2.~4.は6.の後でもいいし、前後してもいい。重要なのは、5.だ。ViewportTextureはランタイムでnewしてはいけない。


ViewportTextureのnewはNG
上記の補足。

ViewportTextureはViewportへの参照を持っていないといけない。"Viewport Texture must be set to use it."のエラーメッセージは、この参照が無い時に出る。 ViewportTextureをスクリプトで直接newして作った場合、この参照をゲーム実行中に解決する手段がおそらく無い。ソースコードを荒く検索する限り見当たらない。
(viewport_pathプロパティでViewportのパスを設定することができるが、そのパスが使われるのは、setup_local_to_scene関数内だけである。どう見たってエディタから呼ばれる関数だ。同プロパティは、シーン埋め込みのリソースを、エディタ上のスクリプト実行で作るためのものなのだろう。)

Viewportは作られた時点で、内部にデフォルトのViewportTextureを作って保持する。このViewportTextureは、作成者であるViewportが参照としてセットされているので、使用可能である。

なので、ViewportTextureを使いたければ、SubViewportから、get_textureで取得すればいい。むしろそれしかなさそう。


カメラの座標変換は自作するしかない
カメラによる座標変換。グローバル座標からスクリーン座標への変換はCamera3D .unproject_position()が用意されているが、その中間の結果を得るにはユーザ側で処理が必要。カメラ座標空間だとどこかな?と知りたければ、自分のスクリプトで計算するしかない。

# カメラ座標空間
var trans_cam_inv: Transform3D = my_cam.get_camera_transform().inverse()
var pos_cam: Vector3 = trans_cam_inv * node.global_position

# 射影空間
var aspect: float = my_width / my_height
var proj_cam: Projection = Projection.create_perspective(
    my_cam.fov, aspect, my_cam.near, my_cam.far, false
    )
var pos_proj: Vector4 = proj_cam * Vector4(pos_cam.x, pos_cam.y, pos_cam.z, 1.0)
var x: flat = pos_proj.x / pos_proj.w
var y: flat = pos_proj.y / pos_proj.w
var z: flat = pos_proj.z / pos_proj.w

# スクリーン座標空間
var pos_screen: Vector2 = my_cam.unproject_position(node.global_position)

エンジンはシェーダバイナリに渡す変換行列をどこかで作ってるはずだが、それはC++のカメラやViewportクラス内ではやっていない。これをGD側から取得する関数はどうやら無さそう。
(メモ:ソースコード RendererSceneCull::render_camera()が該当する処理の起点のはず。)


Blender exeへのパスが消えた
エディタが急に、blendファイルを自動でインポートしなくなった。同時に、エディタのファイルシステムタブ(res://下のファイル一覧のある画面)に、blendファイルが表示されなくなった。
エディター設定を開くと、「Blender 3 Path」がなぜか空欄になっていた。再設定しなおすと、正常にblendファイルを認識するようになった。何がきっかけでエディタ設定が壊れたのかは不明。


Arrayでも+=は使える
Array[float]に対して、加算代入演算子(+=)は使用可能。
array[idx] += 0.1 ←これはOK。ちゃんと書き変わる。
本来Arrayはなんでも入るコンテナだからこういうのはダメなのかな、と勝手に思ってた。なんとなく。


mipmap設定方法
CanvasItem(Node2D派生とControl派生)で表示するテクスチャは、デフォルトではmipmap表示されない。mipmapを適用する方法。

1. テクスチャのインポート設定
 「ミップマップ>Generate」にチェックを入れる

2. CanvasItem.texture_filterに値をセット
 - TEXTURE_FILTER_NEAREST_WITH_MIPMAPS
 - TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
 - TEXTURE_FILTER_PARENT_NODE(GUI上では"Inherit")※

※TEXTURE_FILTER_PARENT_NODEに設定した場合、
 フォールバック先としてプロジェクト設定が適用される。
 「プロジェクト設定>レンダリング>キャンバスのテクスチャ
 >デフォルトのテクスチャフィルタ」を次のどちらかに設定にする。
 - Linear Mipmap
 - Nearest Mipmap


posmodの使い方
添え字に使うインデックスをループさせるときの簡潔な書き方。
idx = posmod(idx + 1, COUNT)
または
idx = posmod(idx + 1, INDEX_MAX - 1)
ゲームによく使う処理が組み込み関数に入ってるのはゲーム用言語のいいところ。


_processは描画の前に呼ばれるか?
作成したノードの最初の_process関数は描画より先に呼ばれるとは限らない。条件は不明だが、描画後に初回の_processが呼ばれることがある。この場合おそらく、作成したフレームでは呼ばれておらず、その次のフレームまで呼び出しが持ち越されている。
こういった懸念があるので、描画に関わる初期化処理は_initか_readyでやったほうがいい。ただし、initで初期化するときは注意が必要である。例えば自分のスクリプトでビルボード処理をしている場合、_initでglobal_positionを設定することはできない。まだ親が無いので。


Subviewportの解像度は特別に高く取ろう
UIは「想定解像度」的なものを決めておいて、そのサイズでデザインして、実際の画面解像度に合わせてスケーリングする、という手法を採用している。Godotのドキュメントも推奨しているやり方だ。その場合、想定解像度を小さめの解像度、1280x720など、にすることもあるだろう。その方が16の倍数などで綺麗にレイアウトしやすいから。この解像度感で自分なりの規格を決めるのだ。アイコンは32pxなど。

しかしその小さい解像度でデザインしたUIを、そのままSubviewportに配置して、3D空間上のUIとして使ってはならない。落とし穴だ。Windowと違ってSubviewportの解像度は一定なので、想定解像度が実際の解像度になってしまう。これは低すぎる。さらに、3D空間に配置するということは、UIが斜めになったり拡縮したりするので、相応に高めの解像度が求められる。

なので、Subviewportの解像度とUIのControl.scaleを定数倍してあげるか、もしくは3D空間上のUIはHUD的なUIとは別のサイズ規格でデザインする、といった対応が必要になる。

長々と書いたが何が言いたいかというと、オフリーンレンダリングするときの解像度はちゃんと高めにしようね、だ。


find_childの制限
Node.find_child()は、引数のパスが絶対か相対かに関わらず、シーンツリー上に無ければ機能しない。_initの中や、newしたばかりのノードには使えない。代わりにNode.get_node()を使うべし。
node.find_child("./Sail") as Node3D # NG
node.get_node("./Sail") as Node3D # OK


Wheelと名付けたばっかりに
Blender上で名前の末尾に"_Wheel"を付けたオブジェクトは、インポート時にVehicleWheel3Dノードになってしまう。Import Hint機能によるものだ。水車小屋を作って水車部分に"Watermill_Wheel"と名付けたら、このオブジェクトが名前で取得できずバグった。

ドキュメントによると実際のキーワードは”_Wheel”ではなく"-wheel"だ。アンダースコアとハイフンの違いがあるし、大文字小文字の違いもあるし、いろいろと違うはずなのだが。よろしく解釈されしまうのだろう。
結局、"Watermill_WheelObj"と名付けたら回避できた。


レンダーターゲットのアルファチャンネル
SubViewportにUIなどをオフスクリーンレンダリングしたい場合は、SubViewportのtransparent_bgをtrueにする。こうすると、レンダーターゲットにアルファチャンネルが追加され、クリア時にアルファが0.0になる。falseの場合、たとえシェーダーでアルファに0.0を書き込もうとしてもアルファチャンネルに反映されない。
ちなみに、このレンダーターゲットをテクスチャとして使う場合には、使用側のマテリアルのtransparencyを、ALPHAやSCISSORに設定するのを忘れないようにしないといけない。


レンダーターゲットを好きな色でクリアする
レンダーターゲットのクリアカラー
【Camera3Dノードがある場合】
Environmentリソースのbackground_modeとbackground_colorが適用される。適用されるEnvironmentには優先順がある(詳細は省略)。フォールバック先として、プロジェクト設定の「デフォルトのクリア―色」が適用される。
【Camera3Dノードが無い&ViewportがWindowの場合】
プロジェクト設定の「デフォルトのクリアー色」が適用される。
【Camera3Dノードが無い&ViewportがSubviewportの場合】
RGB = オール0.0でクリアされる

上記のようになっている。このため、2Dノードだけを置いたSubviewportは、強制的に黒をバックに描画される。これの何が問題かというと、UIである。UIをSubviewportに配置して、3D空間上に配置するような場合だ。UIなので矩形以外の形をしていることが多い。Labelノードで文字を描画することもあるだろう。このときに、アルファ=0.0の境界がフィルタの関係で黒を拾ってしまい、強制的に暗くなる。これではデザインの自由度が下がってしまう。

回避策1
描画対象が2Dしかないけど、あえて3Dのカメラを置く。
ダミーのCamera3DノードをSubViewport下に置く。その上で、EnvironmentがViewportに適用されるようにセットアップする。例えばWorldEnvironmentノードを配置する、など。
スマートな実装ではないし、パフォーマンス的にも悪いかもしれないが、簡単。

回避策2
ビューポート全体を覆うようにTextureRectを配置する。この矩形は、シェーダーマテリアルで描画する。シェーダーは次の通り。
shader_type canvas_item;
render_mode blend_disabled; // アルファ含めてそのまま出力
void fragment() { // 白でクリアしたい場合
  COLOR.r = 1.0;
  COLOR.g = 1.0;
  COLOR.b = 1.0;
  COLOR.a = 0.0;
}

回避策3
下記フォーラムによると、RenderingServer.set_default_clear_colorを使うといいらしい。が、上手くいかない。なんでだろう?

2019年の投稿だが、SubViewportのクリア色に関してはこのころから問題視されていたようだ。

it appears individual viewport clear colors is a relatively noticeable oversight in Godot


_init内でも誰かの子になれる
ノードの_init関数内で、自分自身を他のオブジェクトの子にするのは問題ない。

func _int(new_parent: Node) ->void:
  new_parent.add_child(self) # OK!

「オブジェクト作成途中だからまだダメ」とかそういう制約ありそうだな、と思ったけど、大丈夫。


AnimationLibraryとSkeleton3Dのインポート条件
blendファイルインポート時の、AnimationLibraryやSkeleton3Dが作られる条件

  • ボーンアニメーションじゃなくても、オブジェクトのトランスフォームが移動するなどの、何らかのアクションがあればAnimationLibraryが作られる。

  • アクションが存在しなくても、アーマチュアが存在すればSkeleton3Dノードが作られる。

  • アーマチュアの子が存在せず、ウェイトを付けたメッシュさえも存在しなくても、アーマチュアが存在すればSkeleton3Dノードが作られる。


長くなりすぎたのでパートを分けました。パート2に続く!


この記事が気に入ったらサポートをしてみませんか?