【Unity】Input SystemのComposite Bindingで入力値を合成する

こじゃらこじゃら

方向キープラスマイナスボタンの入力を楽に管理する方法ってないの?

このはこのは

Input SystemのComposite Bindingを使えば良いわ。

Input Systemには、複数の入力(Binding)を1つの入力(Binding)に合成するComposite Bindingがあります。

これを用いると、例えばWASDキーをスティックのような2軸入力に合成できます。

Composite Bindingによる入力の合成イメージ

参考:Input Bindings | Input System | 1.6.1

他にも次のようなことが実現できます。

Composite Bindingで出来ること
  • キーボードの矢印キーやゲームパッドの十字ボタン入力を2軸入力に合成する
  • ゲームパッドのLRキーを1軸の入力値に合成する
  • ボタンの同時押しを判定する
  • 独自のカスタム合成を実装する

キャラクターを4方向のボタンで移動させる場合、Composite Bindingを使うと一つのベクトルに変換する処理をInput System側で行えるようになり、複数ボタン入力から移動方向を計算するコードを書かなくてもよくなります。

本記事では、このようなComposite Bindingの基本的な使い方について解説していきます。カスタムのComposite Bindingを実装する方法についても解説します。

動作環境
  • Unity 2023.1.1f1
  • Input System 1.6.1

スポンサーリンク

前提条件

事前にInput Systemがインストールされ、有効化されているものとします。ここまでの手順が分からない方は、以下記事をご覧の上セットアップを済ませてください。

また、Input Actionが設定されているものとします。Input Actionの使い方に関しては以下記事をご覧ください。

Composite Bindingの仕組み

Composite Bindingを用いることで、複数のBindingを合成して1つのBindingとして模倣することが出来ます。

そのため、参照元からは1つのBindingのみが存在しているように見えます。 [1]

Composite Bindingには、次のようなものが予め用意されています。

  • 1D axis – 2つのボタンを1軸の値にする
  • 2D vector – 4つのボタンを2次元ベクトルにする
  • 3D vector – 6つのボタンを3次元ベクトルにする
  • One Modifier – あるボタンが押されている間だけ、指定のBindingが入力されたことにする
  • Two Modifiers – ある2つのボタンが押されている間だけ、指定のBindingが入力されたことにする

上記だけでは物足りない場合、独自のComposite Bindingを実装して適用することも可能です。

参考:Input Bindings | Input System | 1.6.1

Composite Bindingの適用方法

ここからは、実際のActionにComposite Bindingを適用する方法を解説していきます。

例では、MoveというActionに対してWASDキーの4方向のComposite Bindingを適用するものとします。

Actionの定義方法はいくつかありますが、ここではInput Action Assetを新規作成し、PlayerというMapの下にMoveという名前のActionを作成します。

次に、連続した2軸入力として受け取るため、Action TypeValueにし、Control TypeVector 2に設定します。

メモ

Action Typeに設定するValueは、入力値が変化するたびに入力(performedコールバック)を通知する設定です。

Control Typeに設定するVector 2は、Action自体が受け付ける入力値の型を表します。2軸入力なのでVector 2を指定しています。

参考:Actions | Input System | 1.6.1

Actionを新規追加した場合は空のBindingが追加されるため、これを削除しておきます。そして、Actionの右の+アイコンからAdd Up\Down\Left\Right Compositeを選択します。

そして、Composite Binding下のそれぞれの方向のBindingを選択し、Path項目をクリックし、該当のキーを指定します。

例では、ListenボタンをクリックしてからWASDなどのキーを入力して選択しています。

ここまで設定し終えたら、忘れずにSave AssetボタンをクリックしてInput Action Assetの内容を保存しておきます。

Composite Bindingの入力を受け取るスクリプト

上記で設定した2軸のAction入力値を受け取ってログ出力する例です。

GetMoveExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class GetMoveExample : MonoBehaviour
{
    // 2軸入力を受け取る想定のAction
    [SerializeField] private InputActionReference _actionRef;

    private void Awake()
    {
        // 値が変化した瞬間をコールバックで取得する
        _actionRef.action.performed += OnMove;
        _actionRef.action.canceled += OnMove;
    }

    private void OnDestroy()
    {
        _actionRef.action.performed -= OnMove;
        _actionRef.action.canceled -= OnMove;

        _actionRef.action.Dispose();
    }

    private void OnEnable()
    {
        _actionRef.action.Enable();
    }

    private void OnDisable()
    {
        _actionRef.action.Disable();
    }

    private void OnMove(InputAction.CallbackContext context)
    {
        // 受け取った入力値をログ出力
        print(context.ReadValue<Vector2>());
    }
}

上記をGetMoveExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチします。

そして、インスペクターより前述のComposite Bindingを設定したActionを指定すると、入力値をログ出力するようになります。

実行結果

Composite Bindingで設定した4方向ボタンを入力すると、入力値が変化した瞬間にコンソールにログ出力されます。

複数方向のボタンが同時に入力された場合は、その合算したベクトル値となります。

動画では斜め方向のベクトルの長さが1に正規化されていますが、これはComposite Bindingのデフォルト設定によるものです。

スクリプトの説明

Composite Bindingより得られた入力値を取得するために、次のようにperformed、canceledコールバックに登録しています。

private void Awake()
{
    // 値が変化した瞬間をコールバックで取得する
    _actionRef.action.performed += OnMove;
    _actionRef.action.canceled += OnMove;
}

これにより、入力値が変化した瞬間にOnMoveメソッドが呼ばれるようになります。

OnMoveメソッドでは、次のように引数contextに対してReadValue<Vector2>メソッドを呼び出すことで2軸入力値を取得してログ出力しています。

private void OnMove(InputAction.CallbackContext context)
{
    // 受け取った入力値をログ出力
    print(context.ReadValue<Vector2>());
}

参考:Struct InputAction.CallbackContext | Input System | 1.6.1

各Composite Bindingの説明

ここからは、既に提供されているComposite Bindingについて一通り紹介していきます。

細かい仕様は以下リファレンスに記載されています。

参考:Input Bindings | Input System | 1.6.1

注意

本記事では、Input System 1.6.1時点でのものを紹介します。古いバージョンでは一部存在しなかったり名称が変わっているものがありますのでご注意ください。

1D axis

プラス方向とマイナス方向の2入力から1軸入力に合成するComposite Bindingです。

これは、該当ActionのControl TypeがAxisなどの1軸入力型の場合に使用可能になります。

Action右の+アイコンよりAdd Positive\Negative Bindingを選択すると追加されます。

設定項目は以下の通りです。

  • Min Value – 取り得る入力の最小値
  • Max Value – 取り得る入力の最大値
  • Which Side Winsボタンが同時に押された時に、どちら側の入力値を使うかどうか。
    • Neither0を返す。
    • Positiveプラス方向の入力値を返す。
    • Negativeマイナス方向の入力値を返す。

2D vector

上下左右の4入力を2軸入力に合成するComposite Bindingです。前述の設定例ではこれを使用していました。

Control TypeがVector 2の時に使用可能です。

追加は、Action右の+アイコンから表示されるAdd Up\Down\Left\Right Bindingから行えます。

設定項目は以下の通りです。

  • Mode – 4方向の入力値の処理方法を指定する。
    • Analog– 各軸の入力値をそのまま加減算する。
    • Ditigal Normalized入力値の大きさを1に正規化(ただし入力値が(0, 0)ならそのまま(0, 0)を返す)する。
    • Digital各軸単位で閾値Press Pointを基準に-1、0、1のどれかにスナップさせる。

3D vector

上下左右前後の6入力を3軸入力に合成するComposite Bindingです。

Control TypeがVector 3の時に使用できます。

追加は、該当Action右の+アイコンからAdd Up\Down\Left\Right\Forward\Backward Compositeを選択することで行います。

設定項目は以下の通りです。

  • Mode – 6方向の入力値の処理方法を指定する。
    • Analog – 各軸の入力値をそのまま加減算する。
    • Ditigal Normalized入力値の大きさを1に正規化(ただし入力値が(0, 0, 0)ならそのまま(0, 0, 0)を返す)する。
    • Digital各軸単位で閾値Press Pointを基準に-1、0、1のどれかにスナップさせる。

One Modifier

あるボタン(Modifier)が押されている時だけ指定されたBindingの入力を流すComposite Bindingです。複数キーの同時押しによるショートカットキー操作や、ドラッグ操作などを実現できます。

このComposite Bindingは、Action TypeやControl Typeの設定によらず使用可能です。

追加は、Add Binding With One Modifierから行います。

すると、次のようなBindingが追加されます。

それぞれ次のような意味を持ちます。

  • Modifier – 修飾子となるボタン入力。このボタン入力がある間、もう一方のBinding側の値が流れる。
  • Binding – Modifierに指定されたボタンが押されている間に受け取る入力。

Composite Bindingの設定項目では、Override Modifiers Need To Be Pressed First項目の有効・無効を指定できます。

これは、主にショートカットなどで用いられるボタン押下の順序によって押された判定にするかどうかを指定する項目です。

この項目のチェックが外れている場合(初期値)、その挙動は次のProject SettingsのInput System PackageEnabled Input Consumption項目の挙動によって決まります。

これは、ショートカットの修飾子(Ctrl、Shiftキーなど)を最初に押す必要があるかどうかの設定で、この項目にチェックが入っていると、Modifier→Bindingの順にボタンを押さないと入力が得られなくなります。

これは、例えば「Ctrl」+「C」などのショートカットで最初に「Ctrl」キーが押された時だけ反応し、逆に「C」から押された時は反応しないようにしたい場合に役立ちます。

チェックが付いていないと、どちらのキーから先に押されても反応するようになります。

参考:Class InputSettings | Input System | 1.6.1

注意

この設定はアプリケーション全体に影響を及ぼすため、取り扱いにはご注意ください。

Override Modifiers Need To Be Pressed First項目にチェックが付いている場合は、Input System PackageのEnabled Input Consumption項目の設定にかかわらず、常にどちらのキーから先に押されても反応するようになります。

参考:Class OneModifierComposite | Input System | 1.6.1

Two Modifiers

ある2つのボタン(Modifier1、Modifier2)が押されている時だけ指定されたBindingの入力を流すComposite Bindingです。3ボタンの同時押しを実現したい場合に使えます。

追加はAdd Binding With Two Modfiersから行います。

追加すると、次のように3つのBinding(Modifier1、Modifier2、Binding)が指定できます。

Composite Bindingの設定項目は、One Modifierと同様Override Modifiers Need To Be Pressed Firstのみです。

設定項目の意味と挙動もOne Modifierと一緒です。

Composite Bindingを自作する

既存のComposite Bindingでは物足りない場合、Composite Bindingを自作することも可能です。

カスタムComposite Bindingの基本形

次のようにInputBindingComposite<T>継承クラスを実装します。

public class MyCustomComposite : InputBindingComposite<float>

このクラスでは、次のようにReadValueメソッドの実装が必須です。

public override float ReadValue(ref InputBindingCompositeContext context)
{
    // TODO : Bindingの値を合成して返す
    return 0;
}

また、Interactionでのボタンの押された判定や長押し判定など、ボタンの押下状態を判定するのに入力値の大きさを用います。

これらを動作させるためには、EvaluateMagnitudeメソッドの実装も必要です。

public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
    // 入力値の大きさを返す
    return ReadValue(ref context);
}

そして、合成元となるBindingは、次のようにInputControl属性を指定したpublicなint型フィールドとして定義します。

// ボタン1
[InputControl(layout = "Button")] public int button1 = 0;

// ボタン2
[InputControl(layout = "Button")] public int button2 = 0;

参考:Class InputControlAttribute | Input System | 1.6.1

また、これだけではInput System側にカスタムComposite Bindingが登録されないため、次の処理で登録する必要があります。

    /// <summary>
    /// 初期化
    /// </summary>
#if UNITY_EDITOR
    [UnityEditor.InitializeOnLoadMethod]
#else
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
#endif
    private static void Initialize()
    {
        // 初回にCompositeBindingを登録する必要がある
        InputSystem.RegisterBindingComposite<XorComposite>(nameof(XorComposite));
    }

エディタ時と実行時で登録処理を行うInitializeメソッドの呼び出し方法を分けています。

エディタ時ではUnityエディタが読み込まれたとき実行時ではシーンの読み込み前にそれぞれInitializeメソッドを呼び、カスタムComposite Bindingの登録処理を行っています。

Composite Bindingの登録はInputSystem.RegisterBindingCompositeメソッドにて行います。

public static void RegisterBindingComposite(Type type, string name);
public static void RegisterBindingComposite<T>(string name = null);

Composite Bindingのクラスの型と名前をそれぞれ指定して登録します。

参考:Class InputSystem | Input System | 1.6.1

Composite Bindingはステートレス

カスタムComposite Bindingを実装する際の注意点として、Composite Bindingはステートレスでなければならないという決まりがあります。

参考:Input Bindings | Input System | 1.6.1

例えば、Composite Bindingインスタンスの中で状態変数を持つような実装をしてはいけません。

もしこのような状態変数を持った振る舞いを実現したい場合はInteractionを使用する、または併用する方が適しているかもしれません。

サンプルスクリプト

以上を踏まえたカスタムComposite Bindingの実装例です。2つのボタン入力を判定し、どちらか片方のボタンのみが押されている間だけそのボタンの入力値を返します。

XorComposite.cs
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;

public class XorComposite : InputBindingComposite<float>
{
    // ボタン1
    [InputControl(layout = "Button")] public int button1 = 0;

    // ボタン2
    [InputControl(layout = "Button")] public int button2 = 0;


    /// <summary>
    /// 初期化
    /// </summary>
#if UNITY_EDITOR
    [UnityEditor.InitializeOnLoadMethod]
#else
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
#endif
    private static void Initialize()
    {
        // 初回にCompositeBindingを登録する必要がある
        InputSystem.RegisterBindingComposite<XorComposite>(nameof(XorComposite));
    }

    /// <summary>
    /// どちらか一方のボタンが押されている場合のみ、押されているボタンの入力値を返す
    /// </summary>
    public override float ReadValue(ref InputBindingCompositeContext context)
    {
        // ボタンの押下状態取得
        var button1Pressed = context.ReadValueAsButton(button1);
        var button2Pressed = context.ReadValueAsButton(button2);

        // どちらか片方のボタンが押されている場合のみ、
        // 押されているボタンの入力値を返す
        if (button1Pressed ^ button2Pressed)
            return context.ReadValue<float>(button1Pressed ? button1 : button2);

        return 0;
    }

    /// <summary>
    /// 入力値を返す
    /// </summary>
    public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
    {
        return ReadValue(ref context);
    }
}

上記をXorComposite.csという名前でUnityプロジェクトに保存すると、カスタムComposite BindingとしてInput Action側で使えるようになります。

例えば適当なAction右の+アイコンをクリックすると、メニューに追加されていることが確認できます。

その後、各Bindingを指定してください。例では数字の「1」と「2」キーを対象としました。

検証用スクリプト(必要ならば)

以下、カスタムComposite Bindingの入力値を受け取る検証用スクリプトです。

CustomExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class CustomExample : MonoBehaviour
{
    [SerializeField] private InputActionReference _actionRef;

    private void OnDestroy()
    {
        _actionRef.action.Dispose();
    }

    private void OnEnable()
    {
        _actionRef.action.Enable();
    }

    private void OnDisable()
    {
        _actionRef.action.Disable();
    }

    private void Update()
    {
        print("入力値: " + _actionRef.action.ReadValue<float>());
    }
}

上記をCustomExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターよりカスタムComposite Bindingを設定したActionを指定してください。

実行結果

指定された2つのボタンのうち、どちらか片方のボタンが押されたときのみ入力値が出力されるようになりました。

分かりやすさのため、ボタンの押下状態を可視化しています。

どちらのボタンも押していない、または両方のボタンを押した時は入力値を出力しません。

スクリプトの説明

カスタムComposite Bindingの値を合成する処理では、どちらか一方のボタンが押されている間のみ入力値を流すようにするため、2つのボタンの押下状態の排他的論理和を取っています。

/// <summary>
/// どちらか一方のボタンが押されている場合のみ、押されているボタンの入力値を返す
/// </summary>
public override float ReadValue(ref InputBindingCompositeContext context)
{
    // ボタンの押下状態取得
    var button1Pressed = context.ReadValueAsButton(button1);
    var button2Pressed = context.ReadValueAsButton(button2);

    // どちらか片方のボタンが押されている場合のみ、
    // 押されているボタンの入力値を返す
    if (button1Pressed ^ button2Pressed)
        return context.ReadValue<float>(button1Pressed ? button1 : button2);

    return 0;
}

そして、この排他的論理和が真の時のみ押されているボタン側の入力値を返しています。それ以外は常に0です。

Composite Bindingの各種ボタン入力を判定したい場合

Composite Bindingを使用した際にあり得るケースとして、例えばWASDキーによる移動Actionがあり、なおかつWキーが押されたことを個別で判定したいケースを考えます。

この場合は、Composite Bindingとは別にWキー専用のActionを作るのが望ましいです。

注意

Input Actionからコールバックで受け取る時にどのボタンの入力なのかの判定も可能ですが、次のようなコードにしてしまうと正しく処理できません。

悪い例

private void OnMove(InputAction.CallbackContext context)
{
    if (context.control.name == "w")
    {
        // 上キーが押された処理など
    }
}

理由は、複数の方向キーが同時に押された場合に意図しない判定になるためです。

これは、例えば対象ActionのAction TypeにValueが指定されている場合などに起こります。Valueが指定されると値が変化された瞬間にコールバックが発火するため、離されたボタンが認識されたり、押されたボタンが認識されたりとまちまちです。

他にも、個別のControlをチェックすることによりInput Actionの持つ柔軟性が失われてしまうスクリプトにデバイス固有の判定ロジックが入ってしまうなど数々の副作用を引き起こすため避けるべきです。

さいごに

Input SystemのComposite Bindingを用いることで、複数のBindingの入力値をある型の1つの入力値に合成することが出来ます。

プリセットとしていくつか提供されていますが、物足りない場合は自作することも可能です。

また、Composite Bindingはステートレスでなければならず、ステートフルな処理を実装したい場合はInteractionを使う流れになるでしょう。

関連記事

参考サイト

スポンサーリンク