Godot Engine / GDScript備忘録
これは何?
私がGodot Engineを使っていく中で、
- 気になった挙動
- ハマったポイント
- たまに使うが、よく忘れてしまうTips
などのメモを取っていくスクラップです[1]。
3.x系と4.x系のコードが混在しているので、ご注意ください。
目次
コメントがかなり増えてきたので、目次を用意しました。
シグナル、通知関連
型関連
GDScript Tips集
- コピペで使えるコード集
- 関数の引数関連のTips
- 内部クラスの継承
- リソースファイルの読み込み
setget
関連のTips- インスタンスが削除されたか調べる
- 値の変化を検知する
- 配列のように
Vector2
などの値を抽出 - 多角形の中にマウスカーソルがあるかを知る
- 変数の有無を調べる
get_node()
を便利にする%
prefix- シングルトンの読み込み順
- Node2D型のオブジェクトをスキュー変形する
- 三項演算子の書き方
GridContainer
にrows
を実装するResource
を継承したクラスの_init()
で引数を使うな- シーンのルートにあたるノードを簡単に取得する
- カスタムクラスのインスタンス同士で、型が一致するかを比較
-
String.format()
でEnum
を使う方法 (2024/03/20追加) -
最初の
_process()
実行時にだけメソッドを呼び出したい (2024/08/18追加) - シングルトンに内部クラスを定義すると? (2024/09/05追加)
-
シグナルに
connect
されているanonymous lambda
をdisconnect
する (2024/09/13追加) -
bind()
関数で、filter()
関数をもっと便利に使おう (2024/09/24追加) - 引数にクラスを指定する (2024/09/25追加)
-
Callable
に値を保管する (2024/12/09追加)
配列・辞書関連
-
Dictionary
型のキーにはCallable
を設定できる (2024/09/27追加) -
Array.filter()
でnull
な値を除外する (2024/11/10追加) -
Typed Array
をキャストする (2024/11/11追加)
GDScriptの特徴・仕様
add_child()
しないと_ready()
等は発生しない- プライベート変数・関数の仕様について
static
関数から呼び出せない関数Callable
では、呼び出し先の変数・子ノードも参照できる- 引数で型を明示すると、キャストされる
_notification()
を含めたready
やenter_tree
の実行順Resource
は内部クラスとしては使えない
エラー関連
エディタ関連
その他
GraphNode
の使用感- 3.xではシェーダーに配列を渡せない
- データの保存にはJSON以外も使える
-
自作シグナルで
await
が動かない場合の対処法 (2024/05/24 追加)
-
コメントが長くなったものについては、記事化したりしなかったりします。 ↩︎
引数付シグナルは、接続のタイミングを考えるべし
-
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_started
とscroll_ended
シグナルはGodot3.4.4時点では動作しません。代替手段として、get_v_scrollbar()
などで参照したスクロールバーのscrolling
シグナルと、Timer
を駆使することでほぼ同じ機能を実装することができます[1]。
なお、2020年の時点で正常に動作していないシグナルのリストはGithubにまとめられています。
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
されるようです。公式ドキュメントにも記載があります。
複数のインスタンスのシグナルを、ひとつのメソッドで処理する
シグナル引数を使えば、複数のインスタンスのシグナルを、ひとつのメソッドで処理することができます。
コードで説明するのが早いと思うので、以下に示します。
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の処理
シグナルごと個別に関数を設定しなくていいので、こうすることで処理がスマートになる場合も多々あるかと思います。
デフォルト引数に計算式を指定してもOK
例として、以下のコードではインスタンスのサイズに1.25を乗算した数を、計算式ごとデフォルト引数として設定しています。
func set_new_size(transformed_size:Vector2 = rect_size * 1.25):
self.rect_size = transformed_size
条件文でクラスタイプを比較する場合には「is」
Godotには、インスタンス型チェッカーとしてis
演算子が用意されています[1]。条件文でクラスの比較をしたい場合にはとても便利ですね。
カスタムクラスの場合
スクリプトファイルの比較で同定する
カスタムクラスのインスタンス同士で、型が一致しているかを比較
is_class()
などをオーバーライトする
is
をカスタムクラスで使用した場合の挙動については、不正確な部分を修正し、より良い解決方法を加筆した上で記事にしました[2]。
-
GDScriptの基本に書いてあるのにすぐ忘れてしまいます。 ↩︎
-
String
での比較となるので、typoの可能性がある点は注意が必要です。 ↩︎
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
-
外側のクラスは必ずしも
ParentClass
を継承する必要はなく、無関係なクラスの内部クラスを直接継承することも可能です。 ↩︎
.tresファイルの中身を変数として読み出す
公式のドキュメントやコミュニティにもあまり情報がなかったのですが、Godotのリソースファイルである.tresファイルは、スクリプト内で簡単に変数として読み出しができます。
以下に.tresファイルの中身を例示します。行ごとに変数名・値が書かれていて、.iniファイルなどに似ています。
[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ファイルから、position
やbg_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の値が出力されます。
GraphNodeについて
記事化しましたので、そちらをご覧ください。
ノードのプロパティに対して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
を使って値を書き換えようとすると無限ループに陥るため、少し複雑な処理を要する」と、サンプルコードとともに書かれています(こちらは未検証)。
AtlasTextureをもっと知る
repeat, mirrored repeatは無視される
Godotのドキュメントにも記載があります[1]が、AtlasTexture
のflags
のうちrepeat
、mirrored repeat
オプションは無視されます。つまりAtlasTexture
を使って画像を反復させることは、基本的にできません。
これらのフラグはチェックを入れても無視されます
スクリプトで値を変更すると同期される
スクリプトからregion
やflags
の値などを変更すると、同じAtlasTexture
を参照して表示されている画像全てに変更が反映されます[2]。
-
https://docs.godotengine.org/ja/stable/classes/class_atlastexture.html ↩︎
-
AtlasTexture
固有の挙動ではなくResource
派生クラスに共通するものだと想像しているのですが(未確認)、備忘録のために言及。 ↩︎
Tweenをもっと知る (Godot 3.4)
Tweenをもっと知る (Godot 3.x)というタイトルで記事にしたので、コンテンツはそちらに移動しました。
Tweenの使い方 (Godot 3.5 + / 4.0)
Godot 3.5以降のTween
の使い方は、こちらの記事でまとめています。
インスタンスが削除されたかを調べる
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です」と出力されます
Godotのプライベート変数、プライベート関数について
Godot DocsのGDScriptスタイルガイド > 命名規則 > 関数と変数に記載されていますが、Godotではプライベートな関数や変数は、関数名・変数名の前にアンダースコアを追加して書くこととされています。
var _private_variable # プライベート変数
func _private_function(): # プライベート関数
ただこのルールはあくまで慣習的なもので、例えアンダースコアが付いていても関数・変数のどちらも外部からのアクセス自体は可能です[1][2]。これは言語的に近い Python の仕様に倣ったものと思われます。
リソースファイルの複製は参照渡し
複数のノードから別々に読み込んだとしても、読み込んでいるリソースファイルが同じであれば、値が共有されるので注意が必要です[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です。
# 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
)のままにすると、サブリソースは親リソース間で共有されるためメモリなどの効率が良いです。
-
それぞれのノードの変数には、元々のリソースファイルへの参照が代入されています。参照を渡すことで、メモリの節約になりますね。 ↩︎
配列の複製も参照渡し
上述のリソースファイルのくだりとも通底しますが、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]と出力されます。
-
GDScriptが特殊というわけではなく、配列などが参照渡しの言語は一般的かと思います。 ↩︎
PoolVector2Arrayなどは値渡し
実はPoolVector2Array
、PoolVector3Array
など、Pool***Array
系の型は参照渡しではなく、値渡しだったりするので、duplicate()
関数はありません[1][2][3]。
-
https://docs.godotengine.org/ja/stable/classes/class_poolvector2array.html ↩︎
-
配列に格納されている値が少ない場合、
Pool***Array
系の型は、Array
に比べてパフォーマンスが落ちる という言説をどこかで見た気がするのですが、見当たらず。 ↩︎ -
Pool****Array
系の型は、Godot 4.0以降Packed****Array
となったので、適宜読み替えてください。 ↩︎
Dictionary
が共有される
インスタンス化したシーン間でもシーンエディタで、別のシーンを複数個インスタンス化した際、同じPackedScene
のインスタンス間ではDictionary
の値が共有されるので注意しましょう。
スクリプト経由でduplicate()
してからインスタンス化するか、Dictionary
型を格納するためだけのクラスを用意して、インスタンス化の際に初期化して読み込むなど、工夫する必要があります。
ちょっと面倒 & 気づかないとしばらくハマります。
値の変化を検知する
ある値、特にノードにもともと組み込まれている変数が変化したかどうかを検知するのに、変化前の値を保持して、変化後の値と比較して…という処理を書くと、検知のためだけに変数を新たに用意しなければいけなかったり、コードが長くなってしまいます。
そんな時は、_set()
をオーバーライドします。
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"と出力されます
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:
# ツリー内のノードの順番が変化した際に、変化したノードで呼び出される
上述の例のように、オブジェクトによってはシグナル一覧にない、言わば隠し通知のような痒いところに手が届くものがあったりします。
ちなみに notification(what, reversed)
関数で自作の通知を発火することも可能です[1]。第1引数は、標準の通知と同様に列挙型
を使って管理するのが良いと思います。
なお公式ドキュメントには第2引数をtrue
にすると、通知がツリーを遡るとの記述がありますが、自分が試した限りでは自身の_notification()
しか呼び出されませんでした。
[2024/09/23 追記]
第2引数をtrue
にすると、通知の呼び出し順が逆になるようです。コード付きの詳細が記載されていました。
以下に引用します。
var player = Node2D.new()
player.set_script(load("res://player.gd"))
player.notification(NOTIFICATION_ENTER_TREE)
# 呼び出し順 : Object -> Node -> Node2D -> player.gd.
player.notification(NOTIFICATION_ENTER_TREE, true)
# 呼び出し順 : player.gd -> Node2D -> Node -> Object.
シグナルと違って、引数渡しはできないので注意。
そのクラスにどのような_notification
通知があるのかを知りたい場合は、以下のようなコードを書けばOKです。
extends Node
func _notification(what):
print("通知: %s" % what)
# 出力例→ 通知: 18
_notification()
が実行されるたびに、通知の内容がInt
で出力されます。
どの数値が何の通知なのかは、ドキュメントで確かめると良いです[1]。
ちなみに、18
はNOTIFICATION_PARENTED
です。
-
ここでは一番notificationが充実してる
Node
のドキュメントへのリンクとしています。 ↩︎
変数があるかどうかのチェック
任意のノードに、指定した名前の変数があるかどうかは、以下のようなコードで確認できます[1]。
if "変数名" in get_parent(): # 親ノードに"変数名"があるかどうかをチェック
# 変数があった場合の処理
match
文の条件にエラーがある場合のデバッガの挙動
全ての条件で試したわけではないですが、例えばmatch
文の条件に指定したenum
列挙型などが存在しない場合、デバッガは条件式ではなく、match
式の行で停止してエラーを吐き出します。
どこにエラーがあるのか一瞬分からなくなるので注意が必要。
マウスが任意の多角形の中にあるかどうかを判定する
Area2D
+ CollisionPolygon2D
を使ってもいいのですが、Geometry
クラスを使っても手軽に実装できます。
# 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()
があります。
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。
データの保存にJSONを使う必要はない
記事を書きました。
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に入るくらいには便利です。
-
なお、ノードの名前は、そのシーン内で重複しないよう一意なものにしなければなりません。 ↩︎
$
と%
は組み合わせられる
4.0 beta2
時点での動作確認なので、3.5.x
系でも使えるかどうかは不明ですが、$
と%
は組み合わせて使えます。
# 上述のコードと同じ内容を $ キーワードを使って書く
func foo():
$%OpenButton.disabled = true
static
メソッド内で呼び出せない関数
get_node()
-
get_tree()
など
call()
call_deferred()
callv()
get()
-
set()
なども呼び出せません。
当然っちゃ当然ですが、動的に何かするような関数は呼び出せません。
コピペで使える小技集
GDScriptに貼り付けてすぐ使える短いコードを色々
ファイル名から拡張子を削除する
ファイル名に含まれる.
が1個であることが確実な場合のコード。
# 拡張子を取りたいファイルの名前
var file_name = "sample_file.dat"
# ファイル名を「.」で区切って、それぞれを配列に格納
# そのまま、0番目の値を取得
var result = file_name.split(".")[0]
# ログに結果を出力:sample_file
print(result)
.
が2個以上含まれる可能性があって、拡張子だけを取りたい場合は、後述のコードで拡張子を抽出し、得られた拡張子をreplace()
で削除するのが手っ取り早いかと思います。
ファイル名から拡張子を抽出する
ファイル名に.
がいくつ含まれてても、拡張子だけを抽出できます。
# 拡張子を抽出したいファイルの名前
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)
任意の文字列を削除
# この文字列から「hello」を削除します。
var str = "hello world"
# replace()関数で「hello 」を空文字で置き換え
var result = str.replace("hello ", "")
# ログに結果を出力:world
print(result)
Callable
では、呼び出し先の変数・子ノードも参照できる
Callable
を使い、違うノードからプロパティや直接親子関係でないノードを呼び出してみます。ノードツリーの状況は以下の通りです。
# 呼び出される側
(親) be_called.gd ———— (子) Node2D # 親子関係
|
| # 親子関係はない
|
caller.gd
# 呼び出す側
まずは呼び出される方のスクリプトを示します。
# ※このノードには、"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()
で実行するメソッドだけを実装します。
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
)の方のプロパティが参照されていることもわかると思います。
AutoLoad
)の読み込まれる順番
シングルトン(公式のドキュメントにも書いてありますが、シングルトン(自動読み込み / AutoLoad)は上から順番に読み込まれます。
シングルトンが全部読み込まれた後に特定の処理をさせたい場合は、一番最後に専用のノードを追加すると便利かもしれません。
Node2D
型のオブジェクトをスキュー(シアー)変形する
□を▱に変形するサンプルは以下の通りです。
var t = Transform2D()
t.y = Vector2(1, 0.1)
$Sprite.transform = t
上記のコードをメソッド化すれば、tween_method()
関数を使って、変形をアニメートすることができます。
こんな感じ。
func skew(skew_x:Vector2, skew_y:Vector2):
var t = Transform2D()
t.x = skew_x
t.y = skew_y
$Node2D.transform = t
Control
系のノードをスキュー(シアー)変形したい場合
Control
系のクラスはtransform
プロパティがないため、そのままではTransform2D
を使った変形はできません。
ただし、Node2D
系のノードに設定されたtransform
は子ノードにもまとめて適応されるので、Control
系ノードをNode2D
系ノードでラップする方法があります。
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"
GridContainer
にrows
プロパティを実装する
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)
引数で型を明示すると、その型にキャストされる
例えばControl
を拡張したカスタムクラスのControlExtA
とControlExtB
の二つを用意したとします。そして、ControlExtA
とControlExtB
のいずれかのインスタンスが引数として代入されるメソッドがあったとします。この場合、どちらのクラスにも共通する基底クラスControl
を、引数の型として明示することが可能です[1]。が、型を明示した時点でメソッド内では基底クラスであるControl
にキャストされてしまいます。
コードで例示した方がわかりやすいと思うので、以下にサンプルコードを書きます。
# 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」 と出力される
カスタムクラスの厳格な型チェックを行う際には注意が必要です。
-
引数の型を明示する理由としては、例えば意図せずに
Node2D
系のインスタンスなどが代入された際にエラーが返ってくるので、デバッグがしやすいことが挙げられます。 ↩︎
逆に返り値は大丈夫
以下のように、メソッドの返り値の型を明示していたとしても、返り値を受け取った時点ではちゃんと継承先のクラスとして処理されます。
func get_ext_a() -> Control:
return ControlExtA.new()
func foo():
print(get_ext_a().get_class()) # -> 「ControlExtA」 と出力される
ここら辺の細かい挙動を把握しつつコーディングするのは、なかなか大変ですね。
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
-
こんなくだらないミスで半日ほど無駄にしました… ↩︎
なお、リソースがload
された瞬間に発火する_notification()
がないかな…と、以下の方法でチェックしてみましたが、ありませんでした。
やはり、リソースファイルで初期化が必要な場合は、load
した後に、専用のメソッドを呼び出す必要がありそうです。
初期化忘れの可能性を限りなく低くするには、Factoryパターン
で初期化済みのリソースを返すようなクラスを実装するのが一番良さそう…?
_notification()
を含めたready
, enter_tree
などの実行順
すでに色々なところで出尽くしている内容ですが、意外と_notification()
を含めて比較している記事はあまり見かけないので、親子関係にあるノードを使って実験してみました。
親ノードのスクリプトは以下の通り
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")
子ノードのは以下の通り
# 基本的に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]。
-
普通に使う分には、コードの見通しは悪くなりそうですが…。 ↩︎
シーンのルートにあたるノードを簡単に取得する
「もっとスマートな方法があるはずだ」と思いつつも、今までシーンのツリーを見ながらget_parent().get_parent()...
みたいに取得していたルート。self.owner
で取得できました。
シーンのツリー構造が変化しても書き換えなくて済むし、こちらの方が良いですね。
# シーンのルートを取得する
var scene_root = self.owner
# ちなみに、ゲーム自体のルートを取得する方法はこちら
var game_root = get_tree().get_root()
カスタムクラスのインスタンス同士で、型が一致しているかを比較
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()
で取得するために、組込関数をオーバーライドする方法を紹介しました。
上述の方法は非推奨ながら一応は動きます。とはいえ、やはりString
同士の比較なのでミスが発生する可能性があります[1]。
プログラム側で機械的に比較できるので、get_script()
が利用できるシチュエーションでは冒頭に紹介した方法の方が良さそうです。
-
すべてのカスタムクラスで
get_class()
やis_class()
を間違いなく実装できる自信がある人はあまりいないと思います。 ↩︎
Resource
は内部クラスとしては使えない
Resource
派生クラスを内部クラスとして運用する場合、Godot 4.1.2
時点では保存・読込が正常に動作しないようです。Resource
を使う場合には内部クラスとしては使わないようにする必要があります。
Resource
に関する公式ドキュメントの一番下にも、サブクラスのカスタムプロパティを正しくシリアライズすることができないと言及がありますね。
スクリプトエディタで複数の範囲を選択する
Godot 4.x
のどこかの時点で実装されたようですが[1]、Godot 4.1.2
時点のスクリプトエディタでもVS Codeなどと同じように、複数範囲の選択ができます。
ショートカットも他の一般的なエディタと同じです。
- Mac版
Cmd + D
- Windows版
Ctrl + D
(…のはず)
ちなみに、公式ドキュメントには(まだ)載ってません。
複数箇所にキャレットを置くこともできる
脚注にも書いていますが、単純に複数箇所にキャレットを置くだけであれば、Alt + クリック
で行けます(個人的にはあまり使わないですが…)。
-
Alt + クリックで複数箇所にキャレットを置ける機能が実装されたのと同じタイミングだと想像しています。 ↩︎
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
に代入されてしまっているのが原因です。
動く例
コードを以下のように変えると想定通りに動きます。
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()
を使いやすくなると思います。少し冗長ですが…。
まとめてメソッドにする
上記の方法は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
を使う必要がない。
-
実行時に内容が一意に定まらないため、
const
文では使えません。イミュータブルな変数が実装されるのを期待してます。 ↩︎ -
あちこちで
String.format()
を呼び出すようなコードは、保守が難しそうですよね。 ↩︎
BBCodeで使える色名
X11 color namesが使えます。
await
が動かない場合の対処法
自作シグナルで例えば、以下のようなコードを書いた場合、_wait_signal()
内のawait
以降の処理がなされません。
extends Node
signal test_sig
func _ready():
print("ready : これからシグナルを待ちます")
_wait_signal()
print("ready : シグナルを待ち終わりました")
func _signal_sender():
test_sig.emit()
func _wait_signal():
_signal_sender()
await test_sig
print("awaitでシグナルを待ち終わりました")
出力結果は以下の通りとなります。
# ready : これからシグナルを待ちます
# ready : シグナルを待ち終わりました
末尾のprint("awaitでシグナルを待ち終わりました")
の行がまるっと実行されず、_wait_signal()
の関数を抜けて、次の処理に移っていることがわかると思います。
call_deferred()
を使おう
シグナルを発火する関数をcall_deferred()
を使って呼び出すことで、正常にawait
部分を動作させることができます[1]。以下に変更箇所のコードのみ記載します。
# _signal_sender() を以下の文と差し替える
call_deferred("_signal_sender")
ちなみに、call_deferred()
では引数として渡した関数の呼び出されるたタイミングは次のフレームとなるため、以下のような順番でログが出力されます。
# ready : これからシグナルを待ちます
# ready : シグナルを待ち終わりました
# awaitでシグナルを待ち終わりました
GDScript Playgroundで動作の確認ができます。
-
シグナル発火関数を別スレッドで実行した場合の動作もいずれ確認したいです。 ↩︎
_ready()
内でsize
が正常に取得されない場合のtips / 最初の_process()
実行時にだけ呼び出したい処理がある場合のtips
例えば、シーンエディタなどでAnchor
を設定したノードのsize
を、_ready()
内で取得しようとしても、想定した値が取得されないことがあります[1]。
これは、おそらく_ready()
と、_process()
の間のどこかのタイミングで、ノードのsize
が設定されるのだと想像しています。
一応、以下のようなコードならsize
の値が想定通りに取れるのですが、「process()
の中に処理を書きたくない」、「スクリプト内に変数を増やしたくない」って場合もあると思います。
var _toggled := false
func _ready():
print(self.get_rect().size) # 想定した値じゃない
func _process(delta):
if not _toggled:
print(self.get_rect().size) # 想定した値が出力された
_toggled = true
そんな時は、call_deferred()
を使いましょう。ざっくり言うと、フレームの一番最後までメソッドの実行タイミングが延期されます。上記のコードと比較すると、余計な変数などを追加せずに済むことが分かります。
func _ready():
_print_size.call_deferred()
func _print_size():
print(self.get_rect().size) # 想定した値が出力された
「最初の_process()
が実行された時にだけ呼び出したい処理」も、同じような方法でスマートに書ける場合があると思います。
-
しっかりと検証などはしてないので、発生する具体的な状況が分かってません。ご容赦ください。 ↩︎
Q.シングルトンに内部クラスを持たせるとどうなる?
A. シングルトン名.内部クラス名.new()
といった感じで扱える。
コードを例示
以下のようなスクリプトファイルを、自動読み込み(≒シングルトン[1])として設定する。
extends Node # エディタ設定で、Singletonという名前で自動読み込みをする
# インナークラス / 内部クラスの宣言・定義
class Child extends RefCounted:
var test_var = 1
func test_func() -> int:
return 10
このシングルトンの内部クラスは、Singleton.Child.new()
でインスタンス化できるし、以下のような感じでメソッドや変数の呼び出しも可能。
extends Object # 先ほどのコードとは別のファイル
func foo():
var child_a = Singleton.Child.new()
print(child_a.test_var) # 出力結果 -> 1
print(child_a.test_func()) # 出力結果 -> 10
var child_b = Singleton.Child.new() # もちろん複数箇所でインスタンス化もできる
print(child_b.test_var) # 出力結果 -> 1
print(child_b.test_func()) # 出力結果 -> 10
Builderパターンのクラスなどを、シングルトン内で利用することが多い場合などに使えるかも…?
例えば、あるシーンをシングルトンのスクリプト経由で生成するとして、そのシーンにたくさんの変数を渡さなきゃいけない局面など。シングルトンで、内部クラスに変数を持たせてインスタンス化して、それをシーンに渡すような使い方…とか。一連の流れをひとつのファイル内で完結できるのは利点(長すぎるコードは欠点にもなりえますが)。
-
厳密にはシングルトンではないけれど、ここではシングルトンという呼称に統一します。 ↩︎
connect
されているanonymous lambda
をdisconnect
する
シグナルに例えば、tree_exited
シグナルに接続されているされているanonymous labda
なCallable
を削除する場合はこんな感じのコードを書けば、disconnect
できるはず。
# 接続されているシグナル数をチェック
if target.get_signal_connection_list("tree_exited").size() > 0:
# 接続されているシグナルをfor文ですべて処理
for sig in target.get_signal_connection_list("tree_exited"):
# anonymous callableでも、シグナルリストから指定すればOK
target.disconnect("tree_exited", sig["callable"])
この方法の難点としては、tree_exited
に複数の別のCallable
が接続されている場合はまとめてdisconnect
されてしまうところ。
そういった場合は、シグナルのラッパーを用意するといい(はず)。
# ラッパーシグナル
signal exited_tree_wrapper
# シグナルのセットアップ
func _enter_tree():
exited_tree.connect(
func():
exited_tree_wrapper.emit()
)
exited_tree_wrapper.connect(disconnector)
# 先ほどのコードのtree_exited部分を、tree_exited_wrapperに変更
func disconnector():
if self.get_signal_connection_list("tree_exited_wrapper").size() > 0:
for sig in self.get_signal_connection_list("tree_exited_wrapper"):
self.disconnect("tree_exited_wrapper", sig["callable"])
tree_exited
に繋がってるCallable
はそのままで、tree_exited_wrapper
に繋がってるCallable
だけを削除できます。
だいぶトリッキーですが。
bind()
関数で、filter()
関数をもっと便利に使おう
公式ドキュメントには、配列をフィルタリングするfilter()
関数の使用例として以下のようなコードが掲載されています。
# 偶数かどうかを調べるメソッド
func is_even(number):
return number % 2 == 0
func _ready():
# 偶数のみを残した配列 [4, 8] が出力される
print([1, 4, 5, 8].filter(is_even))
is_even()
はfilter()
関数経由で、配列内の要素を引数number
として受け取っています。
ただ、filter()
の条件を動的に変えたい場合などに、is_even()
にあたるメソッドに第2引数を渡したい時、ありますよね。
そんな時はbind()
を使いましょう。
# 第1引数と第2引数が一致するかどうかを調べるメソッド
func is_same(number:int, number_2:int):
if number == number_2:
return true
return false
func _ready():
var array = [0, 1, 2, 3, 4, 4, 4 ]
# 引数として、is_same()メソッドに4を渡す
var result = array.filter(is_same.bind(4))
# 出力結果は [4, 4, 4]になり、ちゃんと動いていることがわかる
print(result)
ラムダ式を使いたい場合は、Callable
をパーレンで囲みましょう。
func _ready():
var array = [0, 1, 2, 3, 4, 4, 4 ]
# Callableを()で囲み、bind()を使って4を渡す
var result = array.filter((func(number:int, number_2:int):
return number == number_2).bind(4)
)
# 出力結果は [4, 4, 4]になり、ちゃんと動いていることがわかる
print(result)
試してはいませんが、reduce()
などでも使えるはずです。
ちなみにfilter()
の戻り値は必ずArray
になるので、以下のようなコードでは空っぽの配列を返す。
func _ready():
var filter_sample = func(number: int):
return number > 0 # 0より大きい場合だけtrueを返す
var array = [0, 0, 0]
print(array.filter(filter_sample)) # 出力結果 : []
こちらで動作確認ができます:
引数としてクラスを指定することもできるよ
こんな感じで、インスタンスじゃなくてクラスを引数として渡すこともできます。
func _ready():
# 引数としてInnerクラスを指定して、sample関数を呼び出す
# ※ Innerクラスはインスタンス化されていない
sample(Inner) # 出力結果 -> test_string
sample(Inner2) # 出力結果 -> test_string 2
func sample(arg: RefCounted):
print(arg.TEST) # 渡されたクラスの定数をコンソールに出力する
# 内部クラス1
class Inner extends RefCounted:
const TEST = "test_string" # 定数はインスタンス化してなくても呼び出せる
# 内部クラス2
class Inner2 extends RefCounted:
const TEST = "test_string 2"
String
型のキーを持つDictionary
の代わりに、クラスに定数を持たせて自動補完の利く小さなデータの塊として使ったりできます[1]。
-
使い方次第では、typoの心配をしなくて済みます。 ↩︎
Dictionary
型のキーにはCallable
を設定できる
こんな感じで、Dictionary
型のキーにCallable
を指定できます。
func _ready():
# 引数に15を乗算し返すCallableを宣言
var callable_1 = func(factor: int):
return 15 * factor
# キーに先ほどのCallableを設定したDictionary
var dic = {
callable_1 : 5
}
# 辞書から、最初のキー( callable_1 )を取り出す
var key = dic.keys()[0]
# call()関数で引数を渡しつつ、
# 先ほど取り出したキー ( callable_1 ) を実行する
print(key.call(3)) # 出力 -> 45
トリッキーですが、例えばGodot 4.3で実装されたrand_weighted()
と組み合わせて、特定のメソッドが実行される確率を辞書で指定するような使い方ができそうです。
Array.filter()
でnull
な値を除外する
Array
にnull
が代入されてしまう可能性のあるコードを書いたときに、楽にnull
が除外できるように、専用のCallable
を用意すると使い回しもできて楽です。
コード:
-
Array
の操作用にArrayTool
というクラスを作ってしまいます[1]。
extends RefCounted
class_name ArrayTool
# nullをfilterするためのstaticなメソッド
static func null_filter(value) -> bool:
return value != null
-
Array.filter()
と、先ほど作ったstatic
メソッドを組み合わせて使う
extends Node
func _ready() -> void:
var array = [1, 2, 3, null, 5, null, null, 8, 9]
print(array) # 出力結果: [1, 2, 3, null, 5, null, null, 8, 9]
var filtered_array = array.filter(ArrayTool.null_filter)
print(filtered_array) # 出力結果: [1, 2, 3, 5, 8, 9]
-
他にも重複を除外するためのフィルタなどを
ArrayTool
クラスに実装してます。便利。 ↩︎
実際に動作するコードはこちらで確認できます:
配列からnull
を削除するメソッドを作ってもいいのですが、配列の操作を前提にした場合、filter
用のメソッドを用意した方が行数が減って見通しも良いです。
以下のような感じで、2行で済みます(配列の操作自体は1行)。
# nullの削除をメソッドで行う場合
# 1. 元になる配列
var array = [1, 2, 3, 4]
# 2. nullが代入されうる操作を配列に対して行う
var controlled_array = do_something_to_array(array)
# 3. nullを削除するメソッドを呼び出す
controlled_array = ArrayTool.remove_null(controlled_array)
# nullの削除をfilterで行う場合
# 1. 元になる配列
var array = [1, 2, 3, 4]
# 2. nullが代入されうる操作を配列に対して行いつつ、nullを削除する
var controlled_array = do_something_to_array(array).filter(ArrayTool.null_filter)
Typed Array
のキャスト
GodotではTyped Array (型付き配列)
が使えますが、いざ初期化した変数に配列を代入しようとすると、以下のような型エラーが出ることがあります。
# 型付き配列の初期化
var typed_array: Array[String]
# メソッドを呼び出して生成した配列を、=を使って代入しようとする
typed_array = generate_array().filter(...)
# するとこんな感じのエラーが出る
# Trying to assign an array of type "Array" to a variable of type "Array[String]".
そんな時は、assign()
関数を使うと良いです。
# 型付き配列の初期化
var typed_array: Array[String]
# メソッドを呼び出して生成した配列を、assignで代入する
# すると型付き配列にキャストされる
typed_array.assign(generate_array().filter(...))
Callable
の引数でTyped Array
を渡す際はキャストが必要
Godot 4.3
の時点では、Calleble
の引数としてTyped Array
を渡す場合、as
でキャストする必要があります。
var callable: Callable = func(args: Array[String]) -> String:
return args[0]
print(callable.call(["try", "assign"]))
var callable: Callable = func(args: Array[String]) -> String:
return args[0]
print(callable.call(["try", "assign"] as Array[String]))
ちなみに、メソッドの引数としてだったらキャストする必要はありません。
func _ready() -> void:
print(foo(["try", "assign"]))
func foo(args: Array[String]) -> String:
return args[0]
Callable
に値を保管する
以下のコードのように、インスタンス変数にCallable
を代入する際、値を渡して内部に保管することができます。
test_callable = func()
の行で引数としてtest_var
を渡さず、print(test_var)
の行で直接値を使っている点に注意・注目してください。
extends Node
# 1. 空っぽのCallableを宣言
var test_callable: Callable
# 2. 与えられた引数をそのまま出力する処理を、Callableに代入
func ready_callable(test_var: int):
test_callable = func():
print(test_var) # 渡された引数をCallable内部で直接呼び出す
# 3. 実行する
func _ready():
ready_callable(3) # 先ほどの関数に3を渡して実行
test_callable.call() # 3が出力される
ready_callable(15) # 先ほどの関数に15を渡して実行
test_callable.call() # 15が出力される
メリットとしては以下のような感じです:
- (無駄なインスタンス変数を使わず)値代入のタイミングと、値を呼び出すタイミングを離すことができる
- 他のメソッドからは参照しなくてもよいインスタンス変数を、Callableに閉じ込めることができる
インスタンス変数の総数を減らしたり、想定外のメソッドから変数を書き換えられるミスを防げるので、リーダブルなコードを保つのに役に立ちそうですね。
実際に動作するコードはこちらで確認できます: