💃

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

2024/02/26に公開



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

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


第2回です。ご覧いただきありがとうございます。




上記のリンクのシグナルの項目の前回である第1回の続きから引用します。
以下の通りです。

signalを定義後の括弧内に追加の引数名を書くことが出来ます:




第2回は、カスタムシグナルの引数の活用がテーマになっているようです。




もう少しシグナルの項目を読んでみます。
今回は以下の状況から、カスタムシグナルで引数を活用する方法を理解していこうという感じです。

  • キャラクターが受けたダメージをメッセージとして表示する








まず、

カスタムシグナルに引数を指定します。


以下のコードが、カスタムシグナルに引数を指定するコードのようです。


signal this_is_my_signal_name( parameter_one, parameter_two )
SIGNAL_NAME PARAMETER
this_is_my_signal_name 1.parameter_one
2.parameter_two




それでは、カスタムシグナルに引数を指定する方法がわかったのところで、キャラクターが受けたダメージをメッセージとして表示する方法を見ていこうと思います。








キャラクターが受けたダメージをメッセージとして表示する


シグナルの項目を一通り読んでみます。


以下のことを解説されているようです。

  • 引数を指定してカスタムシグナルを定義する
  • connect 関数で引数を指定したシグナルと関数を接続する
  • 接続した関数に追加で引数を渡す








まず、スクリプトを見ていきましょう。


それでは最初に、スクリプトを見ていきましょう。




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

signal health_changed( old_value, new_value )
game.gd
# ゲームノードのスクリプト

func _ready():
    var character_node = get_node('Character')
    var battle_log_node = get_node('UserInterface/BattleLog')

    character_node.health_changed.connect(
        battle_log_node._on_Character_health_changed,
        [character_node.name])
battle_log.gd
# バトルログノードのスクリプト

func _on_Character_health_changed(old_value, new_value, character_name):
    if not new_value <= old_value:
        return

    var damage = old_value - new_value
    label.text += character_naem + " took " + str(damage) + " damage."




しかし、前述した通りこのスクリプトは実行時にエラーが発生してしまいました。





【問題発生】


エラー箇所は以下です。

game.gd
# ready()
character_node.health_changed.connect(
    battle_log_node._on_Character_health_changed,
    [character_node.name])


以下がスタックトレースに表示されるエラーです。

Invalid type in function 'connect' in base 'Signal'. Cannot convert argument 2 from Array to int.

DeepL で翻訳します。

ベース 'Signal' の関数 'connect' の型が無効です。引数 2 を Array から int に変換できません。




エラーからわかることとして、 [character_node.name] の部分は Array 型であり、connect 関数のシグネチャが正しくないということでしょう。






それでは、問題の関連情報を整理してみましょう。

【整理】


  1. connect 関数のシグネチャを見てみる
  2. bind 関数のシグネチャを見てみる



🕺 connect 関数のシグネチャを見てみる


まず、connect 関数のシグネチャを以下に示します。

int connect ( Callable callable, int flags=0 )
RETURN_VALUE METHOD_NAME PARAMETERS
int connect Callable
int


このシグネチャからわかることを、以下のリストにまとめてみます。

  1. 関数名は、connect
  2. 戻り地は int
  3. 引数は2つ
    • 1つ目が Callable 型で、シグナルに接続する関数を渡す引数
    • 2つ目が int 型で、ConnectFlags 列挙型から選択する引数
      接続の動作を設定することができます


ということは、こちらの解説にある通りに、追加の引数を渡す機能は connect 関数にはなさそうです。




では、追加で引数を渡す方法はないのでしょうか。

そこで、connect 関数の説明をもう一度確認してみましょう。
以下に引用します。

You can provide additional arguments to the connected by using Callable.bind. callable flags callable

DeepL で翻訳します。

Callable.bindを使えば、connectedに追加の引数を与えることができる。callable flags callable




💃 bind 関数のシグネチャを見てみる


「Callable.bindを使えば、...」 ということで、Callablebind 関数を呼び出すと、エラーを発生させることなく達成できそうです。


では、ここで bind 関数のシグネチャを見てみましょう。

Callabel bind ( ... ) vararg const
RETURN_VALUE METHOD_NAME PARAMETERS
Callabel bind ...


このシグネチャからわかることを、以下のリストにまとめてみます。

  1. 関数名は、bind
  2. 戻り地は Callabel
  3. 引数は vararg で、可変長。
  4. const なので、関数内の変数を変更しない。

でしょうか。

bind 関数の説明を全部引用します。

Returns a copy of this Callable with one or more arguments bound. When called, the bound arguments are passed after the arguments supplied by call. See also unbind.

Note: When this method is chained with other similar methods, the order in which the argument list is modified is read from right to left.

DeepL で翻訳します。

1つ以上の引数をバインドした Callable のコピーを返す。呼び出されると、バインドされた引数は call で指定された引数の後に渡されます。unbind も参照してください。

注意: このメソッドが他の同様のメソッドと連結されている場合、引数リストが変更される順序は、右から左に読み込まれます。


bindv() 関数もあるんですね。こちらは引数を Array 型で指定するようです。
unbind() 関数ってどんなときに使うんですかね。




「1つ以上の引数をバインドした Callable のコピーを返す。」 ということから、やはり、bind 関数を使うことで引数を追加で渡すことができるようです。




【解決】


connect 関数と bind 関数を使い、game.gd スクリプトを書き換えてみましょう。

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

func _ready():
    var character_node = get_node('Character')
    var battle_log_node = get_node('UserInterface/BattleLog')

    character_node.health_changed.connect(
        battle_log_node._on_Character_health_changed.bind(
            character_node.name))


Callable.bind(character_node.name) とすることで、シグナルに接続した関数に追加で引数を渡すことができます。




【ちょっと感想】


エラーが発生して、びっくりしましたね。

bind() 関数をコールバック関数から呼び出せば、追加したい引数をバインドできるという簡単なもので解決できて良かったです。

bindv() 関数では、引数を Array 型で [] を使って、バインドすることができるみたいです。










もう一度、スクリプトを見ていきましょう。


書き換えたスクリプトを見ていきましょう。

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

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

signal health_changed( old_value, new_value )

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

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

func _ready():
    var character_node = get_node('Character')
    var battle_log_node = get_node('UserInterface/BattleLog')

    character_node.health_changed.connect(
        battle_log_node._on_Character_health_changed.bind(
            character_node.name))

そして以下は、バトルログノードのスクリプトです。

battle_log.gd
# バトルログノードのスクリプト

func _on_Character_health_changed(old_value, new_value, character_name):
    if not new_value <= old_value:
        return

    var damage = old_value - new_value
    label.text += character_name + " took " + str(damage) + " damage."




この2つのスクリプトからわかることを、以下のリストにまとめてみます。

  1. キャラクターノードcharacter.gd がアタッチされていること
  2. ゲームノードgame.gd がアタッチされていること
  3. バトルログノードbattle_log.gd がアタッチされていること
  4. キャラクターノードhealth_changed カスタムシグナルを定義していること
  5. health_changed カスタムシグナルに、old_valuenew_value の2つの引数が指定されていること
  6. ゲームノードキャラクターノードバトルログノードを取得していること
  7. ゲームノードキャラクターノードUserInterface ノードは同じ階層にあること
  8. バトルログノードUserInterface ノードの子ノードであること
  9. ゲームノード_read() 関数で、バトルログノード_on_Characterhealth_changed() 関数を、health_changed シグナルに接続していること( connect() 関数)
  10. 項目 9 で接続している際、connect() 関数の引数として ( 接続する関数名.bind( 追加の引数 )) を使って引数を追加で渡していること( bind() 関数)
  11. バトルログノード_on_Character_health_changed() 関数を定義していること
  12. _on_Character_health_changed() 関数は、old_valuenew_valuecharacter_name の3つの引数を受け取って処理をする関数ということ
  13. また、_on_Character_health_changed() 関数は、受け取った old_valuenew_value の値を比較して、条件を満たした場合、character_name と一緒にラベルノードにテキストとして表示する関数ということ


NODE GDScript SIGNAL(
PARAMETER )
METHOD(
PARAMETER )
Character character.gd health_changed(
old_value,
new_value )
-
Game game.gd - _read()
BattleLog battle_log.gd - _on_Character_health_changed(
old_value
new_value
character_name)




今回も、emit() 関数を使っていないため、シグナルを送信していないようです。




emit() 関数を呼び出す時に、emit() 関数の引数に値を渡すと、その値がシグナルと一緒に送信されるようです。

今回の場合のコードは以下のようになります。

health_changed.emit(value_one, value_two)








それでは、解説をもう少し読んでみます。


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

GDScriptは、シグナルとメソッド間の接続に値の配列をバインドすることができます。 シグナルが発信された時、コールバックメソッドがバインドされた値を受け取ります。 これらのバインドされた引数は、それぞれの接続に固有のもので、同じ値が維持されます。

もし、発信されたシグナル自身が必要とするデータへのアクセスをあなたに与えなかったとしても、決まった情報を接続に追加するために値の配列を使うことが出来ます。


今回は長めですね。




こちらも、リストにまとめてみましょう。

  1. コールバックメソッドとは、シグナルに接続した関数のこと
  2. 値の配列をバインドするとは、bind() 関数でシグナルと一緒に送信する引数を渡せるということ
  3. シグナルが発信された時、シグナルに接続した関数が bind() 関数の引数を受け取る受け取ること
  4. バインドされた引数は、それぞれの接続に固有のもので、同じ値が維持されること
    別途の connect() 関数でバインドされた引数は別のものであるということ




主にまとめたリストの項目 4, 5 と 9 から 12 と重なるかと思います。








ここで、疑問に思うことがあります。

引数は emit() 関数でも、connect() 関数の bind() 関数でも送信することができました。




それでは、

引数を送受信する順番はどうなっているのでしょうか。


順番によっては、引数を受け取る関数である connect() 関数で接続する関数のシグネチャと異なる順番で引数を受け取ってしまうこともありそうです。

気をつけなければ、バグが発生してしまうのではないでしょうか。






検証してみましょう。
少し改変したスクリプトが以下です。

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

signal my_signal( one, two )
game.gd
# ゲームノードのスクリプト

func _ready():
	var character_node = $Character
	var battle_log_node = $UserInterface/BattleLog
	
	character_node.health_changed.connect(
		battle_log_node._on_Character_health_changed.bind(
    		'bind_0', 'bind_1', 'bind_2'))
	
	character_node.health_changed.emit('emit_0', 'emit_1', 'emit_2')
battle_log.gd
# バトルログノードのスクリプト

func _on_Character_health_changed(one, two, three, four, five, six):
	self.text = "1: " + one + "\n2: " + two + "\n3: " + three + "\n4: " + four + "\n5: " + five + "\n6: " + six


出力結果は、以下のようになります。

1: emit_0
2: emit_1
3: emit_2
4: bind_0
5: bind_1
6: bind_2


今回の結果で、引数の渡される順番は、emit() 関数の引数 -> bind() 関数の引数の順だとわかりました。


こうみると、シグナルの引数は自由度が高いですね。




ここでわかることとしては、

  • emit() 関数で引数を指定することで、そのシグナル共通の引数を送信して、
  • bind() 関数で引数を指定することで、その connect() 関数固有の引数を送信する。

という操作が可能になることでしょう。








【感想】


常設のシグナルでも追加の引数を接続した関数に渡すこともできるのでしょうか?
常設のシグナルとカスタムシグナルを組み合わせたら、できそうですね。




カスタムシグナルの使い方の幅広さがよくわかる勉強会だったと思います。







ここまで読んでいただき、本当にありがとうございます。だいぶ長文になってしまいました。

読んでいただいた皆様に、こちら、かっこいい船です。

Discussion