Open69

Godot Engine / GDScript備忘録

ピン留めされたアイテム
SLMNLLSLMNLL

これは何?

私がGodot Engineを使っていく中で、

  • 気になった挙動
  • ハマったポイント
  • たまに使うが、よく忘れてしまうTips

などのメモを取っていくスクラップです[1]
3.x系と4.x系のコードが混在しているので、ご注意ください。

目次

コメントがかなり増えてきたので、目次を用意しました。

シグナル、通知関連

型関連

GDScript Tips集

GDScriptの特徴・仕様

エラー関連

エディタ関連

その他

脚注
  1. コメントが長くなったものについては、記事化したりしなかったりします。 ↩︎

SLMNLLSLMNLL

引数付シグナルは、接続のタイミングを考えるべし

  • connect()でシグナルに引数を設定した場合、シグナルを接続した時点の値が渡される
  • シグナルの接続後に参照元の変数に別の値を代入しても、接続時の値が参照される

ですので、例えば以下のコードでは、マウスオーバー時には bar ではなく _id の初期値である foo が出力されます。これは、シグナルの接続後に _id の値を変更しているためです。

extends Control
var _id = "foo"

func _ready():
  self.connect("mouse_entered", self, "_on_mouse_entered", [_id])
  _id = "bar"

func _on_mouse_entered(id):
  print(id) # 実行結果 → foo
SLMNLLSLMNLL

connect()関数を使った際のことしか書いてなかったので、emit_signal()との比較も。

connect()に引数を設定する場合

connect()を呼び出した時点の引数が参照される。上述の通り、シグナルを接続した時点の値が格納されるため、シグナルが発火した場合には接続時の引数の中身が参照されます。

emit_signal()に引数を設定する場合

emit_signal()を呼び出した時点の引数が参照される。シグナルを発火した時点の引数が参照されるので、参照元が変数の場合は最新の値が渡されます。


Godotのシグナルにはconnect()emit_signal()のどちらを使っても引数を設定することが可能ですが、以上のような違いがあるため、状況に応じて使い分けると良いかと思います。

SLMNLLSLMNLL

一部、動作しないシグナルがある

一部のシグナルが発火しないことには注意が必要です。

例えば、ScrollContainerクラスのscroll_startedscroll_endedシグナルはGodot3.4.4時点では動作しません。代替手段として、get_v_scrollbar()などで参照したスクロールバーのscrollingシグナルと、Timerを駆使することでほぼ同じ機能を実装することができます[1]

なお、2020年の時点で正常に動作していないシグナルのリストはGithubにまとめられています

脚注
  1. 出典:https://github.com/godotengine/godot/issues/22936 ↩︎

SLMNLLSLMNLL

インスタンスを削除する際に、シグナルをdisconnectする必要はない

削除されたインスタンスにconnectしてたシグナルがどうなるのかがふと気になったので、軽く実験。

extends Node

func _ready()
    # インスタンスを初期化し、シグナルを繋ぐ
    var sig_test = SignalTest.new()
    $Button.button_down.connect(sig_test.test_func)

    # ログに、connectされたシグナルを再び出力
    # 出力結果:
    # [{"signal":Button::[signal]button_down, "callable":Control::test_func, "flags":0}]
    print($Button.button_down.get_connections()) 

    # インスタンスの削除
    sig_test.free()

    # ログに、connectされたシグナルを再び出力
    # 出力結果:[]
    print($Button.button_down.get_connections())


# シグナルを繋ぐためだけに用意した内部クラス
class SignalTest extends Control:
    func test_func():
        print("foo")

接続先のインスタンスが削除された際に、自動的にdisconnectされるようです。公式ドキュメントにも記載があります。

SLMNLLSLMNLL

複数のインスタンスのシグナルを、ひとつのメソッドで処理する

シグナル引数を使えば、複数のインスタンスのシグナルを、ひとつのメソッドで処理することができます。

コードで説明するのが早いと思うので、以下に示します。

✅ Godot 3.x
func _ready():
    # Buttonの各インスタンスのbutton_downシグナルを、ひとつのメソッドに接続
    # その際、Buttonのname変数を引数に設定
    $Button1.connect("button_down", self, "_on_button_down", [$Button1.name])
    $Button2.connect("button_down", self, "_on_button_down", [$Button2.name])
    $Button3.connect("button_down", self, "_on_button_down", [$Button3.name])

# シグナルが発火した際に呼び出されるメソッドには、ボタン名を引数として設定する
func _on_button_down(button_name:String):
    # ボタンの名前に応じて処理を振り分ける
    match button_name:
        "Button1":
            # Button1の処理
        "Button2":
            # Button2の処理
        "Button3":
            # Button3の処理

シグナルごと個別に関数を設定しなくていいので、こうすることで処理がスマートになる場合も多々あるかと思います。

SLMNLLSLMNLL

デフォルト引数に計算式を指定してもOK

例として、以下のコードではインスタンスのサイズに1.25を乗算した数を、計算式ごとデフォルト引数として設定しています。

func set_new_size(transformed_size:Vector2 = rect_size * 1.25):
  self.rect_size = transformed_size
SLMNLLSLMNLL

条件文でクラスタイプを比較する場合には「is」

Godotには、インスタンス型チェッカーとしてis演算子が用意されています[1]。条件文でクラスの比較をしたい場合にはとても便利ですね。

カスタムクラスの場合

スクリプトファイルの比較で同定する

カスタムクラスのインスタンス同士で、型が一致しているかを比較

is_class()などをオーバーライトする

isをカスタムクラスで使用した場合の挙動については、不正確な部分を修正し、より良い解決方法を加筆した上で記事にしました[2]
https://zenn.dev/slm/articles/c6924394de0bda

脚注
  1. GDScriptの基本に書いてあるのにすぐ忘れてしまいます。 ↩︎

  2. Stringでの比較となるので、typoの可能性がある点は注意が必要です。 ↩︎

SLMNLLSLMNLL

add_child()しないと、_ready()等は発生しない

Godot DocsのNodeのページにも記載の通り、_ready()はノードがツリーに加わると実行されます。

Object、Referene、Resourceを拡張して使用する場合には、add_child()ができません。つまり、必然的に_ready()_enter_tree()、そして_input(event)に書かれたコードは実行されないことになります。

SLMNLLSLMNLL

Godotで内部クラスの継承をする

Godotでは内部クラスの継承が可能です。…が、ほとんど誰も使ってないのか、英語ですらあまり情報が出てきません。念の為、備忘録として情報を残しておきます。


例えば、以下のような構造の内部クラスがあったとします。

extends Node

class_name ParentClass

func _ready():
    #do something

# ParentClassの内部クラス
class InnerClass extends Reference:
    func _init():
        #do something

例えばParentClassを継承した上で、その内部クラスであるInnerClassを継承する場合には、以下のように書けば良いです[1]

extends ParentClass # クラスの継承

class_name ChildClass

func _ready():
    #do something

# ParentClassの「InnerClass」を継承した内部クラスを
# 親クラス名 + ドット + 親クラスの内部クラス名で指定し、継承
class ChildInnerClass extends ParentClass.InnerClass: 
    func _init():
        #do something

脚注
  1. 外側のクラスは必ずしもParentClassを継承する必要はなく、無関係なクラスの内部クラスを直接継承することも可能です。 ↩︎

SLMNLLSLMNLL

.tresファイルの中身を変数として読み出す

公式のドキュメントやコミュニティにもあまり情報がなかったのですが、Godotのリソースファイルである.tresファイルは、スクリプト内で簡単に変数として読み出しができます。


以下に.tresファイルの中身を例示します。行ごとに変数名・値が書かれていて、.iniファイルなどに似ています。

example.tres
[resource]
bg_color = Color( 0.2, 0.2, 0.2, 1 )
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
anti_aliasing = false
position = Vector2(8, 8)

例えば上記の.tresファイルから、positionbg_colorを読み出ししたい場合、GDScript内ではこう書けばOKです。拍子抜けするくらい簡単。

var tres_variables = load("res://***/exapmle.tres")
var position = tres_variables.position
var bg_color = tres_variables.bg_color

print(position) # -> Vector2(8, 8)
print(bg_color) # -> Color(0.2, 0.2, 0.2, 1)
SLMNLLSLMNLL

テーマファイルから特定の変数を読み出す場合には、以下のように書くことができます。

var theme_variables = load("res://***/exapmle_theme.tres")
var bezier_len_neg = theme_variables.get_theme_item(Theme.DATA_TYPE_CONSTANT, "bezier_len_neg", "GraphEdit")

print(bezier_len_neg) # -> GraphEditノードのTheme ConstantsであるBezier Len Negの値が出力されます。
SLMNLLSLMNLL

ノードのプロパティに対してsetget

Godot 3.4時点では、rect_sizeや、positionなど、継承元のノードのプロパティに対しsetgetを定義することはできません。が、_set_getなどのメソッドをオーバーライドすることは可能です。これを利用して、プロパティに対してsetter、getter(のようなもの)を実装することが可能です[1]

サンプル
# self.rect_sizeに値を代入すると、その値がログに出力される
func _set(property, value):
    if property == "rect_size":
        print(rect_size) 

# positionの値が読み出されると、その値がログに出力される
func _get(property):
    if property == "position":
        print(position)

_setが反応するためには、rect_sizeではなくself.rect_sizeで値を代入しなければならない点には注意が必要。また、参照元のQ&Aには「_setを使って値を書き換えようとすると無限ループに陥るため、少し複雑な処理を要する」と、サンプルコードとともに書かれています(こちらは未検証)。

脚注
  1. 参照元:https://godotengine.org/qa/74149/add-setget-onto-existing-variable-from-inherited-class ↩︎

SLMNLLSLMNLL

AtlasTextureをもっと知る

repeat, mirrored repeatは無視される

Godotのドキュメントにも記載があります[1]が、AtlasTextureflagsのうちrepeatmirrored repeatオプションは無視されます。つまりAtlasTextureを使って画像を反復させることは、基本的にできません。
無視されるflagオプション
これらのフラグはチェックを入れても無視されます


スクリプトで値を変更すると同期される

スクリプトからregionflagsの値などを変更すると、同じAtlasTextureを参照して表示されている画像全てに変更が反映されます[2]

脚注
  1. https://docs.godotengine.org/ja/stable/classes/class_atlastexture.html ↩︎

  2. AtlasTexture固有の挙動ではなくResource派生クラスに共通するものだと想像しているのですが(未確認)、備忘録のために言及。 ↩︎

SLMNLLSLMNLL

インスタンスが削除されたかを調べる

free()でインスタンスを解放する処理がある場合、インスタンスの代入先である変数がnullかどうかを調べたいことがあると思います。

しかし、実はインスタンスが解放された後も参照自体は残っているため、すぐに変数の値がnullになるわけではありません。ですので以下のようなコードでは、インスタンスが解放・削除されたかどうかは調べられません。

🚫
deleted_object.free() # インスタンスを解放
if deleted_object == null:
    print("nullです")
    # インスタンスを解放しているのに、「nullです」は出力されません

こういった場合には、インスタンスが有効かどうかを調べるis_instance_valid()が使えます[1]

deleted_object.free() # インスタンスを解放
if !is_instance_valid(deleted_object):
    print("nullです")
    # 「nullです」と出力されます

弱参照を扱えるweakref()と、その参照先を返すget_ref()を組み合わせて調べることもできます[2]

deleted_object.free() # インスタンスを解放
if weakref(deleted_object).get_ref() == null:
    print("nullです")
    # 「nullです」と出力されます
脚注
  1. 参照元:Godot Engine - Q&A > How are you supposed to handle null objects? ↩︎

  2. 参照元:Godot Engine - Q&A > Freeing objects/Nodes ↩︎

SLMNLLSLMNLL

Godotのプライベート変数、プライベート関数について

Godot DocsのGDScriptスタイルガイド > 命名規則 > 関数と変数に記載されていますが、Godotではプライベートな関数や変数は、関数名・変数名の前にアンダースコアを追加して書くこととされています。

var _private_variable # プライベート変数

func _private_function(): # プライベート関数

ただこのルールはあくまで慣習的なもので、例えアンダースコアが付いていても関数・変数のどちらも外部からのアクセス自体は可能です[1][2]。これは言語的に近い Python の仕様に倣ったものと思われます。

脚注
  1. Godotエディタのコード補完でも、外部のオブジェクトの "プライベート関数”"プライベート変数" が候補として出てきます。 ↩︎

  2. もちろん、変数などはsetgetを使って外部からの書き換えができないようにもできます。ただ、全ての変数にsetgetを設定するのは現実的ではありませんね。 ↩︎

SLMNLLSLMNLL

リソースファイルの複製は参照渡し

複数のノードから別々に読み込んだとしても、読み込んでいるリソースファイルが同じであれば、値が共有されるので注意が必要です[1]

例えば以下のように2つのノードで同一のリソースファイルを読み込んで、Node_2.gdの方でだけ値を変更したとしても、その値は共有されます。

# Node_1.gd
var test_resource = load("res://resource.tres")

# Node_2.gd
var test_resource = load("res://resource.tres")
test_resource.name = "Node2"
# この後、Node_1.gdの方で print(test_resource.name) を実行した場合、「Node2」と出力されます。

参照ではなく、値を渡したい場合はduplicate()

同じリソースファイルを元にしつつも、それぞれで値が共有されないユニークなリソースを扱いたい場合はduplicate()関数を使うか、resource_local_to_sceneプロパティをtrueにして、リソースの値が共有されないようにすればOKです。

先程のソースの例であれば、以下のように書き換えればOKです。

duplicate()関数を使った場合
# Node_1.gd
var test_resource = load("res://resource.tres").duplicate()

# Node_2.gd
var test_resource = load("res://resource.tres").duplicate()
test_resource.name = "Node2"
# この後、Node_1.gdの方で print(test_resource.name) を実行した場合、
# リソースファイル側でデフォルトの値が設定されていない限り、Nullが出力されます。

また、duplicate()関数の第1引数をtrueにすることで、リソースファイルのサブリソースも複製されます。初期値(false)のままにすると、サブリソースは親リソース間で共有されるためメモリなどの効率が良いです。

脚注
  1. それぞれのノードの変数には、元々のリソースファイルへの参照が代入されています。参照を渡すことで、メモリの節約になりますね。 ↩︎

SLMNLLSLMNLL

配列の複製も参照渡し

上述のリソースファイルのくだりとも通底しますが、GDScriptでは基本的な型以外は=を使っても値がコピーされず、元の変数の参照が渡されます[1]

# 配列を準備
var original_array = [1, 2, 3]

# 配列をコピー
var copied_array = original_array

# コピーした配列に値を代入
copied_array.append(4)

# 元の配列をログに出力
print(original_array)

# copied_arrayはoriginal_arrayを参照しているだけなので、
# append関数でcopied_arrayに追加された「4」は、original_arrayにも追加されます。
# ですので、この場合は[1, 2, 3, 4]と出力されます

値を渡したい場合はduplicate()

独立した配列を複製したい場合には、こちらでもduplicate()関数を使います。

# 配列を準備
var original_array = [1, 2, 3]

# 配列をコピー / この行だけ、先程のコードから変更しました
var copied_array = original_array.duplicate()

# コピーした配列に値を代入
copied_array.append(4)

# 元の配列をログに出力
print(original_array)

# copied_arrayは複製された変数なので、original_arrayには値が追加されません。
# この場合は[1, 2, 3]と出力されます。
脚注
  1. GDScriptが特殊というわけではなく、配列などが参照渡しの言語は一般的かと思います。 ↩︎

SLMNLLSLMNLL

PoolVector2Arrayなどは値渡し

実はPoolVector2ArrayPoolVector3Arrayなど、Pool***Array系の型は参照渡しではなく、値渡しだったりするので、duplicate()関数はありません[1][2][3]

脚注
  1. https://docs.godotengine.org/ja/stable/classes/class_poolvector2array.html ↩︎

  2. 配列に格納されている値が少ない場合、Pool***Array系の型は、Arrayに比べてパフォーマンスが落ちる という言説をどこかで見た気がするのですが、見当たらず。 ↩︎

  3. Pool****Array系の型は、Godot 4.0以降Packed****Arrayとなったので、適宜読み替えてください。 ↩︎

SLMNLLSLMNLL

インスタンス化したシーン間でもDictionaryが共有される

シーンエディタで、別のシーンを複数個インスタンス化した際、同じPackedSceneのインスタンス間ではDictionaryの値が共有されるので注意しましょう。

スクリプト経由でduplicate()してからインスタンス化するか、Dictionary型を格納するためだけのクラスを用意して、インスタンス化の際に初期化して読み込むなど、工夫する必要があります。

ちょっと面倒 & 気づかないとしばらくハマります。

SLMNLLSLMNLL

値の変化を検知する

ある値、特にノードにもともと組み込まれている変数が変化したかどうかを検知するのに、変化前の値を保持して、変化後の値と比較して…という処理を書くと、検知のためだけに変数を新たに用意しなければいけなかったり、コードが長くなってしまいます。

そんな時は、_set()をオーバーライドします。

rect_positionが変化したことを検知する
func _set(property, value):
    if property == "rect_position":
        # rect_positionに新しく値が代入された場合に、ログが出力されます
        print("rect_positionが変化しました")

print()の部分をemit_signal()に変えれば、自作のシグナルの発火などにも使えます。

SLMNLLSLMNLL

Vector2などの値の、少し変わった呼び出し方

Vector2, Rect2など、複数の値をまとめて扱える型は通常、変数名.size.yのように書くことで一部の値を取り出すことができます。

ですが、実は配列などと同じく変数名["size"]["y"]のように書くことも可能です[1]
以下に例示するように、呼び出したい値を動的に選択可能です[2]

# 引数に応じて、Vector2のx,yのどちらかの値を返す関数
func get_axis(key:String)
    return Vector2(128, 256)[key]


# 上述の関数を呼び出して、ログに出力する処理
func _ready():
    print(get_axis("x")) # ログには"128"と出力されます
    print(get_axis("y")) # ログには"256"と出力されます

脚注
  1. 逆にDictionary型もdictionary.keyのように書いて値を操作することができます。 ↩︎

  2. 例示したコード程度であればif文を使っても良いですが、もう少し込み入った処理をする場合には、この方法が役に立つかもしれません。 ↩︎

SLMNLLSLMNLL

return文で型キャスト

以下のように戻り値の型を宣言した場合、returnで返す型と、宣言した型が一致しない」 とGodotに注意されます。

func get_texture() -> Texture:
    return load("res://sprite_01.png") 
    # ↑ Resource型だと解釈され、エラーが出ます

そういった場合は、asを使って型をキャストしましょう。

func get_texture() -> Texture:
    return load("res://sprite_01.png") as Texture
    # ↑ Texture型として解釈されるので、エラーは出ません
SLMNLLSLMNLL

_notification(what)をもっと知る

Godotには、オブジェクトの状態変化を検知できる組込関数_notification(what)があります。

_notification(what)で扱える通知であれば、オブジェクトの状態変化時のコールバック処理を設定することができます。こちらはシグナルと違ってconnect関数などを使う必要がないので、場合によってはコードが簡略化できるかもしれません。

公式のドキュメント[1]にも記載がありますが、分かりやすいコード例を載せます。この例ではNodeクラスに実装されている通知を使ってみてます。

func _notification(what):
    match what:
        Node.NOTIFICATION_PARENTED:
            # ノードの親が変更された際に、子側で呼び出される
        Node.NOTIFICATION_MOVED_IN_PARENT:
            # ツリー内のノードの順番が変化した際に、変化したノードで呼び出される

上述の例のように、オブジェクトによってはシグナル一覧にない、言わば隠し通知のような痒いところに手が届くものがあったりします。

脚注
  1. Godotの通知の冒頭にいくつか、通知の例が載っています。 ↩︎

SLMNLLSLMNLL

ちなみに notification(what, reversed) 関数で自作の通知を発火することも可能です[1]。第1引数は、標準の通知と同様に列挙型を使って管理するのが良いと思います。

なお公式ドキュメントには第2引数をtrueにすると、通知がツリーを遡るとの記述がありますが、自分が試した限りでは自身の_notification()しか呼び出されませんでした。

脚注
  1. https://docs.godotengine.org/ja/stable/classes/class_object.html#id3 ↩︎

SLMNLLSLMNLL

シグナルと違って、引数渡しはできないので注意。

SLMNLLSLMNLL

そのクラスにどのような_notification通知があるのかを知りたい場合は、以下のようなコードを書けばOKです。

すべての通知のたびにログが出力される
extends Node

func _notification(what):
    print("通知: %s" % what)
    # 出力例→ 通知: 18

_notification()が実行されるたびに、通知の内容がIntで出力されます。
どの数値が何の通知なのかは、ドキュメントで確かめると良いです[1]

ちなみに、18NOTIFICATION_PARENTEDです。

脚注
  1. ここでは一番notificationが充実してるNodeのドキュメントへのリンクとしています。 ↩︎

SLMNLLSLMNLL

match文の条件にエラーがある場合のデバッガの挙動

全ての条件で試したわけではないですが、例えばmatch文の条件に指定したenum列挙型などが存在しない場合、デバッガは条件式ではなく、match式の行で停止してエラーを吐き出します。

どこにエラーがあるのか一瞬分からなくなるので注意が必要。

SLMNLLSLMNLL

マウスが任意の多角形の中にあるかどうかを判定する

Area2D + CollisionPolygon2Dを使ってもいいのですが、Geometryクラスを使っても手軽に実装できます。

16px四方の矩形の中にカーソルがあるかどうかを判定する
# 16px四方の矩形をPoolVector2Arrayで定義
var polygon = PoolVector2Array([Vector2(0, 0), Vector2(16, 0), Vector2(16, 16), Vector2(0, 16)])

# 条件文
if Geometry.is_point_in_polygon(get_local_mouse_position(), polygon):
    # 多角形の中にあった場合の処理を書く

get_local_mouse_position()の部分を書き換えれば、任意の座標の点が多角形の中にあるかどうかを判定できます。

ちなみにGeometry.is_point_in_circle()関数では、円の中にあるかどうかが判断できます。
Geometryクラスでは、他にも幾何学にまつわる便利な関数が沢山利用できます。

SLMNLLSLMNLL

Geometryクラスを使って、角丸のPoolVector2Arrayを取得する

Geometryクラスには、与えられた多角形を太らせたり痩せさせたりする関数、offset_polygon_2d()があります。

offset_polygon_2dの仕様
Array offset_polygon_2d ( PoolVector2Array polygon, float delta, PolyJoinType join_type=0 )

offset_polygon_2d()の第3引数が、以下に引用の通り角の処理となっているので、ここで角丸の指定をします。

enum PolyJoinType:

  • JOIN_SQUARE = 0 --- Squaring is applied uniformally at all convex edge joins at 1 * delta.
  • JOIN_ROUND = 1 --- While flattened paths can never perfectly trace an arc, they are approximated by a series of arc chords.
  • JOIN_MITER = 2 --- There's a necessary limit to mitered joins since offsetting edges that join at very acute angles will produce excessively long and narrow "spikes". For any given edge join, when miter offsetting would exceed that maximum distance, "square" joining is applied.

簡単な矩形を角丸にする処理のコード例はこちら。

# 32px四方の多角形をPoolVector2Arrayで定義
var polygon = PoolVector2Array([Vector2(0, 0), Vector2(32, 0), Vector2(32, 32), Vector2(0, 32)])

# polygonで定義された多角形を4.0pxのオフセットで太らせて、角を丸くする処理
var raounded_polygon = offset_polygon_2d(polygon, 4.0, 1)

offset_polygon_2d()にマイナスの値を代入すれば、マイナスにオフセットされた値を返してくれるので、多角形を太らせずに角丸にしたい場合は、一回痩せた多角形を作って、それを同じ値だけ太らせればOK。

SLMNLLSLMNLL

3.5から実装された% prefixが便利

シーンのツリー構造を変更した時に、それに合わせていちいちスクリプト内のノードパス(ノードの場所)を書き換えないといけない状況が、Godot 3.5で大きく改善されました。

Godotのシーン編集画面から任意のノードを選択し、右クリックから% Access as Scene Unique Nameを選ぶだけ[1][2]

上記手順で% prefixを付与したノードは、スクリプト編集画面で、get_node("%任意のノード名")と書くことで参照できます。以降は、シーン内のどこにノードが移動してもget_node()が動作します。

コードのサンプル
func foo():
    get_node("%OpenButton").disabled = true

個人的には、Godot 3.5で実装された機能の中でベスト3に入るくらいには便利です。

脚注
  1. 参照:https://github.com/godotengine/godot/pull/60298 ↩︎

  2. なお、ノードの名前は、そのシーン内で重複しないよう一意なものにしなければなりません。 ↩︎

SLMNLLSLMNLL

$%は組み合わせられる

4.0 beta2時点での動作確認なので、3.5.x系でも使えるかどうかは不明ですが、$%は組み合わせて使えます。

✅ Godot 4.0 beta2
# 上述のコードと同じ内容を $ キーワードを使って書く
func foo():
    $%OpenButton.disabled = true
SLMNLLSLMNLL

staticメソッド内で呼び出せない関数

  • get_node()
  • get_tree()
    など
SLMNLLSLMNLL
  • call()
  • call_deferred()
  • callv()
  • get()
  • set()
    なども呼び出せません。

当然っちゃ当然ですが、動的に何かするような関数は呼び出せません。

SLMNLLSLMNLL

コピペで使える小技集

GDScriptに貼り付けてすぐ使える短いコードを色々

SLMNLLSLMNLL

ファイル名から拡張子を削除する

ファイル名に含まれる.が1個であることが確実な場合のコード。

# 拡張子を取りたいファイルの名前
var file_name = "sample_file.dat"

# ファイル名を「.」で区切って、それぞれを配列に格納
# そのまま、0番目の値を取得
var result = file_name.split(".")[0]

# ログに結果を出力:sample_file
print(result)

.が2個以上含まれる可能性があって、拡張子だけを取りたい場合は、後述のコードで拡張子を抽出し、得られた拡張子をreplace()で削除するのが手っ取り早いかと思います。

SLMNLLSLMNLL

ファイル名から拡張子を抽出する

ファイル名に.がいくつ含まれてても、拡張子だけを抽出できます。

# 拡張子を抽出したいファイルの名前
var file_name = "sample.file.name.txt"

# ファイル名を「.」で区切って、それぞれを配列に格納
# 配列の中身:["sample", "file", "name", "txt"]
var splited_file_name = file_name.split(".")

# ファイル名に含まれる「.」が1個とは限らないので、配列の一番最後の値を抽出
var result = splited_file_name[splited_file_name.size() - 1]

# ログに結果を出力:txt
print(result)
SLMNLLSLMNLL

任意の文字列を削除

# この文字列から「hello」を削除します。
var str = "hello world"

# replace()関数で「hello 」を空文字で置き換え
var result = str.replace("hello ", "")

# ログに結果を出力:world
print(result)
SLMNLLSLMNLL

Callableでは、呼び出し先の変数・子ノードも参照できる

Callableを使い、違うノードからプロパティや直接親子関係でないノードを呼び出してみます。ノードツリーの状況は以下の通りです。

# 呼び出される側
() be_called.gd ———— () Node2D # 親子関係
    |
    | # 親子関係はない
    |
caller.gd
# 呼び出す側

まずは呼び出される方のスクリプトを示します。

be_called.gd / Callableで呼び出される側のノード
# ※このノードには、"Node2D"という名前の子ノードがあります。
extends Node
var test_var = "これはbe_called.gdのプロパティです"

func _ready():
    # 呼び出し元のスクリプトをインスタンス化
    var test_node = load("res://caller.gd").instantiate()
    # インスタンス化したスクリプトに、Callableとしてtest_funcメソッドを渡す
    test_node.caller = Callable(test_func)

# 呼び出し元に渡すメソッド
func test_func():
    print(test_var) # be_called.gdのプロパティを出力する
    print($Node2D.name) # be_called.gdの子ノードの名前を出力する

次に呼び出す側のスクリプトです。Callableを格納するプロパティと、それをcall()で実行するメソッドだけを実装します。

caller.gd 呼び出し元のスクリプト
extends Node

var caller:Callable
var test_var = "検証のために用意された、be_called.gdにあるのと同名のプロパティ"

func _call_callable():
    caller.call()

この状態で、caller.gd_call_callable()メソッドを実行すると、以下のように出力され、be_called.gdのメソッド外のプロパティやノードを参照できることがわかります。

出力されたログ
これはbe_called.gdのプロパティです
Node2D 

呼び出し側(caller.gd)に同名のプロパティがあったとしても、呼び出される側(be_called.gd)の方のプロパティが参照されていることもわかると思います。

SLMNLLSLMNLL

シングルトン(AutoLoad)の読み込まれる順番

公式のドキュメントにも書いてありますが、シングルトン(自動読み込み / AutoLoad)は上から順番に読み込まれます。

シングルトンが全部読み込まれた後に特定の処理をさせたい場合は、一番最後に専用のノードを追加すると便利かもしれません。

SLMNLLSLMNLL

Node2D型のオブジェクトをスキュー(シアー)変形する

□を▱に変形するサンプルは以下の通りです。

スプライトを変形する
var t = Transform2D()
t.y = Vector2(1, 0.1)
$Sprite.transform = t

上記のコードをメソッド化すれば、tween_method()関数を使って、変形をアニメートすることができます。

SLMNLLSLMNLL

こんな感じ。

func skew(skew_x:Vector2, skew_y:Vector2):
	var t = Transform2D()
	t.x = skew_x
	t.y = skew_y
	$Node2D.transform = t
SLMNLLSLMNLL

Control系のノードをスキュー(シアー)変形したい場合

Control系のクラスはtransformプロパティがないため、そのままではTransform2Dを使った変形はできません。

ただし、Node2D系のノードに設定されたtransformは子ノードにもまとめて適応されるので、Control系ノードをNode2D系ノードでラップする方法があります。

SLMNLLSLMNLL

Godotの三項演算子の書き方

毎回忘れるので、公式ドキュメントのGDScriptスタイルガイドからそのまま引用メモ。

var next_state = "fall" if not is_on_floor() else "idle"

return文にも使えます。

func get_state()
    return "fall" if not is_on_floor() else "idle"
SLMNLLSLMNLL

GridContainerrowsプロパティを実装する

GridContainerにはcolumnsという「列数を設定する / 返す」プロパティがありますが、行数に関わるrowsはありません。

行数は子ノードの数次第で決まるため値を任意に代入することができないのは当然ですが、行数を取得したいケースはたまにあると思います。そんな時は以下のようにGridContainerを拡張すればOKだと思います。

var rows:int:
    set(mod_value):
        # 値を代入できないようにする
        push_error("Tried to set value to read-only variable.")
    get:
        return ceil(get_child_count() / columns)
SLMNLLSLMNLL

引数で型を明示すると、その型にキャストされる

例えばControlを拡張したカスタムクラスのControlExtAControlExtBの二つを用意したとします。そして、ControlExtAControlExtBのいずれかのインスタンスが引数として代入されるメソッドがあったとします。この場合、どちらのクラスにも共通する基底クラスControlを、引数の型として明示することが可能です[1]。が、型を明示した時点でメソッド内では基底クラスであるControlにキャストされてしまいます。

コードで例示した方がわかりやすいと思うので、以下にサンプルコードを書きます。

4.0 beta12
# Control継承クラスAの宣言 - - - - - - - - - - - - - - - - - - - -
extends Control
class_name ControlExtA

func get_class() -> String:
    return "ControlExtA"


# Control継承クラスBの宣言 - - - - - - - - - - - - - - - - - - - -
extends Control
class_name ControlExtB

func get_class() -> String:
    return "ControlExtB"


# 引数のクラス名をログに出力するメソッド - - - - - - - - - - - - - - - - -
# 上述のControlExtAかControlExtBのどちらかが引数に代入されることを想定しています

# 引数の型を基底クラスとして明示した際の挙動
func type_check(node:Control):
    print(node.get_class()) # -> 「Control」と出力される

# 引数の型を明示しない場合の挙動
func type_check(node):
    print(node.get_class()) # -> 「ControlExtA」 もしくは 「ControlExtB」 と出力される

カスタムクラスの厳格な型チェックを行う際には注意が必要です。

脚注
  1. 引数の型を明示する理由としては、例えば意図せずにNode2D系のインスタンスなどが代入された際にエラーが返ってくるので、デバッグがしやすいことが挙げられます。 ↩︎

SLMNLLSLMNLL

逆に返り値は大丈夫

以下のように、メソッドの返り値の型を明示していたとしても、返り値を受け取った時点ではちゃんと継承先のクラスとして処理されます。

4.0 beta12
func get_ext_a() -> Control:
    return ControlExtA.new()

func foo():
    print(get_ext_a().get_class()) # -> 「ControlExtA」 と出力される

ここら辺の細かい挙動を把握しつつコーディングするのは、なかなか大変ですね。

SLMNLLSLMNLL

Resourceを継承したクラスの_init()で引数を使うな

当然すぎる内容ですが、戒めとして[1]

Resourceを継承したクラスは基本的に、.tres.resファイル形式での保存・読込を前提に使うと思います。以下のサンプルコードのように、カスタムクラスの_init()に引数を設定すると、ファイル読込時に初期化エラーを起こします。

特にリソースを入れ子にしている場合など、エラーログで原因が分かりづらかったりするので、注意が必要です。

extends Resource
class_name CustomResource

@export var test_var:int

# ❌ リソース初期化の際に、_init()関数の引数経由で変数の値を設定するのはNG
func _init(_test_var:int): 
    test_var = _test_var
extends Resource
class_name CustomResource

@export var test_var:int

# ✅ 値を代入する場合は、専用のメソッドを用意するべし
func setup(_test_var:int):
    test_var = _test_var
脚注
  1. こんなくだらないミスで半日ほど無駄にしました… ↩︎

SLMNLLSLMNLL

なお、リソースがloadされた瞬間に発火する_notification()がないかな…と、以下の方法でチェックしてみましたが、ありませんでした。
https://zenn.dev/link/comments/4ad2b511e4723e

やはり、リソースファイルで初期化が必要な場合は、loadした後に、専用のメソッドを呼び出す必要がありそうです。

初期化忘れの可能性を限りなく低くするには、Factoryパターンで初期化済みのリソースを返すようなクラスを実装するのが一番良さそう…?

SLMNLLSLMNLL

_notification()を含めたready, enter_treeなどの実行順

すでに色々なところで出尽くしている内容ですが、意外と_notification()を含めて比較している記事はあまり見かけないので、親子関係にあるノードを使って実験してみました。

親ノードのスクリプトは以下の通り

parent.gd
func _ready():
    print("PARENT ready")

func _enter_tree():
    print("PARENT enter tree")

func _notification(what):
    match what:
        NOTIFICATION_ENTER_CANVAS:
            print("PARENT notification enter canvas")
        NOTIFICATION_ENTER_TREE:
            print("PARENT notification enter_tree")
        NOTIFICATION_READY:
            print("PARENT notification ready")

子ノードのは以下の通り

child.gd
# 基本的にPARENTをCHILDに書き換えてるだけですが、
# 親ノードのreadyシグナルを受信してprintを実行するプロセスを追加しました
func _ready():
    get_parent().ready.connect(
        func():
            print("received PARENT ready")
        )
    print("CHILD ready")

func _enter_tree():
    print("CHILD enter tree")

func _notification(what):
    match what:
        NOTIFICATION_ENTER_CANVAS:
            print("CHILD notification enter canvas")
        NOTIFICATION_ENTER_TREE:
            print("CHILD notification enter_tree")
        NOTIFICATION_READY:
            print("CHILD notification ready")

実行結果は以下の通りでした。

PARENT notification enter canvas
PARENT notification enter_tree
PARENT enter tree
CHILD notification enter canvas
CHILD notification enter_tree
CHILD enter tree
CHILD ready
CHILD notification ready
PARENT ready
PARENT notification ready
received PARENT ready

結果

ざっくりと、以下のようなことがわかります。

  • _enter_tree()NOTIFICATION_ENTER_TREEのような、同様のタイミングで処理される関数・通知は、通知(notification)が後に実行されます。
  • _enter_tree()に先んじてNOTIFICATION_ENTER_CANVASが実行されます。
    • 実行はノードごとで、親のNOTIFICATION_ENTER_CANVASの後に、子のNOTIFICATION_ENTER_CANVASが実行されることはないです。
  • _enter_tree()までは親が先に実行され、_ready()は子の方が先に実行されます。
  • readyシグナルが発火されるタイミングは、_ready()NOTIFICATION_READY内に書かれた処理が全てが完了した後です。

応用

_notification()を併用することで、readyの処理の中でも実行順を明確に分けるなどの応用方法が思いつきますね[1]

脚注
  1. 普通に使う分には、コードの見通しは悪くなりそうですが…。 ↩︎

SLMNLLSLMNLL

インスタンス化した子シーンがある場合

インスタンス化した子シーンの挙動(実行順)は、子ノードと全く同じになります。

SLMNLLSLMNLL

シーンのルートにあたるノードを簡単に取得する

「もっとスマートな方法があるはずだ」と思いつつも、今までシーンのツリーを見ながらget_parent().get_parent()...みたいに取得していたルート。self.ownerで取得できました。

シーンのツリー構造が変化しても書き換えなくて済むし、こちらの方が良いですね。

# シーンのルートを取得する
var scene_root = self.owner

# ちなみに、ゲーム自体のルートを取得する方法はこちら
var game_root = get_tree().get_root()
SLMNLLSLMNLL

カスタムクラスのインスタンス同士で、型が一致しているかを比較

get_script()を使えば、カスタムクラスのスクリプトを、以下のように返してくれます。

var custom_class_1 = CustomClassOne.new()
var custom_class_2 = CustomClassTwo.new()
print(custom_class_1.get_script()) # -> <GDScript#-9223372009994451816>
print(custom_class_2.get_script()) # -> <GDScript#-9223372010128669545>

これを利用して、スクリプトが一致するか(= カスラムクラスが一致するか)をチェックできます。

var custom_class_1a = CustomClassOne.new() # -> <GDScript#-9223372009994451816>
var custom_class_1b = CustomClassOne.new() # -> <GDScript#-9223372009994451816>
var custom_class_2a = CustomClassTwo.new() # -> <GDScript#-9223372010128669545>

if (custom_class_1a.get_script() == custom_class_1b.get_script()):
    print("型が一致しました")

if (custom_class_1a.get_script() == custom_class_2a.get_script()):
    print("型が一致しません")

is_class()get_class()は?

以前、カスタムクラス名をis_class()get_class()で取得するために、組込関数をオーバーライドする方法を紹介しました。
https://zenn.dev/slm/articles/c6924394de0bda

上述の方法は非推奨ながら一応は動きます。とはいえ、やはりString同士の比較なのでミスが発生する可能性があります[1]

プログラム側で機械的に比較できるので、get_script()が利用できるシチュエーションでは冒頭に紹介した方法の方が良さそうです。

脚注
  1. すべてのカスタムクラスでget_class()is_class()を間違いなく実装できる自信がある人はあまりいないと思います。 ↩︎

SLMNLLSLMNLL

get_script()の使いどころ

インスタンス同士の型比較にはget_script()を使い、単一のインスタンスの型チェックにはis演算子を使うのが一番良さそう。

isを使った型チェックは、is CustomClassのように、継承先のクラスを使って分岐するのが良いですね[1]

脚注
  1. 例えばis Nodeのように基底クラスを条件文に使うと、Godotの仕様上思わぬところでtrueを返してきてしまうため。 ↩︎

SLMNLLSLMNLL

Resourceは内部クラスとしては使えない

Resource派生クラスを内部クラスとして運用する場合、Godot 4.1.2時点では保存・読込が正常に動作しないようです。Resourceを使う場合には内部クラスとしては使わないようにする必要があります。

SLMNLLSLMNLL

3.x時点で、githubにissueが上がってました。このissueで投稿されている例では、複数のファイルにわたって継承されたResourceにはGodot自体が対応していないとのこと[1]
https://github.com/godotengine/godot/issues/41625

もしそれでも内部クラスのResourceを扱いたいのであれば

Resource以外の、例えばRefCountedなどを継承した内部クラスを実装し、Resourceのラッパークラスとして運用する方法が、上記のissueにて提示されています。

脚注
  1. Resourceの中にResourceがあるような、単純な入れ子構造のResourceはちゃんと動きます。 ↩︎

SLMNLLSLMNLL

スクリプトエディタで複数の範囲を選択する

Godot 4.xのどこかの時点で実装されたようですが[1]Godot 4.1.2時点のスクリプトエディタでもVS Codeなどと同じように、複数範囲の選択ができます。

ショートカットも他の一般的なエディタと同じです。

  • Mac版 Cmd + D
  • Windows版 Ctrl + D(…のはず)

ちなみに、公式ドキュメントには(まだ)載ってません。
https://docs.godotengine.org/en/stable/tutorials/editor/default_key_mapping.html

複数箇所にキャレットを置くこともできる

脚注にも書いていますが、単純に複数箇所にキャレットを置くだけであれば、Alt + クリックで行けます(個人的にはあまり使わないですが…)。

脚注
  1. Alt + クリックで複数箇所にキャレットを置ける機能が実装されたのと同じタイミングだと想像しています。 ↩︎

SLMNLLSLMNLL

String.format()enumを使う方法

GDScriptで使えるString.format()は、文字列中のプレースホルダを任意の文字列に置き換えることができるため、ゲームのダイアログなどを実装するのに便利な機能です。

ただ、公式のドキュメントで紹介されているように「文字列をキーにする」のは、タイポの可能性があって嫌だなと思いました。そこで、enumを使えないか試してみます。

ダメな例

最初にダメ元でトライした方法は、コードにすると以下のような感じ。

❌ 想定通りに動かない例
enum StringKeys { FOOD }

func _ready():
    var str = "私は{StringKeys.FOOD}が好きだ"
    print(str.format({StringKeys.FOOD: "りんご"}))
    # 出力結果:
    # 私は{StringKeys.FOOD}が好きだ

予想通りダメでした。

enumの値が展開されず、キーがそのまま文字列としてstrに代入されてしまっているのが原因です。

SLMNLLSLMNLL

動く例

コードを以下のように変えると想定通りに動きます。

✅ 想定通りに動く例
enum StringKeys { FOOD }

func _ready():
    var str = "私は{" + var_to_str(StringKeys.FOOD) + "}が好きだ"
    print(str.format({StringKeys.FOOD: "りんご"}))
    # 出力結果:
    # 私はりんごが好きだ

    print(str.format({StringKeys.FOOD: "オレンジ"}))
    # 出力結果:
    # 私はオレンジが好きだ

やっていることは単純で、enumのキーをvar_to_str()Stringにキャストして、残りの文字列と結合しています。

そうすると、プログラム的にstrに格納される文字列は"私は{0}が好きだ"となります。続く行は、str.format({0: りんご})と書かれているのと同様なので、公式ドキュメントの例と同じ書き方になっていることがわかります。これなら、想定通り動きます。

文字列でキーを渡すよりはタイポの可能性が減って、String.format()を使いやすくなると思います。少し冗長ですが…。

SLMNLLSLMNLL

まとめてメソッドにする

上記の方法はvar_to_str()を使っているため、変数でしか使えません[1]。値が書き換わってしまう可能性が心配だったりします。

ただ、ほとんどの場合はString.format()を任意のメソッドに閉じ込めてしまう[2]と思うので、あまり書き換えなどを心配する必要もないのかな、と思いました。

✅ 結局こんな感じで使うことが多い
func get_like_string(who:String, food:String) -> String:
    return "{who}は{food}が好きだ".format({"who": who, "food": food})

func _ready():
    print(get_like_string("私", "りんご"))
    # 出力結果:
    # 私はりんごが好きだ

    print(get_like_string("彼", "オレンジ"))
    # 出力結果:
    # 彼はオレンジが好きだ

結論

あまりenumを使う必要がない。

脚注
  1. 実行時に内容が一意に定まらないため、const文では使えません。イミュータブルな変数が実装されるのを期待してます。 ↩︎

  2. あちこちでString.format()を呼び出すようなコードは、保守が難しそうですよね。 ↩︎