まえがき
自分用のメモです。
わかりにくい記述があってもご容赦ください。
2025/03/08 追記
Claude Sonnet 3.7 凄いよこれ
Godot4について質問するとちゃんとしたものを出してくれる
まずは彼に聞いてみてくれ
環境
Windows11
Windows10
Steam版 4.4.1
Steam版 4.2
Steam版 4.1.2
本題
参考にしたサイトなど
UIのレイヤーを最前面にする方法
CharacterBody2Dの衝突判定
2Dオブジェクトの移動(公式Docs)
2Dアニメーションの設定(公式Docs)
コリジョンレイヤーの使い方
Nodeオブジェクトで使える関数など
ポーズ機能の実装1
ポーズ機能の実装2
特定のキー入力を検知(公式Docs)
一定間隔で処理を繰り返す
結論
awaitは待機しないでreturnする
JSとかのawaitとは別物
待ち合わせではなく、実行の予約である
検索して出てくる記事を見ると、
await get_tree().create_timer(1.0).timeout
という記述がよく出てくる。
のだが、ここがUnityとかと扱いが違った。
awaitの挙動としては、それ以降の処理を予約するイメージ。
var count = 0
func _process(delta):
count += 1
await get_tree().create_timer(1.0).timeout
print("now count = %s" % count)
というプログラムの場合、_process()が呼ばれるたびにcountが+1される。
その後、awaitで1秒待つ処理に移るが、これ以降の処理は続きは1秒後に実行するで。今回の_process()の処理はここで一旦終わる(returnする)で。
となる。
_process()の処理が一旦終わって、また先頭に戻り+1。そして処理の予約だけされて次へ。
というのが最初の1秒の間に複数繰り返されるので、1秒後に表示される値は1ではなく、64とか123とかの全然違う値になる。
ということで、1秒待って処理したい場合は以下のように書く必要がある。
(_process関数の中でこの待ち方をすべきではない)
var count = 0
var elapsed = 0
func _process(delta):
elapsed += delta
if elapsed >= 1.0:
count += 1
print("now count = %s" % count)
elapsed = 0
応用すると、こんな書き方もできる。
func Func1():
pass # 好きな処理
func Func2():
pass # 好きな処理
var count = 0
var elapsed = 0
func _process(delta):
elapsed += delta
if elapsed >= 1.0:
Func1()
# 別の処理
if elapsed >= 1.0:
Func2()
# 別の処理
if elapsed >= 1.0:
elapsed = 0
……ところで、1秒間隔の処理と2秒間隔の処理を扱おうとしたらどう書くんだろうね。
1秒用のelapsed1
と2秒用のelapsed2
を用意して回すことになる?
なんか気持ち悪いなぁ。
良い書き方を思いついたら追記する。
次の節で説明する。
一定間隔で実行したい処理を複数扱う
結論
Timerオブジェクト使おう
timer1.timeout.connect(func1)
timer1.one_shot = true # 1回実行すると終わり
timer1.start(0) # 何秒後に開始するか
周期が違うときの話。
シーンにタイマーオブジェクトを追加し、それを変数に入れておく。
同一オブジェクトでもOK。
勘違いだった。周期が上書きされるので、別々でタイマーオブジェクトを用意する必要がある。
(下記スクリプトは修正済み)
var timer1 = $Timer1
var timer2 = $Timer2
あとは、_ready関数の中でタイマーに関数を割り当てて起動するだけ。
# 1秒周期で処理する関数
func func1():
while true:
await get_tree().create_timer(1.0).timeout
print("func1")
# 0.5秒周期で処理する関数
func func2():
while true:
await get_tree().create_timer(0.5).timeout
print("func2")
func _ready():
var timer1 = $Timer1
var timer2 = $Timer2
timer1.timeout.connect(func1)
timer1.one_shot = true # 1回実行すると終わり
timer1.start(0)
timer2.timeout.connect(func2)
timer2.one_shot = true
timer2.start(0)
関数の引数に関数を渡す
結論
引数の型としてCallable
を指定するだけ
func Caller(f: Callable):
f.call()
今どきの言語ならたいていできるアレ。
Pythonで書くとこんな感じ。
def SayHello():
print("Hello")
def Caller(f):
f()
if __name__ == "__main__":
Caller(SayHello)
これをGDScriptでやろうとすると、ちょっと面倒だった。(公式ドキュメントにはちゃんと書いてある)
忘れそうだった&英語だったのでここにメモ
func SayHello():
print("hello")
func Caller(f: Callable):
f.call()
func _ready():
Caller(SayHello)
こんな感じで、引数を書くときにCallable
だと明示した上で、使うときはcall
メソッドを呼ぶ必要がある。
オブジェクト生成
結論
var obj = preload("res://<目的のプレハブ.tscn>")
func _pressed():
var _obj : Node2D = obj.instantiate()
_obj.position.x = 100
_obj.position.y = 100
基本的にはUnityと同じ。
シーンで組み立てて、ドラッグアンドドロップでファイルシステムに放り込んでプレハブ作成。
それをプログラムで呼び出す。
そのときのスクリプトは以下のようになる。
(ボタンを押したらオブジェクト生成)
var obj = preload("res://<目的のプレハブ.tscn>")
func _pressed():
var _obj : Node2D = obj.instantiate()
_obj.position.x = 100
_obj.position.y = 100
こう書けば、指定した座標にオブジェクトが生成される。
あとは基本的にUnityと同じ感じ。
オブジェクトにアタッチした関数を実行
結論
var obj = preload("res://<目的のプレハブ.tscn>")
func _pressed():
var _obj : Node2D = obj.instantiate()
_obj.setPos(100, 100)
Unityより簡潔に書けるので最高。
以下のように、直接メソッド呼び出しできる。
上記の座標指定であれば、こんな感じ。
var obj = preload("res://<目的のプレハブ.tscn>")
func _pressed():
var _obj : Node2D = obj.instantiate()
_obj.setPos(100, 100)
ただ、こう書く場合は、目的のスクリプトはそのオブジェクトのルートノードにアタッチしておく必要がある。
StaticBody2D
└-MeshInstance2D
みたいな構成の場合、一番上のStatiBody2D
にアタッチする。
シーンツリーにあるオブジェクトを絶対パスで取得
結論
正式な最上位ノードはroot
なので、絶対パスではこう書く
var Balls = get_node("/root/Root/Balls")
Unityで言うGameObject.Find
とかの処理。
Godot4ではget_node("<object path>")
と書くらしい。
絶対パスで指定しようとして詰まったのでメモ。
正しく記述してるはずなのに「そんなものは無い」と言われた。
で、Godot4におけるシーンツリーの全貌がこれ。
ルートノードはRoot
ではなくroot
。
ずっとローカルツリー(Root
以下のツリー)しか見えてなかったので混乱した。
ローカルツリーだけ見るとRoot
が最上位に見えるが、実はもっと上があるって話。
なお、このリモートやローカルの切り替えは実行中でないと行えない。
実際のプログラムとしては以下のような記述になる。
var Balls = get_node("/root/Root/Balls")
グローバル変数の設定をしてやれば、ローカルルートの名前を変えたりしても対応が楽。
var Balls = get_node("%s/Balls" % Global.LocalRootPath)
しかし、この仕様についてドキュメントに記載が無いように思うんだが、調査不足だろうか。
自身の直下の子要素を取得するなら$
マークを使うことで取得可能。
上記ツリーにおいて、Root
から子要素のCamera2D
を参照したいなら以下のように書ける。
var camera = $Camera2D
UIを最前面に置く
結論
CanvasLayer
を親にして、z_index
を大きくする
(3Dの場合は知らん。必要になったら調べる)
2Dの場合、z_index
なるもので描画順を管理してるらしい。
最前面に近いほど値が大きい。
ということで、CanvasLayerノードを作成し、それのz_index
を大きくしておく。
そのノードの中にUI部品を入れれば、これでボタンなどが最前面に表示されるってワケ。
参考URL
接触した相手を調べる(PhsyicsBody2D同士)
※PhysicsBody: CharacterBody, StaticBody, Rigidbody
自機をPlayerグループに追加しておく。
以下のスクリプトを、敵機、敵弾、…のどれかに追加する。
for i in get_slide_collision_count():
var collider = get_slide_collision(i).get_collider()
if collider.is_in_group("Player"):
queue_free() # 自分を(シーンツリーから)消す
接触した相手を調べる(Area2Dを使用)
Area2Dの中に接触している(入っている)相手を取得する
相手がPhysicsBodyの場合はget_overlapping_bodies()
でまとめて取得できる。
相手がArea2Dの場合はget_overlappingareas()
でまとめて取得できる。
使用例
for body in get_overlapping_bodies():
if body.is_in_group("Enemy"):
body.doSomething()
アニメーションのコントロール
詳細は公式Docsを参照。
単に割り当てるだけでは再生されない。
設定したアニメーションを名指しで再生/停止を制御する必要がある。
$CharaAnime.play("run")
また、左右反転や上下反転も同様に制御できる。
$CharaAnime.flip_h = true # 左右反転
$CharaAnime.flip_v = true # 上下反転
オブジェクトを回転させる
発射体とかで向きを進行方向に合わせたい時がある。
(弾丸とかをちゃんと進行方向に向ける、など)
直接rotation
パラメータをイジってたら沼ったのでメモ。
向きを合わせるにはrotate
関数を使う。
引数として回転角度[rad]を渡す。
回転角は上向きを0[deg]として時計回り(正方向)に0~180[deg]、反時計回り(負方向)に0~180[deg]の範囲。
必要に応じて矯正する。
使用例
var dir = Input.get_vector("left", "right", "up", "down")
var rad = atan(dir[1] / dir[0])
# 左向きのときは追加で回転させて頭を進路方向に向ける
if dir[0] < 0:
rad += deg_to_rad(180)
bullet.rotate(rad)
ポーズ機能の実装
基本的には下記記事2つのとおり。
ポーズ機能の実装1
ポーズ機能の実装2
その他のことも含めて少しメモ。
いわゆるポーズ状態は、UIとかBGMとか以外が止まる状態のこと(再確認)
それを実装するには、とりあえずシーンツリーをまとめて止めてやれば良い。
get_tree().paused = true
ただ、この状態ではあらゆるオブジェクトの時が止まってしまうので復帰できない。
そこで、オブジェクトごとにポーズ時の挙動を決めておく。
インスペクターのNode → Process → Modeから設定できるが、スクリプトでも制御できる。
例えば、常に動作してて欲しい場合は以下のように書く。
process_mode = Node.PROCESS_MODE_ALWAYS
とりあえずボタンとかをこの設定にしておけば、いつでもポーズから復帰できる。
なお、ポーズ状態でも止まらない部分がある。
それが_ready関数(というか実行中の関数)
ユニークネームの扱い
グローバルな扱いになるかと思ったが、そうではなかった。
(ユニークネームにすると上下関係を無視してどこからでもアクセスできると思ってた)
詳細は公式Docsを読んでくれ。
同じシーンのオブジェクトに対しては短縮形でアクセス可能らしい。
同じシーンとは、例えば「Node2D」オブジェクトの中が一つのシーンになる(と解釈した)
つまり、以下の形式の場合Player
から見るとEquips
とEnemies
は同一シーンに存在してることになるが、Enemy
は別のシーンに存在しているのでユニークにしても参照できない。
Main(Node2D)
├─Player(CharaBody2D)
│ └─Equips(Node2D)
└─Enemies(Node2D)
└─Enemy(CharaBody2D)
逆も同じで、Enemy
から見るとPlayer
は別のシーンに属しているので、ユニークにしても参照できない。
参照したい場合は、公式Docsに書いてるようにget_node("Enemies/%Enemy")
などと書く必要がある。
この機能要る?(純粋な疑問)
WhileUntilの実装
「ある条件が成立するまで待機する」処理。
UnityのUniTaskで使えるやつとか、そういう機能。
例:選択肢を出している間は止める
while Sentakushi.visible == true:
await get_tree().create_timer(0.01).timeout
doSomething()
awaitを使わないとプロジェクト全体が止まるので注意。
公式がサポートしてくれねえかな。
Random.choices(重複なし複数選択)
pythonで言うrandom.choices(list)
な処理
GDScriptにはまだ無いようなので。
var items = [...]
items.shuffle() # 直接書き換わる
for i in range(3):
print(items[i])
シャッフルしたあと、先頭からn個取り出してるだけ。
オブジェクトのイージング(Tween)
ノベルゲームとかでよくある、立ち絵が増えたらヌルっと横にずれたりするアレを実装するのに使えるのがTween。
ちょっとハマったので残しておく。
結論
create_tween().bind_node(NODE)
しよう
公式ドキュメントには以下のような記載がある。
var tween = get_tree().create_tween()
tween.tween_property($Sprite, "modulate", Color.RED, 1)
tween.tween_callback($Sprite.queue_free)
関数化し、$Sprite
を引数で渡すことを考えると、以下のような記述になる。
func move(object, new_color, duration):
var tween = get_tree().create_tween()
tween.tween_property(object, "modulate", new_color, duration)
tween.tween_callback(object.queue_free)
これが罠だった。
これを試しても反応しない。
create_tweenのドキュメントには
The Tween will start automatically on the next process frame or physics frame (depending on TweenProcessMode).
とあり、要はget_tree().create_tween()
した次のフレームから再生されると書いてある。(誤読してれば指摘ください)
しかし再生されない。
ではどうすれば良いか。
create_tween().bind_node(NODE)
すれば良かった。
以下のように書くことで、問題なく再生された。
func move(object, new_color, duration):
var tween = get_tree().create_tween().bind_node(object)
tween.tween_property(object, "modulate", new_color, duration)
tween.tween_callback(object.queue_free)