2
1

Godot4勉強ノート(自分用まとめ)

Last updated at Posted at 2023-10-24

まえがき

自分用のメモです。
わかりにくい記述があってもご容赦ください。

環境

Windows10
Steam版 4.2
Steam版 4.1.2

本題

参考にしたサイトなど

UIのレイヤーを最前面にする方法
CharacterBody2Dの衝突判定
2Dオブジェクトの移動(公式Docs)
2Dアニメーションの設定(公式Docs)
コリジョンレイヤーの使い方
Nodeオブジェクトで使える関数など
ポーズ機能の実装1
ポーズ機能の実装2
特定のキー入力を検知(公式Docs)

一定間隔で処理を繰り返す

検索して出てくる記事を見ると、

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を用意して回すことになる?
なんか気持ち悪いなぁ。
良い書き方を思いついたら追記する。
次の節で説明する。

一定間隔で実行したい処理を複数扱う

周期が違うときの話。
シーンにタイマーオブジェクトを追加し、それを変数に入れておく。
同一オブジェクトでも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)

関数の引数に関数を渡す

今どきの言語ならたいていできるアレ。
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メソッドを呼ぶ必要がある。

オブジェクト生成

基本的にはUnityと同じ。
シーンで組み立てて、ドラッグアンドドロップでファイルシステムに放り込んでプレハブ作成。
それをプログラムで呼び出す。

そのときのスクリプトは以下のようになる。
(ボタンを押したらオブジェクト生成)

var obj = preload("res://<目的のプレハブ.tscn>")
func _pressed():
    var _obj : Node2D = obj.instantiate()
    _obj.position.x = 100
    _obj.position.y = 100

こう書けば、指定した座標にオブジェクトが生成される。
あとは基本的にUnityと同じ感じ。

オブジェクトにアタッチした関数を実行

Unityより簡潔に書けるので最高。

以下のように、直接メソッド呼び出しできる。
上記の座標指定であれば、こんな感じ。

var obj = preload("res://<目的のプレハブ.tscn>")
func _pressed():
    var _obj : Node2D = obj.instantiate()
    _obj.setPos(100, 100)

ただ、こう書く場合は、目的のスクリプトはそのオブジェクトのルートノードにアタッチしておく必要がある。

StaticBody2D
└-MeshInstance2D

みたいな構成の場合、一番上のStatiBody2Dにアタッチする。

シーンツリーにあるオブジェクトを取得

Unityで言うGameObject.Findとかの処理。
Godot4ではget_node("<object path>")と書くらしい。

絶対パスで指定しようとして詰まったのでメモ。
正しく記述してるはずなのに「そんなものは無い」と言われた。

で、Godot4におけるシーンツリーの全貌がこれ。

image.png

ルートノードは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を最前面に置く

(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を使用)

公式Docs

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から見るとEquipsEnemiesは同一シーンに存在してることになるが、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個取り出してるだけ。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1