Input Systemのコールバックをasync/awaitで待機させたいの…
UniTaskのUniTaskCompletionSourceを使えばこの辺がスマートに実装できるわ。
Input SystemのActionからの入力は、コールバックとして受け取ることが出来ます。
コールバック経由で入力値を受け取る場合、例えばボタンが指定順序通りに押されたか判定するケースでは、ステートマシンの実装やコールバックのネストを行う必要が出てくるかもしれません。
このような問題は、C#のasync/await構文で非同期待機する処理に書き換えれば解決できます。
実現方法は一通りではありませんが、UniTaskのUniTaskCompletionSourceクラスを使うとこの辺が楽に実装できます。
参考:UniTaskCompletionSource Class| UniTask
本記事では、次のコードのようにInput Actionの入力をawaitで非同期待機できるようにする方法を解説します。
また、以下要件を満たすawaitを実現するものとします。
- started、performed、canceledコールバックに対してawait可能にする
- CancellationTokenによるキャンセルにも対応させる
- InputActionに対して拡張メソッドとして呼び出す形にする
- 入力値の受取りにも対応する
- Unity 2022.3.0f1
- Input System 1.5.1
- UniTask 2.3.3
目次 非表示
前提条件
事前にInput Systemパッケージがインストールされ、有効化されているものとします。
ここまでのセットアップ手順については以下記事をご覧ください。
また、本記事ではUniTaskを用いてawaitによる入力待ちを実現するものとします。記事を読み進めるにあたっては、C#のasync/await構文およびUniTaskの基本的な使い方を理解していることが前提となります。
UniTaskはUPM経由でインストール可能です。インストール未実施の場合は以下手順で実施してください。
- トップメニューのWindow > Package Managerを選択し、Package Managerウィンドウを開く
- 左上の+アイコン > Add package from git URL…を選択
- URLの入力欄に以下を入力し、右側の「Add」ボタンをクリック
本記事では、Input Actionに存在する3種類のコールバックstarted、performed、canceledに対してawaitで待機させるところを目指して解説を進めます。
それぞれのコールバックの意味は、以下記事で解説しています。
コールバックをawaitに置き換える仕組み
Actionからコールバック経由で入力を受け取る場合、次のようなコードになるでしょう。
上記のコードを例えば、何かボタンが押されたら処理を実行させるフローにしたい場合を考えます(キャンセル処理については後述します)
このような仕組みは、UniTaskのUniTaskCompletionSourceクラスを使うとスムーズに実現できます。
参考:UniTaskCompletionSource Class| UniTask
UniTaskCompletionSourceクラスは、次のようにnewでインスタンス化して使います。
呼び元からは、次のようにTaskプロパティに対してawaitする形で入力を受け取るまで待機します。
performedコールバックなどの処理では、次のようにTrySetResultメソッドを実行して結果を確定させます。
これにより、performedコールバックが実行されたら入力待機側のawait待機が終了し、以降の処理に進むことが出来ます。
例えば拡張メソッドとして実装したい場合は、次のようなコードになるでしょう。
.NETには、同様の役割を持つTaskCompletionSourceクラスが存在します。メソッドやプロパティなど、使い方は基本的に一緒です。
結果のキャンセル
前述のコードでは、結果が確定するまで永遠にawaitされます。
例えば、キャンセルボタンやその他何らかの要因でawaitを中断したい場合、CancellationToken構造体を使って拡張メソッド側を対応させる必要があります。
呼び元では、次のようにCancellationTokenSourceインスタンスを生成し、拡張メソッド側にTokenプロパティ(CancellationToken構造体)を渡します。
これにより、呼び元からは好きなタイミングでawaitの処理を中断できるようになります。
拡張メソッド側では、次のコードでCancellationTokenからキャンセル要求が来た時に、UniTaskCompletionSourceに対して結果のキャンセルを行います。
キャンセル要求が来た時の処理は、CancellationToken.Registerメソッドで登録できます。
performedコールバックをawaitする最低限のコード
以下、InputActionのperformedコールバックをawaitで待機する拡張メソッドです。CancellationTokenによるキャンセル処理にも対応しています。
上記をInputActionAsyncExtensions.csという名前でUnityプロジェクトに保存しておきます。
実際の使用例は以下のようになります。
決定ボタンが押されたらログ出力します。決定前にキャンセルボタンが押されたら、決定ボタンの入力待機を中断し、キャンセルの旨のログを出力します。
上記をUseExample1.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターより決定ボタン、キャンセルボタンそれぞれのActionを設定してください。
例では決定ボタンにキーボードのスペースキー、キャンセルボタンにエスケープキーを割り当てるものとします。
実行結果
決定ボタンを押すと、決定された旨がログ出力されます。
決定後にキャンセルボタンを押しても何も起こりません。
逆にキャンセルボタンを先に押すと、キャンセルされた旨がログ出力されます。
この時、決定ボタンを押しても何も起こりません。
スクリプトの説明
決定とキャンセルのActionは、InputActionProperty構造体としてインスペクターから編集可能にしています。
使用側では、まずキャンセル処理のための準備を行います。
CancellationTokenSourceの作成、およびキャンセルActionのコールバックが発火したときに、CancellationTokenSourceからキャンセル要求を出すようにします。
キャンセルを考慮した決定ボタン入力の待機は、try-catch-finallyブロックで実現しています。
上記のコードが成り立つ理由は、キャンセル要求が来た時にOperationCanceledException例外がスローされるためです。
CancellationTokenSourceは最終的にDisposeメソッドで破棄する必要があります。また、キャンセルActionに登録したコールバックも解除したいので、finallyブロックでこれらの処理を行うようにしています。
started、performed、canceledコールバックをawaitする
同様の流れでperformed以外のstarted、canceledコールバックに対しても同様のawaitが実現できます。
拡張クラスの完成形
まず、本記事の要件を満たす完成形の拡張クラスを示します。
started、performed、canceledコールバックのほか、入力値の受取りにも対応しています。
上記をInputActionAsyncExtensions.csという名前でUnityプロジェクトに保存します。既に前述の例のInputActionAsyncExtensions.csが存在する場合は、上書きしてください。
使用例のスクリプト
上記拡張メソッドの使用例です。
上記をUseExample2.csという名前でUnityプロジェクトに保存し、ゲームオブジェクトにアタッチし、インスペクターより各種Actionを設定してください。
実行結果
started、performed、canceledコールバックが順番に呼ばれ、受け取った入力値がログ出力されていることが確認できます。
スクリプトの説明
使用例で異なる部分は以下コードです。
started、performed、canceledコールバックの順に待機し、その入力値を受け取っています。
拡張クラス側では、次のように入力値を返す版と返さない版の拡張メソッドをそれぞれ実装しています。
3種類のコールバックに対する処理は共通化できるため、OnCallbackAsyncメソッドにまとめています。
performedプロパティなどに対してコールバックを登録・解除する処理ですが、このプロパティはgetをサポートしていないため、次のようにするとコンパイルエラーになってしまいます。
そのため、遠回りに感じるかもしれませんが、それぞれ登録と解除のメソッドを共通メソッドに渡す必要があります。
参考:Class InputAction| Input System | 1.5.1
共通メソッドは、以下のようにコールバック登録と解除をそれぞれonAdd、onRemoveのdelegate呼び出しという形で実装しています。
それ以外の処理は、前述の拡張メソッドの例と一緒です。
さいごに
Input SystemのActionから受け取るコールバックは、UniTaskCompletionSourceを使うと比較的スムーズに実装できます。
拡張クラスや拡張メソッドを自分で実装しないといけない点は面倒に感じるかもしれませんが、一度実装してしまえば使いまわしが効いて便利になるでしょう。
CancellationTokenによる入力待機の中断も実現できることを示しました。キャンセルボタンによる中断のほか、画面遷移など何らかの要因で処理を中止せざるを得ない場面でも活用できるでしょう。