見出し画像

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

Godotエンジンを使ってて、わからなかったところや、気づきがあったところをリストしています。
常時追加して、長くなってきたら次のパートに移行します。

最終更新: 2024/05/19


global_rotationへのセットはscaleをリセットする
Node3Dで、global_rotationプロパティにVector3をセットすると、scaleプロパティがオール1になってしまう。globalではないただのrotationならば、リセットされない。

_node_3d.scale = Vector3(0.2, 0.2, 0.2)
print(_node_3d.scale.x) # この時点では0.2

_node_3d.global_rotation = Vector3(0.0, 0.0, 0.0)
print(_node_3d.scale.x) # 1.0になっている

ソースコードを見ると、Node3D:: set_global_rotationは、特に現在のスケール値を考慮せず、transformを上書きしている。
スケールを維持したければ、global_rotationのセットの前に、一度現在のスケール値を一時的な変数に記録して、セット後にスケールもセットしなおすという操作が必要。

# Y軸だけビルボードするような処理
var current_scale: Vector3 = scale
global_rotation_degrees = Vector3(0.0, rot_y_deg, 0.0)
scale = current_scale

asによるキャストは複製が発生することも
型指定の無いオブジェクトを型指定した変数で受け取るときに、よかれと思ってasを書くだけで別物になってしまうことがある。
そもそもasは例外なくぜーんぶ複製を作るのだろうか?

var pack: Variant = PackedVector3Array()

pack.append(Vector3(1.0, 1.0, 1.0))
var pack_a: PackedVector3Array = pack
var pack_b: PackedVector3Array = pack as PackedVector3Array
pack.append(Vector3(2.0, 2.0, 2.0))
pack.append(Vector3(3.0, 3.0, 3.0))

print(pack.size()) # -> 3
print(pack_a.size()) # -> 3
print(pack_b.size()) # -> 1 キャストした方は増えていない

ArrayMeshのTangentは自動で作られる
ArrayMesh.add_surface_from_arraysでメッシュにサーフェスを追加した場合、元のarraysにTangentが含まれていなくても、メッシュにはTangentが追加される。おそらくNoramlから計算&生成されている。
つまりコードでメッシュを作るときには、Tangentは不要で、Normalさえあればいい。


複数アーマチュアのblendファイル
アーマチュアが複数あるblendファイルをインポートすると、シーン直下にアーマチュアノード(Node3D)が複数ぶら下がったシーンになる。シーンに含まれるAnimationPlayerは1つだけで、それの保持するAnimationLibraryも1つ。
各Animationは、Blender上のアクションと一対一で対応している。ただし、Blenderのアクションのチャンネルには含まれていない他アーマチュアのボーンも、トラックとして追加されている。(こうして追加されたトラックは時間0にキー値0.0があるだけっぽい?)おそらく、どのAnimationも、全アクションの全チャンネルをとりあえず含めるような方針で生成される。

Animationリソースのイメージ
アーマチュアArmaAが動くアニメAnimXと、
アーマチュアArmaBが動くアニメAnimYAnimY2
の3つがあった場合

AnimX
  ArmaA.BoneA0 *----*-*--*
  ArmaA.BoneA1 *----*-*--*
  ArmaB.BoneB0 *           ←Blenderには無いトラック
  ArmaB.BoneB1 *           ←Blenderには無いトラック
AnimY
  ArmaA.BoneA0 *           ←Blenderには無いトラック
  ArmaA.BoneA1 *           ←Blenderには無いトラック
  ArmaB.BoneB0 *-*-----*
  ArmaB.BoneB1 *-*-----*
AnimY2
  ArmaA.BoneA0 *           ←Blenderには無いトラック
  ArmaA.BoneA1 *           ←Blenderには無いトラック
  ArmaB.BoneB0 *----**---*---*
  ArmaB.BoneB1 *----**---*---*

アニメーションしないアーマチュアArmaCや、
ボーンArmaA.BoneA2があっても、
これらはトラックには追加されない

コールバック関数はemit_signal関数内で呼ばれる
コールバック関数は、emit_signal関数の中で、即時呼ばれる。関数から帰ってくるまでの間に呼ばれる。ラグがあるわけではない。

# コールバック側
func _on_sub_node_finished() -> void:
    print("1")

# エミット側
emit_signal("finished")
print("2") # 1 -> 2の順に表示される

これは、コールバック先でさらにエミットしてる場合でも同じで、折り返し電話してたり、他のオブジェクトにエミットしていても、その場で解決される。
なので、なんらかの実行要求シグナルに対して成否を即答したり、参照の要求に対してすぐ参照を渡してやったりというのは、可能ではある。
ただ、そうして得られる返答は、emitした関数内のローカル変数に直接取ることはできない。どうしても、コールバック関数内で完結して使い切るか、一度メンバ関数に格納するなどが必要。

# Swordmanクラスの実装
# Blacksmithクラスに依存したくない

# 第三者が事前にコネクトしておく
swordman.request_weapon.connect(blocksmith.on_someone_reqested_weapon.bind())

# シグナルでリクエストして取得
emit_signal("request_weapon")
var weapon: Weapon = _received_weapon # emit後、メンバ変数に格納されている
_received_weapon = null

# ----------------------------
# シグナルの代わりにインターフェイスクラスを使う例
# 事前に第三者がBlocksmith参照を渡す
swordman.set_blacksmith(blacksmith)
# IBlocksmithインターフェース越しに取得
var weapon: Weapon = i_blacksmith.get_weapon()
# ----------------------------

そもそもエミット側が反応を期待してる時点で、シグナルに向いてない処理のように思う。


メッシュ未適用マテリアルはサーフェイスにならない
Blender上でマテリアルのスロットが複数あって、それにマテリアルが割り当てられていても、メッシュに割り当てられていないマテリアルは、サーフェイスにならない。よくもわるくも取り除かれる。
これは、サーフェスが存在することを期待してマテリアルにアクセスしようとするときに、エラーの原因となる。


アーマチュア下のオブジェはオフセット無しでインポートされる

Blender上で、アーマチュア下のスキンメッシュオブジェクトが、トランスフォームにオフセットを持っていた場合。

アーマチュア
 メッシュオブジェ(XYZ = (0.0, 0.0, 0.0))
 メッシュオブジェ(XYZ = (3.0, 0.0, 0.0))
 メッシュオブジェ(XYZ = (2.0, 2.0, 2.0))

インポートするとこうなる。

Skeleton3D
 MeshInstance3D(XYZ = (0.0, 0.0, 0.0))
 MeshInstance3D(XYZ = (0.0, 0.0, 0.0))
  ※メッシュの頂点に、X=-3.0のオフセットが入ってる
 MeshInstance3D(XYZ = (0.0, 0.0, 0.0))
  ※メッシュの頂点に、各XZY=-2.0のオフセットが入ってる

オブジェクト自体のオフセットが消えて、メッシュで差が吸収される。


emit_signalは古い
Object.emit_signal関数はもう古い。Godot 4以降はSignal.emit関数が使える。

# リテラルでシグナル名を書きたくない
emit_signal("my_signal")
# 長い
emit_signal(my_signal.get_name())
# 簡素
my_signal.emit()

3Dにかぶせて別カメラで3Dを表示
別カメラ別ライティングで描画される別レイヤーのオブジェクトを、通常の画面の上に描画したい。
例えばFalloutや時のオカリナのインベントリのように、3Dオブジェクトを画面を覆うメニューとして使うとか、JRPG風の会話シーンで立ち絵的にキャラクターが表示されるとか。
この場合、SubviewportのViewportTextureに一度描いて、画面に貼り付ける必要がある。こうするしかないっぽい。1つのレンダリングターゲットで実現できると、シンプルだしパフォーマンス的にも嬉しい。どうにかできないかなあ。
(もう忘れたけどUnityではCameraのtargetTextureをnullにすることで画面に直接描画できたっぽい。)

【方法1】

・レイヤー1ノード
・レイヤー1カメラ
・SubViewport (TransparentBG = true、Size = 画面サイズ)
 ・レイヤー2ノード
 ・レイヤー2カメラ
・TextureRect(ViewportTexture、Expand Mode = Ignore Size、レイアウトを画面全体)

# 自分のスクリプトで、画面サイズ変更に合わせてsizeを都度更新する
# window.size_changed.connect(_on_window_size_changed) # コールバックを繋いでおく
# my_sub_viewport.size = window.size # シグナルを受け取ったら修正

【方法2】

・レイヤー1ノード
・レイヤー1カメラ
・SubViewportContainer(Stretch = true、AnchorPreset = Rect全体)
 ・SubViewport (TransparentBG = true)
  ・レイヤー2ノード
  ・レイヤー2カメラ

# SubViewportContainerの機能で、
# 画面サイズに合わせて自動で動的にテクスチャサイズが変わる

size_2d_overrideはテクスチャのサイズを変えない
SubViewportのsize_2d_overrideをいくら書き換えてもテクスチャの解像度が変わったように思えない。size_2d_overrideは、ViewportTextureの実サイズに関係しないようだ。

ソースコード
RenderingServer上のViewport実体のサイズを変えてると思しき箇所2つ。
どちらも、size_2d_overrideの値を取っていない。

1. Viewport::_set_size(...)
RS::get_singleton()->viewport_set_size(viewport, size.width, size.height);

2. SubViewport::SubViewport()
RS::get_singleton()->viewport_set_size(
    get_viewport_rid(), get_size().width, get_size().height
    );

ではsize_2d_overrideはなんだろう?
https://github.com/godotengine/godot-proposals/issues/6221
上記フォーラムでは「size_of_display_areaという名前のほうが妥当では」と提案されている。2D系のノードが、テクスチャの一部をトリミングして表示するための機能、のような感じだろうか?


get_window().sizeは描画領域のサイズ
Node.get_window().sizeで取得できるサイズは、実際にゲームで使われる描画領域のサイズ。ウィンドウのフチは含まれていないので心配いらない。
Node.get_window()は、そのノードが含まれているWindowを返す関数らしい。(たぶん自作Windowを使ってない限りは、)get_tree().get_root()と同じとのこと。


シグナルでウィンドウのサイズ変更を検知できる
ウィンドウのサイズを監視して、サイズ変更があったらSubViewportのテクスチャサイズも変えたい。しかし毎回Windowを取得しては、多分、パフォーマンスに影響が出る。そんなときは、Viewport.size_changedシグナルを受け取るようにすればいい。

var scene_tree: SceneTree = Engine.get_main_loop() as SceneTree
var window: Window = scene_tree.root as Window
window.size_changed.connect(_on_window_size_changed)

Node.NOTIFICATION_WM_SIZE_CHANGED (1008)を使ってもよさそう。


SubViewportの内容を見るにはTexture2D.get_image
デバッグ目的でSubViewportの中身を見たい。そんな時は、Texture2D. get_image()でImageが生成されるので、これをsave_png関数で保存する。

var texture: ViewportTexture = _sub_viewport.get_texture()
var image: Image = texture.get_image() # get_imageはパフォーマンス影響大
image.save_png("user://sub_viewport.png")

保存先
C:\Users\ユーザ名\AppData\Roaming\Godot\app_userdata\プロジェクト名\sub_viewport.png

組み込みクラスは_initも_readyも_processもvirtual
Camera3Dクラスの継承クラスを作ったからと言って、_init内でsuper._init()を呼ぶ必要は無い。そもそも呼べない。なぜならvirtualなので関数自体が無くなる。_readyも_prcessも同様。
おそらく継承元では何もやってないんでしょう。だから気にする必要は無い。


Arrayは==で比較可能
Array同士は==演算子で比較できる。ソースコード見る限り、
1. ポインタが一致するならtrue
2. サイズが一致しないならfalse
3. 中身が1つでも一致しないならfalse
 intやVector3等の場合は値で比較
4. 1~3に該当しなければtrue
という処理をしている。普通に使う分には問題なさそう。


set_layer_maskは追加設定用
VisualInstance3D. set_layer_mask()関数は、layer_maskのビットの1つを書き換えるだけ。さらに、layer_maskは初期値が1になっている。なので、レイヤー2だけで描画されるようにしたければ、
set_layer_mask(1, false)
set_layer_mask(2, true)
という2段階の処理が必要。他のレイヤーのビットがどうなってるかわからなければ、一度全部falseにするしかない。
ビット演算になじみがなくても、layersプロパティに値を直で設定するのが一番確実なように思う。

# ビット演算が苦手なのでstatic関数にしておく
static func derive_layer_mask_single(layer_number: int) -> int:
  return 1 << (layer_number - 1)

layers = MyUtilClass.derive_layer_mask_single(layer_number)

posmodの第2引数は0だとエラー
posmodの第2引数は0だとエラーが発生してプログラムが止まってしまう。配列などのインデックスをループさせるときに、要素数を入れて使うことがあるが、if文でチェックしたほうがいい。
if slots.size() > 0:
 idx = posmod(idx + sub_idx, slots.size())


tmpファイルができることがある
いつのまにかプロジェクト内に拡張子tmpのファイルが作成されることがある。GDファイルの名前の頭3文字+ランダムっぽい16進数数字+.tmp、という感じで名前が付いてる気がする。

【事例】
・main_play.gd → mai75E.tmp
・seq_tester.gd → seq3394.tmp
・seq_tester.gd → seqFFB2.tmp
この2つのgdファイルの共通項としては、それぞれ同名のシーン
main_play.tscnとseq_tester.tscnにアタッチしているということ。
それから割とレイヤー最上位の「使う側」のクラスであるということ。

【seq3394.tmpの中身】
[gd_scene load_steps=2 format=3 uid="uid://ck2tkea48pafo"]

[ext_resource type="Script" path="res://Sequence/Tester/seq_tester.gd" id="1_gy513"]

[node name="SeqTester" type="Node"]
script = ExtResource("1_gy513")

https://github.com/godotengine/godot/issues/956
https://github.com/godotengine/godot/issues/82270

↑フォーラムのやり取り。Windowsだけで起こるバグっぽい? 10年前からあるが、それが4.1で復活した?
なんにせよ、消してよさそう。


コールバック関数内でfreeするとエラー
signalを用いたコールバック関数内で、自分自身をfreeしようとすると、次のようなエラーが出る。
Object is locked and can't be freed.
色々試したけれども、コールバックされる側と、signalをemitする側の、ノードの親子関係は、エラーと関係ない。他人でも出る。
また、シグナルを接続した関係であっても、emit側がsignalを使わずに、process内でコールバックされる側をfreeするのは問題ない。
他のノードは関係無しに、「コールバックされてる最中に自分をfreeしてはならない」ということっぽい。
これはfree()の代わりにqueue_free()関数を使えば回避できる。


queue_free()はいつfreeするのか
queue_free()をしたオブジェクトはいつ解放されるのか? それがわからないので、queue_free()食わず嫌いでいる。
ソースコードを荒く読んだ感じ、Godot製ゲームのメインループは、だいたい大まかにはこのような順番で処理をしている。
・入力取得
・物理
・組み込みクラスの更新(NOTIFICATION_INTERNAL_PROCESS)
・_process()呼び出し(NOTIFICATION_PROCESS)
・queue_free()されたオブジェの削除
・描画
・音再生
自作クラスの_process()内でqueue_free()すると、すべての_processが終わった直後に、オブジェクトが解放される。

queue_free()を使うときの懸念としては、次のようなものがある。
・描画されてしまわないか?
 →描画前にfreeされるので描画されないはず。
・他のクラスが使ってしまわないか?
 →組み込みクラスの処理はすべて終わっているので、
  自作クラスのやらかしだけ心配すればいい。
 →_process内で、ローカル変数に一時的に参照を得るのは、
  問題ない。
 →メンバ変数に参照を保持する場合、注意が必要。
  一蓮托生で自分も一緒に消える、
  つまり参照が自分の親ノードであったり、
  同時にqueue_freeされることが約束されているオブジェクトなら、
  運用として許容できそう。

前述の通り、コールバック関数内で自分をfree()するとエラーとなるのだが、ドキュメントに従ってqueue_free()に置き換えても、特に心配することは無さそう。ただ、queue_free()したあとに、消える予定のオブジェの_processが回ってくる可能性(エンジンが気を利かせてブロックしてるかもしれないけど)があることだけは留意したほうがよさそう。


Animationのvalueとは
Animationクラスの関数。"Value"という言葉が違う意味で2つ出てくる、という話。
キーフレームの時間を知りたい場合は、track_get_key_time()関数で取得できる。キーフレームの値を取り出したい場合は、track_get_key_value()関数で取得できる。戻り値がVariant型なのでVector3やQuaternionで受け取る。この場合の"value"は、連想配列的な意味でのvalueだろう。key引きのvalueだ。

var time: float = animation.track_get_key_time(track_idx, key_idx)
var key_pos: Vector3 = animation.track_get_key_value(track_idx, key_idx)
var key_rot: Quaternion = animation.track_get_key_value(track_idx, key_idx)
var key_scale: Vector3 = animation.track_get_key_value(track_idx, key_idx)

一方、キーフレームではなく、補間値を知りたい場合。value_track_interpolate()関数がある。これを使えばいいのか、というと違う。この関数の"value"は、先ほどのkey_time vs key_valueの"value"とは意味が違う。
Animationに収めるキー値の種類には、Position・Rotation・Scale以外にもいろいろあって、その1つがValueだ。これはPosition・Rotation・Scale以外の「汎用的に使う補間可能な値」のようだ。value_track_interpolate()関数の返すのはこのValue値で、普通のアニメーションでは使ってないトラックのようだ。

# 普通はこの3つの関数を使えばいい
var interp_pos: Vector3 = animation.position_track_interpolate(track_idx, time)
var interp_rot: Quaternion = animation.rotation_track_interpolate(track_idx, time)
var interp_scale: Vector3 = animation.scale_track_interpolate(track_idx, time)

# Valueはボーンの姿勢以外の用途
# 色の変化とかそういうのを格納できるのかな?
var track_type: Animation.TrackType = animation.track_get_type(track_idx)
if track_type == Animation.TYPE_VALUE:
  var interp_val: Variant = animation.value_track_interpolate(track_idx, time)

ReferenceRectについて
ReferenceRectというNodeクラスがある。これは、文字通り枠線を表示するだけのControlで、制作サポート目的のものだ。
これに、set_anchors_presetでPRESET_FULL_RECTをセットして使うと、親Controlが画面上のどこに矩形を展開してるか、可視化することができる。ただし、editor_onlyプロパティがデフォルトでtrueになっていて、これだとエディタ上の実行時に見えなくなってしまう(editor_onlyは、いわばscene_edit_onlyということ)ので、falseにする必要がある。
NinePatch派生クラスとしてデバッグ用アタリ矩形を自作してたけど、ReferenceRect派生で作ってもよさそう。


消せないフォルダ
Godot 4.3 dev6で、フォルダの名前を変えると、元のフォルダが「ファイルシステム」に残ってしまい、消そうとしてもエラーで消せなくなった。.godotフォルダを一度消してから起動すると、改名前のフォルダが無事に消えた。
Godot 4.3 dev6はこの他にも、フォルダ作成時に名前の重複がある旨のエラーが出たりして、フォルダ関係が不安定っぽい。正式版までには直りそう。


SHADOWED_VARIABLEはstatic関数でも出る
引き数名にメンバ変数名と同じものを使ってると、ワーニングが出る。
実はこれ、static関数でも出る。static関数内ではこれがメンバ変数でないことは明確だけど、出る。

class_name MyObj
var val_a: int = 0

# 非staticではワーニングが出る
func set_val_a(val_a: int) -> void:
    val_a = val_a

# staticでも出る
static func create(val_a: int) -> MyObj:
    var obj = MyObj.new()
    obj.val_a = val_a
    return obj

配列のインデックスは負でもいい
配列の添え字は、0~size()-1だけでなく、-size()~-1でもアクセスできる。
なので最後の要素へのアクセスはa[s.size()-1]と書く代わりに、a[-1]と書いてもいい。








この記事は随時更新します


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