【Godor4.2】第2回、Signalを勉強する会。おもにカスタムシグナルについて【終了】
カスタムシグナルってわかりずらいですよね
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
関数で引数を指定したシグナルと関数を接続する - 接続した関数に追加で引数を渡す
まず、スクリプトを見ていきましょう。
それでは最初に、スクリプトを見ていきましょう。
# キャラクターノードのスクリプト
signal health_changed( old_value, new_value )
# ゲームノードのスクリプト
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])
# バトルログノードのスクリプト
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."
しかし、前述した通りこのスクリプトは実行時にエラーが発生してしまいました。
【問題発生】
エラー箇所は以下です。
# 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
関数のシグネチャが正しくないということでしょう。
それでは、問題の関連情報を整理してみましょう。
【整理】
connect
関数のシグネチャを見てみる
🕺 まず、connect
関数のシグネチャを以下に示します。
int connect ( Callable callable, int flags=0 )
RETURN_VALUE | METHOD_NAME | PARAMETERS |
---|---|---|
int | connect | Callable int |
このシグネチャからわかることを、以下のリストにまとめてみます。
- 関数名は、
connect
- 戻り地は
int
型 - 引数は2つ
- 1つ目が
Callable
型で、シグナルに接続する関数を渡す引数 -
2つ目が
int
型で、ConnectFlags
列挙型から選択する引数
接続の動作を設定することができます
- 1つ目が
ということは、こちらの解説にある通りに、追加の引数を渡す機能は 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を使えば、...」 ということで、Callable
の bind
関数を呼び出すと、エラーを発生させることなく達成できそうです。
では、ここで bind
関数のシグネチャを見てみましょう。
Callabel bind ( ... ) vararg const
RETURN_VALUE | METHOD_NAME | PARAMETERS |
---|---|---|
Callabel | bind | ... |
このシグネチャからわかることを、以下のリストにまとめてみます。
- 関数名は、
bind
- 戻り地は
Callabel
型 - 引数は
vararg
で、可変長。 -
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
スクリプトを書き換えてみましょう。
# ゲームノードのスクリプト
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
型で []
を使って、バインドすることができるみたいです。
もう一度、スクリプトを見ていきましょう。
書き換えたスクリプトを見ていきましょう。
以下は、キャラクターノードのスクリプトです。
# キャラクターノードのスクリプト
signal health_changed( old_value, new_value )
そして以下は、ゲームノードのスクリプトです。
# ゲームノードのスクリプト
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))
そして以下は、バトルログノードのスクリプトです。
# バトルログノードのスクリプト
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つのスクリプトからわかることを、以下のリストにまとめてみます。
-
キャラクターノードに
character.gd
がアタッチされていること -
ゲームノードに
game.gd
がアタッチされていること -
バトルログノードに
battle_log.gd
がアタッチされていること -
キャラクターノードが
health_changed
カスタムシグナルを定義していること -
health_changed
カスタムシグナルに、old_value
とnew_value
の2つの引数が指定されていること - ゲームノードがキャラクターノードとバトルログノードを取得していること
- ゲームノード、キャラクターノード、UserInterface ノードは同じ階層にあること
- バトルログノードは UserInterface ノードの子ノードであること
-
ゲームノードの
_read()
関数で、バトルログノードの_on_Characterhealth_changed()
関数を、health_changed
シグナルに接続していること(connect()
関数) -
項目 9 で接続している際、
connect()
関数の引数として( 接続する関数名.bind( 追加の引数 ))
を使って引数を追加で渡していること(bind()
関数) -
バトルログノードが
_on_Character_health_changed()
関数を定義していること -
_on_Character_health_changed()
関数は、old_value
とnew_value
とcharacter_name
の3つの引数を受け取って処理をする関数ということ - また、
_on_Character_health_changed()
関数は、受け取ったold_value
とnew_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は、シグナルとメソッド間の接続に値の配列をバインドすることができます。 シグナルが発信された時、コールバックメソッドがバインドされた値を受け取ります。 これらのバインドされた引数は、それぞれの接続に固有のもので、同じ値が維持されます。
もし、発信されたシグナル自身が必要とするデータへのアクセスをあなたに与えなかったとしても、決まった情報を接続に追加するために値の配列を使うことが出来ます。
今回は長めですね。
こちらも、リストにまとめてみましょう。
- コールバックメソッドとは、シグナルに接続した関数のこと
- 値の配列をバインドするとは、
bind()
関数でシグナルと一緒に送信する引数を渡せるということ - シグナルが発信された時、シグナルに接続した関数が
bind()
関数の引数を受け取る受け取ること - バインドされた引数は、それぞれの接続に固有のもので、同じ値が維持されること
別途のconnect()
関数でバインドされた引数は別のものであるということ
主にまとめたリストの項目 4, 5 と 9 から 12 と重なるかと思います。
ここで、疑問に思うことがあります。
引数は emit()
関数でも、connect()
関数の bind()
関数でも送信することができました。
それでは、
引数を送受信する順番はどうなっているのでしょうか。
順番によっては、引数を受け取る関数である connect()
関数で接続する関数のシグネチャと異なる順番で引数を受け取ってしまうこともありそうです。
気をつけなければ、バグが発生してしまうのではないでしょうか。
検証してみましょう。
少し改変したスクリプトが以下です。
# キャラクターノードのスクリプト
signal my_signal( one, two )
# ゲームノードのスクリプト
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')
# バトルログノードのスクリプト
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