【Godor4.2】第1回、Signalを勉強する会。おもにカスタムシグナルについて
カスタムシグナルってわかりずらいですよね
GDScript reference — Godot Engine (4.x)の日本語のドキュメントを読んでいろいろ勉強してみようと思いました。
シグナルはオブジェクトから通知メッセージを送信する手段で、他のオブジェクトはそれを受信することが出来ます。
ということらしいです。
カスタムシグナルを定義するにはsignal
キーワードを使用します。
signal this_is_my_signal_name
上記のリンクのシグナルの項目では、以下の関係から理解していこうという形になっているようです。
-
signal health_depleted
を使う、キャラクターノードとゲームノードの2つの関係 -
signal health_changed
を使う、キャラクターノードとゲームノードとライフバーノードの3つの関係
まず、
health_depleted
を使う、キャラクターノードとゲームノード2つの関係
1. 1-1. 【解説を読む】
最初に、シグナルの項目のコードを見てみます。
以下は、キャラクターノードのスクリプトです。
# キャラクターノードのスクリプト
signal health_depleted
そして以下は、ゲームノードのスクリプトです。
# ゲームノードのスクリプト
func _ready():
var character_node = get_node('Character')
character_node.health_depleted.connect(_on_character_health_depleted)
func _on_character_health_depleted():
get_tree().reload_current_scene()
この2つのスクリプトからわかることを、以下のリストにまとめてみます。
-
キャラクターノードに
character.gd
がアタッチされていること -
ゲームノードに
game.gd
がアタッチされていること -
キャラクターノードが
health_depleted
カスタムシグナルを定義していること -
ゲームノードが、キャラクターノードを取得していること
-
ゲームノードとキャラクターノードは同じ階層にあること
-
ゲームノードの
_read()
関数で、_on_character_health_depleted()
関数を、キャラクターノードのhealth_depleted
シグナルに接続していること(connect()
関数) -
ゲームノードが
_on_character_health_depleted()
関数を定義していること -
_on_character_health_depleted()
関数は、現在のシーンをリロードしていること
NODE | GDScript | SIGNAL | METHOD |
---|---|---|---|
Character | character.gd | health_depleted | - |
Game | game.gd | - |
_on_character_health_depleted() _read() |
でしょうか。
では、解説をもう少し読んでみます。
シグナルの項目から解説の一部を引用します。
When the Character node emits the signal, the game node's _on_character_health_depleted is called:
上記の引用を DeepL 翻訳 で翻訳すると、以下の通りです。
Characterノードがシグナルを発すると、Gameノードの_on_character_health_depletedが呼び出されます:
これは、まとめたリストの項目 3、5、6 番が解説と重なるかと思います。
health_depleted
シグナルの connect()
関数を呼び出すことで、引数で渡した関数をシグナル接続しています。接続した関数はそのシグナルを送信した時に、呼び出されるようです。
ですが、上記の2つのスクリプトには、emit()
関数を呼び出すコードがないため health_depleted
シグナルを送信していません。[1]
シグナルを送信するタイミングは、シグナルの名前から察するとすれば、キャラクターノードの health
が枯渇した時でしょう。
1-2. 【感想】
ここまでシグナルの項目を読んでみて、わかりずらいなと思っていた理由がなんとなくわかったような気がします。
【ノードがシグナルを送信すると、そのシグナルに接続してある関数が呼び出される】 と説明してもらったとしても、その説明からスクリプトのコードを想像できないという状態だったようです。
signal health_changed
を使う、キャラクターノードとゲームノードとライフバーノードの3つの関係
2. 2-1. 【解説を読む】
最初にシグナルの項目から解説の一部を引用します。
ここに有用な例を示します。アニメーションを伴った、体力の変化を受信するライフバーをスクリーン上に配置しようとしますが、シーンツリー上にあるプレイヤーからはそのユーザーインターフェースを分けたいします。
これは、プレイヤーのライフバーを設けたいが、ライフバーはユーザーインターフェースとしてプレイヤーとはグループを別にしたい。それぞれのノードのグループを分けたいということだと思います。
次に、シグナルの項目のコードを見てみます。
以下は、キャラクターノードのスクリプトです。プレイヤーにアタッチするスクリプトのようです。
# キャラクターノードのスクリプト
...
signal health_changed
func take_damage(amount):
var old_health = health
health -= amount
health_changed.emit(old_health, health)
...
そして以下は、ライフバーノードのスクリプトです。
# ライフバーノードのスクリプト
...
func _on_Character_health_changed(old_value, new_value):
if old_value > new_value:
progress_bar.modulate = Color.RED
else:
progress_bar.modulate = Color.GREEN
progress_bar.animate(old_value, new_value)
...
そして以下は、ゲームノードのスクリプトです。
# ゲームノードのスクリプト
func _ready():
var character_node = get_node('Character')
var lifebar_node = get_node('UserInterface/Lifeber')
character_node.health_changed.connect(lifeber_node._on_Character_health_changed)
この3つのスクリプトからわかることを、以下のリストにまとめてみます。
-
キャラクターノードに
character.gd
がアタッチされていること -
ゲームノードに
game.gd
がアタッチされていること -
ライフバーノードに
lifebar.gd
がアタッチされていること -
キャラクターノードが
health_changed
カスタムシグナルを定義していること -
キャラクターノードが
take_damage()
関数を定義していること -
take_damage()
関数がhealth_chenged
シグナルを発信すること(emit()
関数) -
ライフバーノードが
_on_Character_health_changed()
関数を定義していること -
_on_Character_health_changed()
関数は、ライフバーの色を変えたり、アニメーションをさせること -
ゲームノードが、キャラクターノードとライフバーノードを取得していること
-
ゲームノード、キャラクターノード、UserInterface ノードは同じ階層にあること
-
ライフバーノードは UserInterface ノードの子ノードであること
-
ゲームノードの
_read()
関数で、_on_Character_health_changed()
関数を、health_changed
シグナルに接続していること(connect()
関数)
NODE | GDScript | SIGNAL | METHOD |
---|---|---|---|
Character | character.gd | health_changed | take_damage() |
Game | game.gd | - | _read() |
Lifebar | lifebar.gd | - | _on_Character_health_changed() |
それでは、解説をもう少し読んでみます。
シグナルの項目から解説の一部を引用します。
これは Character ノードとのカップリングをせずに Lifebar に体力の変化を受信することを可能にします。
カップリングとは何でしょうか。
この解説は、キャラクターノードとライフバーノードがお互いのノードを取得していなくてもよい。キャラクターの体力の変化をライフバーノードは受信することができるということでしょう。
それは、以下の2つによって成立しているようです。
-
ゲームノードが、キャラクターノードとライフバーノードを取得して、それぞれの
health_changed
シグナルと_on_Character_health_changed()
関数をhealth_changed
シグナルのconnect()
関数で接続していること。 -
キャラクターノードが、
take_damage()
関数内で、health_changed
シグナルのemit()
関数を呼び出すこと
主にまとめたリストの項目 9 から 12 と重なっているように思います。
しかし、まだ health_changed
シグナルを送信してはいません。
take_damage()
関数を呼び出したタイミングで、emit()
関数が実行されます。その時に、health_changed
シグナルを送信します。
2-2. 【感想】
以上のことをまとめると、シグナルを送受信する主な工程は、以下のようになるのではないでしょうか。
-
シグナルを定義する
-
シグナルに接続するメソッド A を定義する
-
シグナルを送信するメソッド B を定義する
-
シグナルとメソッド A を定義したノードを取得する
-
シグナルとメソッド A を接続する
-
メソッド B を呼び出すことでシグナルを送信する
『 1. health_depleted
を使う、キャラクターノードとゲームノード2つの関係』では、シグナルを送受信するためには、emit()
関数を呼び出していないため、emit()
関数を呼び出す1ステップ(リストの 3 )が足りないと思っていました。
しかし、『 1. health_depleted
を使う、キャラクターノードとゲームノード2つの関係』の2つのスクリプトでは、シグナルを送受信するまでに、2ステップ(リストの 3 と 6 )の工程が足りていないことを、今回知ることができました。
3. 他のメソッドをシグナルに接続する
take_damage()
関数を呼び出したタイミングで実行したい関数があれば、その実行したい関数を health_changed
シグナルに connect()
関数で接続することで、_on_Character_health_changed()
関数と同じように実行することができるでしょう。
例えば、
NODE | GDScript | SIGNAL | METHOD |
---|---|---|---|
Character | character.gd | health_changed | take_damage() |
Game | game.gd | - | _read() |
Lifebar | lifebar.gd | - | _on_Character_health_changed() |
Another_Nodo | another_node.gd | - | _anoter_method() |
表のように、ゲームノードと同じ階層に Another_Group ノードを設けて、その子ノードに Another_Nodo ノードも設けます。
Another_Nodo ノードをゲームノードで取得して、health_changed
シグナルの connect()
関数で _anoter_method()
関数を接続します。以下が game.gd
に追加するスクリプトです。
# ゲームノードのスクリプト
func _ready():
...
var another_node = get_node('Another_Group/Another_Nodo')
character_node.health_changed.connect(another_node._anoter_method)
...
# Another_Nodo ノードのスクリプト
func _anoter_method():
...
pass
...
【感想】
わかりずらいと思っていた、理解の浅さを少しは埋めることができたように思います。
次回も引き続きシグナルを勉強しようと思っています。
ここまで、読んでいただきありがとうございます。船かっこいいですよね。
-
訂正と追記があります。『 2-2. 【感想】』をご覧ください。 ↩︎
Discussion