🚥

Godot4のawaitを使って複数のシグナルを待つ

2023/02/23に公開

はじめに

godot engine4でawaitが追加され、シグナル待機の実装がわかりやすくなりました。ターン制RPGでよくあるコマンド入力待機なんかも、シンプルなコードで記述することができます。

例えば、使用するスキルを選択したらシグナルcommand_selectedがemitされるとして、コマンド待機は以下のようなコードになります。

signal command_selected(skill) #スキルを選んだときのシグナルがあるとする

var skill = await command_selected

これだけでも非常に便利ですが、この記事ではawaitをさらに便利に使うために複数のシグナルを待つクラスの実装を紹介します。

これが何の役に立つのか?ゲームでは何か入力中にキャンセルしたいときがよくあります。例えば、RPGのターン制バトルではコマンド入力待機中にキャンセルボタンを押すことで、一人前の仲間のコマンド入力をやり直す機能があった方が快適です。この場合はコマンド入力とキャンセルのいずれかが実行されるのを待って、処理を分岐させる必要があるわけですが、こういうときに複数のシグナルを待てるとうれしいわけです。

複数のシグナルを待つクラスの実装

以下が、複数のシグナルを待つクラスの実装です。

class_name Promise

signal _any_signal_emitted(signal_, result)

func wait_any_signals(signals: Array):
    var callbacks = {}
    for signal_ in signals:
        var signal_info = signal_.get_object().get_signal_list().filter(func(info):
            return info.name == signal_.get_name()
        )
        assert(signal_info.size() == 1)
        var argc = signal_info[0]["args"].size()
        var callback: Callable
        match argc:
            0: callback = _callback_0.bind(signal_)
            1: callback = _callback_1.bind(signal_)
            2: callback = _callback_2.bind(signal_)
            3: callback = _callback_3.bind(signal_)
        
        callbacks[signal_] = callback
        signal_.connect(callback)
        
    var ret = await _any_signal_emitted

    for signal_ in callbacks:
        signal_.disconnect(callbacks[signal_])
            
    return ret

func _callback_0(signal_) -> void:
    _any_signal_emitted.emit(signal_)

func _callback_1(arg1, signal_) -> void:
    _any_signal_emitted.emit(signal_, arg1)

func _callback_2(arg1, arg2, signal_) -> void:
    _any_signal_emitted.emit(signal_, [arg1, arg2])

func _callback_3(arg1, arg2, arg3, signal_) -> void:
    _any_signal_emitted.emit(signal_, [arg1, arg2, arg3])

wait_any_signalsが処理本体で、渡したシグナル配列のいずれかがemitされたらその情報を返します。渡したシグナルそれぞれにconnectしたコールバック関数で新たなシグナル_any_signal_emittedをemitをしています。これは内部だけで使用するシグナルで、emitされたシグナルとその引数情報をemitしています。

そして、var ret = await _any_signal_emittedとすることで、いずれかのシグナルがemitされるまで待機しています。

シグナル毎に引数の数が違うので、引数の数毎にコールバック関数を用意しています(_callback_0、_callback_1...)。もっと良い方法があるかもしれませんが。とりあえず引数3つ分まで用意しました。

クラスを使う側の実装

使う側は以下のようになります。command_selectedcommand_canceledがコマンド入力、キャンセル時のシグナルです。

signal command_selected(skill) #スキルを選んだときのシグナル
signal command_canceled() # キャンセルしたときのシグナル

var promise = Promise.new()
var result = await promise.wait_any_signals([command_selected, command_canceled])

match result:
    [command_selected, var skill]:
        # コマンド入力処理
    [command_canceled]:
        # コマンドキャンセル処理

Discussion