Open31

Godot Engine / GDScript備忘録

ピン留めされたアイテム

これは何?

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

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

などのメモを取っていくスクラップです[1]

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

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

  • 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

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

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

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

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

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


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

動作しないシグナルには注意

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

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

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

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

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

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

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

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

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

カスタムクラスの場合

isをカスタムクラスで使用した場合の挙動については、不正確な部分を修正し、より良い解決方法を加筆した上で記事にしました。

https://zenn.dev/slm/articles/c6924394de0bda
脚注
  1. GDScriptの基本に書いてあるのにすぐ忘れてしまいます。 ↩︎

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

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

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

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を継承する必要はなく、無関係なクラスの内部クラスを直接継承することも可能です。 ↩︎

.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)

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

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の値が出力されます。

ノードのプロパティに対して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 ↩︎

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派生クラスに共通するものだと想像しているのですが(未確認)、備忘録のために言及。 ↩︎

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

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 ↩︎

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

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

var _private_variable # プライベート変数

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

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

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

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

リソースファイルは値を共有する

複数のノードから別々に読み込んだとしても、読み込んでいるリソースファイルが同じであれば、値が共有されるので注意が必要です[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()関数を使うか、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. それぞれのノードの変数には、元々のリソースファイルへの参照が代入されています。参照を渡すことで、メモリの節約になりますね。 ↩︎

配列の複製もduplicate()を使う

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

# 配列を準備
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()関数を使います。

# 配列を準備
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]と出力されます。

PoolVector2Arrayなどは値渡し

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

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

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

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

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

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

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の処理

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

値の変化を検知する

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

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

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

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

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文を使っても良いですが、もう少し込み入った処理をする場合には、この方法が役に立つかもしれません。 ↩︎

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型として解釈されるので、エラーは出ません

_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の通知の冒頭にいくつか、通知の例が載っています。 ↩︎

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

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

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

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

変数があるかどうかのチェック

任意のノードに、指定した名前の変数があるかどうかは、以下のようなコードで確認できます[1]

if "変数名" in get_parent(): # 親ノードに"変数名"があるかどうかをチェック
    # 変数があった場合の処理
脚注
  1. 出典:https://godotengine.org/qa/3099/check-if-script-of-a-node-has-variable ↩︎

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

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

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

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

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クラスでは、他にも幾何学にまつわる便利な関数が沢山利用できます。

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。

ログインするとコメントできます