Unityの(New)InputSystemでBindingを好きにカスタマイズできるCompositeBindingsの紹介
はじめに
こんにちは、エンジニアリングマネージャーの渡辺(@mochi_neko_7)です。
少し前まではマネジメントや採用を中心にやっていましたが、最近はUnityの開発をメインにやるようになりました。
今開発しているプロジェクトでは、Unityの新しいInputSystemである(New)InputSystemを導入しています。
こちらは複数デバイスでの入力の取り回しにとても便利なシステムで、特にActionという抽象化がとても扱いやすいです。
ただLowLevelなところは以前からあるInputSystemとそこまで変わらないですし、欲しい入力の形が思い通りに用意されているとは限らなかったりしますので、ちょっと凝った入力(例えばTouchscreenでのスマホ特有の操作など)を扱おうとすると難しい場面があります。
今回はそんな時に便利な、InputSystemでの入力の取得方法をある程度カスタマイズできる仕組みであるCompositeBindingsについて紹介します。
(New)InputSystemの設計のおさらい
(New)InputSystemの公式のドキュメントはこちらです。 2022/2/18現在の最新版はver1.3です。
インストール方法などは公式ドキュメントを参照してください。
また、本記事以外にも解説している記事や動画がありますので、まだあまり慣れていない方はこれらを見ていただくと良いかと思います。
ここでは(New)InputSystemの全体的な設計だけおさらいしましょう。
細かい話は公式のドキュメント
にもありますが、今回は InputActionAsset
を触る視点での構造に注目します。
形としてはシンプルなヒエラルキー構造で、上から辿ると、
- Action Asset
- Action Maps
- Actions
- Bindings
- (DeviceInputs)
- Bindings
- Actions
- Action Maps
のような順に並びます。
ここで出てくる用語は公式のまとめ
が詳しいですが、ポイントは
- Actions:アプリケーション内で実現したい動作
- Bindings:Actionとデバイスの入力を紐づけるもの
- DeviceInputs:デバイスの入力
のようにActionという概念を抽象化して、Bindingのレイヤーを挟むことによって入力デバイスの変更に強い設計になっていることです。
今回はあまり説明はしませんが、デバイスの変更の仕組みはControlSchemesを参照してください。
CompositeBindingsとは
Bindingsの中にCompositeBindingsという項目があります。
Composite=合成のようなニュアンスの言葉です。
具体的なサンプルとして分かりやすいものは、PCゲームでよくあるWASDキーでの移動方法で、W、A、S、Dの異なる4つの入力を、1つのVector2にまとめることができます。
- Vector2
- W(Up方向)
- A(Left方向)
- S(Down方向)
- D(Right方向)
このように複数の入力を加工してActionに渡せるのがCompositeBindingsというわけです。
デフォルトでいくつかのパターンのCompositeBindingsがあります。
- Positive\Negative Bindings (1D axis)
- Up\Down\Left\Right Bindings (2D axis)
- Up\Down\Left\Right\Forward\Backward Bindings (3D axis)
- Binding With One Modifier
- Binging With Two Modifiers
お察しの方もいるとは思いますが、このCompositeの部分を自分でカスタマイズできれば、ControlSchemeの仕組みに乗りながら複数デバイスの様々な入力に対応できるかなり柔軟な入力システムを作れることになります。
そのカスタマイズをするための仕組みが、今回のメインテーマのInputBindingComposite
になります。
CompositeBindingsのカスタマイズの仕方
どうやってカスタマイズするのか?というのは公式のドキュメント
にもガイドがあるのでこれのCustomComposite
のコメントを上から読めば分かるのですが、英語ですし、今回はざっくりかいつまんで解説しましょう。
まず基本的な形式は以下のような形になります。
using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.Utilities; #if UNITY_EDITOR using UnityEditor; #endif #if UNITY_EDITOR [InitializeOnLoad] #endif [DisplayStringFormat(nameof(CustomInputBindingComposite))] internal sealed class CustomInputBindingComposite : InputBindingComposite<T> { #if UNITY_EDITOR static CustomInputBindingComposite() { Initialize(); } #endif [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Initialize() { InputSystem.RegisterBindingComposite<CustomInputBindingComposite>(); } [InputControl(layout = "Button")] public int anInputControl; public override T ReadValue(ref InputBindingCompositeContext context) { // Compute T value } public override float EvaluateMagnitude(ref InputBindingCompositeContext context) { // Compute magnitude of T value } }
1. InputBindingCompositeの継承とセットアップ
InputBindingComposite<T>
を継承したクラスを用意することから始まります。
internal sealed class CustomInputBindingComposite : InputBindingComposite<T> { ... }
CustomInputBindingComposite
はもちろん任意の名前で構いません。
<T>
には入力として扱いたい型を指定してください。
特に制約もなく、自前で定義した型も使用できます。
もちろんAccesibilityは任意で構いません。
ClassのAttributeと、Class内のConstructor、InitializeメソッドはInputSystemに乗せるためのお作法です。
#if UNITY_EDITOR [InitializeOnLoad] #endif
... #if UNITY_EDITOR static CustomInputBindingComposite() { Initialize(); } #endif [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Initialize() { InputSystem.RegisterBindingComposite<CustomInputBindingComposite>(); }
2. InputControlの追加
次にデバイスから受け取りたい入力のInputControlを追加します。
[InputControl(layout = "Button")] public int anInputControl;
Attributeのlayout = "Button"
の部分を指定することで、設定できるデバイス入力をフィルターすることができます。
また、1つだけという制約はもちろんありませんので、好きなだけ追加して定義することができます。
ちなみに型がint
なのは、ここで追加したInputControl
たちはそれぞれPartとして区別されていて、そのインデックスに対応する変数PartNumber
だからだと思います。
3. ReadValueメソッドの記述
ReadValue<T>
のメソッドで、具体的にどのような入力を流すのかを記述します。
... public override T ReadValue(ref InputBindingCompositeContext context) { // Compute T value }
先ほど定義したInputControl
の値は、以下のようにInputBindingCompositeContext.ReadValue<>()
で取得することができます。
... public override T ReadValue(ref InputBindingCompositeContext context) { var value = context.ReadValue<float>(anInputControl); ... }
4. EvaluateMagnitudeメソッドの記述
もう一つoverrideすべきメソッドが、EvaluateMagnitude
になります。
... public override float EvaluateMagnitude(ref InputBindingCompositeContext context) { ... }
この結果の値は、Interactionの管理に使用されるようなので、特にButtonなどで結果を受けたい場合に重要になります。
(overrideしないと、まったくイベントが発行されない、なんてことになってしまいます)
...
基本的な説明は以上になります。
入力の取得部分は特に他と変わりません。
あとはInputActionAssetのBindingsで自作したCompositeBindingが追加できることと、実際に思った通りに入力が取れることを確認してみてください。
CompositeBindingsの応用的な使い方
少しだけ応用に踏み込んだ話もしましょう。
InputControl
を追加する→ReadValue()
で使用する、というのが基本的な入力の取得フローですが、実はInputSystemは古いInputManagerのように直接デバイスを指定して入力を取得する低レベルAPIも用意されています。
Touchの例:
var touches = Touchscreen.current.touches; // Touchの0~9の配列を取得
これをReadValue()
の中で好きに呼び出すことができるので、かなり応用の幅が広がると思います。
Touchまわりでの具体的な使用例も少し挙げましょう。
- TapしたPositionを取りたい
- Screen上の特定領域のみでのTouch操作を取りたい
- Screen上での仮想Joystick操作を取りたい
EnhancedTouch
を利用したい- 複数のデータをまとめた自作型で入力を送りたい
- etc...
おわりに
CompositeBindingsは使いようによってはかなり便利なものなのですが、まだドキュメントが少なかったり、全体の仕組みが分からないととっつきづらいのかなと思います。
ですが適切に扱うことで、デバイスに依存する複雑な入力処理を、Actionに紐づくロジックと(InputSystemを跨ぐ形で)きれいに分離することができます。
つまりActionに紐づくロジック側では、デバイスの差異を(ほとんど)気にすることなく実装をすることができるのです。
特に対応プラットフォームが多いプロジェクトでは有用だと思いますので、これを機に触ってみていただけると良いかなと思います。
また、自分自身も手探りで触りながら理解している部分も多いため、もし間違った記述に気づいた方がいましたらコメントいただけますと幸いです。