🕺

【Godor4.2】第1回、Signalを勉強する会。おもにカスタムシグナルについて

2024/02/12に公開


カスタムシグナルってわかりずらいですよね

GDScript reference — Godot Engine (4.x)の日本語のドキュメントを読んでいろいろ勉強してみようと思いました。


シグナルはオブジェクトから通知メッセージを送信する手段で、他のオブジェクトはそれを受信することが出来ます。

ということらしいです。



カスタムシグナルを定義するにはsignal キーワードを使用します。

signal this_is_my_signal_name




上記のリンクのシグナルの項目では、以下の関係から理解していこうという形になっているようです。

  1. signal health_depleted を使う、キャラクターノードゲームノードの2つの関係
  2. signal health_changed を使う、キャラクターノードゲームノードライフバーノードの3つの関係




まず、

1. health_depleted を使う、キャラクターノードとゲームノード2つの関係

1-1. 【解説を読む】




最初に、シグナルの項目のコードを見てみます。


以下は、キャラクターノードのスクリプトです。

Character.gd
# キャラクターノードのスクリプト

signal health_depleted

そして以下は、ゲームノードのスクリプトです。

game.gd
# ゲームノードのスクリプト

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つのスクリプトからわかることを、以下のリストにまとめてみます。

  1. キャラクターノードcharacter.gd がアタッチされていること

  2. ゲームノードgame.gd がアタッチされていること

  3. キャラクターノードhealth_depleted カスタムシグナルを定義していること

  4. ゲームノードが、キャラクターノードを取得していること

  5. ゲームノードキャラクターノードは同じ階層にあること

  6. ゲームノード_read() 関数で、 _on_character_health_depleted() 関数を、キャラクターノードhealth_depleted シグナルに接続していること( connect() 関数)

  7. ゲームノード_on_character_health_depleted() 関数を定義していること

  8. _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. 【感想】


ここまでシグナルの項目を読んでみて、わかりずらいなと思っていた理由がなんとなくわかったような気がします。
【ノードがシグナルを送信すると、そのシグナルに接続してある関数が呼び出される】 と説明してもらったとしても、その説明からスクリプトのコードを想像できないという状態だったようです。










2. signal health_changed を使う、キャラクターノードゲームノードライフバーノードの3つの関係

2-1. 【解説を読む】




最初にシグナルの項目から解説の一部を引用します。

ここに有用な例を示します。アニメーションを伴った、体力の変化を受信するライフバーをスクリーン上に配置しようとしますが、シーンツリー上にあるプレイヤーからはそのユーザーインターフェースを分けたいします。

これは、プレイヤーのライフバーを設けたいが、ライフバーはユーザーインターフェースとしてプレイヤーとはグループを別にしたい。それぞれのノードのグループを分けたいということだと思います。




次に、シグナルの項目のコードを見てみます。


以下は、キャラクターノードのスクリプトです。プレイヤーにアタッチするスクリプトのようです。

Character.gd
# キャラクターノードのスクリプト

...
signal health_changed

func take_damage(amount):
    var old_health = health
    health -= amount

    health_changed.emit(old_health, health)

...

そして以下は、ライフバーノードのスクリプトです。

lifebar.gd
# ライフバーノードのスクリプト

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

...

そして以下は、ゲームノードのスクリプトです。

game.gd
# ゲームノードのスクリプト

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つのスクリプトからわかることを、以下のリストにまとめてみます。

  1. キャラクターノードcharacter.gd がアタッチされていること

  2. ゲームノードgame.gd がアタッチされていること

  3. ライフバーノードlifebar.gd がアタッチされていること

  4. キャラクターノードhealth_changed カスタムシグナルを定義していること

  5. キャラクターノードtake_damage() 関数を定義していること

  6. take_damage() 関数が health_chenged シグナルを発信すること( emit() 関数)

  7. ライフバーノード_on_Character_health_changed() 関数を定義していること

  8. _on_Character_health_changed() 関数は、ライフバーの色を変えたり、アニメーションをさせること

  9. ゲームノードが、キャラクターノードライフバーノードを取得していること

  10. ゲームノードキャラクターノードUserInterface ノードは同じ階層にあること

  11. ライフバーノードUserInterface ノードの子ノードであること

  12. ゲームノード_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. 【感想】


以上のことをまとめると、シグナルを送受信する主な工程は、以下のようになるのではないでしょうか。

  1. シグナルを定義する

  2. シグナルに接続するメソッド A を定義する

  3. シグナルを送信するメソッド B を定義する

  4. シグナルとメソッド A を定義したノードを取得する

  5. シグナルとメソッド A を接続する

  6. メソッド 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 に追加するスクリプトです。

game.gd
# ゲームノードのスクリプト

func _ready():
    ...
    var another_node = get_node('Another_Group/Another_Nodo')

    character_node.health_changed.connect(another_node._anoter_method)

...
another_node.gd
# Another_Nodo ノードのスクリプト

func _anoter_method():
    
    ...
    pass
...










【感想】


わかりずらいと思っていた、理解の浅さを少しは埋めることができたように思います。


次回も引き続きシグナルを勉強しようと思っています。




ここまで、読んでいただきありがとうございます。船かっこいいですよね。

脚注
  1. 訂正と追記があります。『 2-2. 【感想】』をご覧ください。 ↩︎

Discussion